Skip to content

security: hardening release v1.2.0#3

Merged
BrunoAFK merged 12 commits into
mainfrom
security/v1.2.0
Apr 11, 2026
Merged

security: hardening release v1.2.0#3
BrunoAFK merged 12 commits into
mainfrom
security/v1.2.0

Conversation

@BrunoAFK
Copy link
Copy Markdown
Owner

Summary

Security hardening release. See .releases/1.2.0.md for the full changelog.

What changes

  • Full SSRF guard via new safeFetch (DNS pinning, redirect re-validation, IP allowlist incl. CGNAT/multicast/IPv6).
  • All 14 outbound-HTTP handlers migrated from raw axios to safeFetch.
  • Screenshot handler: SSRF gate + remove --no-sandbox, switch to user-namespace sandbox.
  • Non-root containers (api, web, cli) with cap_drop ALL, read_only rootfs, no-new-privileges.
  • CORS default tightened from * to off-by-default explicit allowlist.
  • Rate limit default tightened to 200 / 1 minute.
  • Strict CSP + security headers in nginx.
  • cosign keyless signing of release images + SPDX SBOM via syft.
  • Helm chart removed.

Breaking changes

  • CORS default off (was wildcard). Set API_CORS_ORIGIN.
  • Web container listens on 8080 internally (was 80).
  • Helm chart removed; Docker Compose is the recommended deployment.
  • Chromium screenshot needs SYS_ADMIN + seccomp=unconfined on the api container.

Test plan

  • Full vitest suite green (npm test, 142 tests)
  • Manual docker compose up smoke (api healthy, web serves CSP headers)
  • id -u on each image returns non-zero
  • cosign job will run on release publish

BrunoAFK added 12 commits April 10, 2026 21:25
The Helm chart was unmaintained and not production-hardened (no securityContext,
no resource limits, no NetworkPolicy, plaintext secrets in values.yaml).
Recommended deployment is Docker Compose. K8s users can write a fresh chart
against the v1.2.0 images.
…, mapped IPv6

Adds CIDR-based matching for previously uncovered ranges:
- IPv4: 0.0.0.0/8, 100.64.0.0/10 (CGNAT), 198.18.0.0/15 (benchmark),
  224.0.0.0/4 (multicast), 240.0.0.0/4 (reserved), TEST-NETs.
- IPv6: ::1, fc00::/7 (ULA), fe80::/10 (link-local), ff00::/8 (multicast),
  2001:db8::/32 (documentation), and IPv4-mapped addresses which are
  re-evaluated against the IPv4 blocklist.

Adds RECON_ALLOW_PRIVATE_IPS=1 escape hatch for local testing.
Adds SsrfBlockedError class for typed error handling.
assertPublicHost now returns the resolved IP for callers to pin against.
…ct revalidation

New utility wrapping axios with:
- Protocol allowlist (http/https only)
- DNS resolution + assertPublicHost on the resolved IP
- Connection pinning to the validated IP (defeats DNS rebinding)
- Manual redirect handling with full re-validation on every Location
- Max body size (default 10 MiB), max redirects (default 5), total
  timeout (default 30s), connect timeout (default 5s)

All handlers that perform outbound HTTP will be migrated to safeFetch
in subsequent commits. RECON_ALLOW_PRIVATE_IPS=1 escape hatch supported.
…RF guard)

Migrates 14 handlers from raw axios.get() to safeFetch:
archives, cookies, firewall, headers, http-security, legacy-rank,
linked-pages, quality, rank, robots-txt, sitemap, social-tags,
tech-stack, threats.

Each handler now:
- Validates URL protocol before fetching.
- Resolves DNS and rejects private/internal IPs.
- Pins connection to the validated IP (DNS rebinding defense).
- Re-validates redirect targets before following.
- Returns { error: 'Blocked: ...' } on SsrfBlockedError instead of leaking
  the underlying network error.

Adds per-handler SSRF integration tests using mocked dns.lookup.
The threats handler aggregated sub-handler errors, which wrapped
SsrfBlockedError into composite strings. Adds a top-level
assertPublicHost gate so SSRF rejection returns the canonical
{ error: 'Blocked: target resolves to private address' } message
before any sub-handler runs.

Tightens the threats.test.ts SSRF assertion to verify the canonical
error message pattern, preventing regressions where SSRF protection
silently stops working in this handler.
Adds URL validation (protocol allowlist + assertPublicHost) before
invoking Chromium. The application-layer guard is the primary SSRF
defense — even if Chromium itself follows a redirect, the only URLs it
sees are pre-validated.

Replaces --no-sandbox with --disable-setuid-sandbox + --disable-dev-shm-usage.
The container now requires CAP_SYS_ADMIN + seccomp=unconfined so Chromium
can create its user namespace sandbox; this is documented inline.

Adds chrome-sandbox setuid fixup in api Dockerfile (conditional on the
helper being shipped by the chromium package).
CORS no longer defaults to wildcard '*' — must be explicitly configured
via API_CORS_ORIGIN as a comma-separated allowlist. Empty/unset disables
CORS entirely.

Rate limit default tightened to 200 / 1 minute (was 100 / 10 minutes,
which is roughly the same average but with much larger burst tolerance).
Per-route overrides for auth endpoints are added by the pro overlay.
Adds Content-Security-Policy, X-Frame-Options, X-Content-Type-Options,
Referrer-Policy, Permissions-Policy, and an opt-in HSTS header (gated
by HSTS_HEADER env, empty by default for non-TLS deployments).

CSP allows 'unsafe-inline' on style-src only — required for Tailwind's
atomic class system. script-src is strict 'self', which blocks injected
inline JS even if XSS surfaces appear in future.
api: USER 10001, mkdir/chown /app and /home/app, cap_drop ALL, cap_add
SYS_ADMIN only for Chromium namespace sandbox, read-only rootfs with
tmpfs for /tmp and /home/app/.cache.

web: nginx user (UID 101), rebind listen to 8080 (unprivileged), update
compose port mapping accordingly, cap_drop ALL, read-only rootfs with
tmpfs for /var/cache/nginx and /var/run.

cli: USER 10001 (alpine), cap_drop ALL.

All three images now reject privilege escalation via no-new-privileges.
Adds id-token: write permission required by cosign keyless OIDC. After
the existing docker push step, the workflow now:
- Signs each pushed image (api, web, cli) with cosign keyless using the
  GitHub Actions OIDC identity. Signatures are stored in the GHCR
  signature mirror and verifiable via cosign verify --certificate-identity.
- Generates an SPDX-JSON SBOM via syft for each image.
- Attaches the SBOM artifact to the GitHub release.

Existing Trivy scans remain in the docker-build job.
Bumps all packages to 1.2.0 and writes the v1.2.0 release notes
documenting the security hardening changes. Adds a SECURITY section
to the README pointing operators at the new defaults and verification
instructions for cosign-signed images.
Resolves:
- axios CVE-2025-62718 (CRITICAL)
- axios CVE-2026-40175 (CRITICAL)
- basic-ftp GHSA-6v7q-wjvx-w8wg (HIGH CRLF injection)

Unblocks the v1.2.0 release pipeline which the Trivy filesystem scan
was rejecting on these three advisories.
@BrunoAFK BrunoAFK merged commit 54f4321 into main Apr 11, 2026
7 checks passed
@BrunoAFK BrunoAFK deleted the security/v1.2.0 branch April 11, 2026 06:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant