Skip to content

fix(security): clear inherited dependency CVEs and stop shipping devDependencies in the image - #6

Merged
patrickdundas merged 3 commits into
mainfrom
fix/dependency-cves
Jul 29, 2026
Merged

fix(security): clear inherited dependency CVEs and stop shipping devDependencies in the image#6
patrickdundas merged 3 commits into
mainfrom
fix/dependency-cves

Conversation

@patrickdundas

Copy link
Copy Markdown
Owner

Clears every CRITICAL/HIGH Trivy finding from the production image. All of these were inherited from upstream's lockfile; none were introduced by the fork.

Trivy image scan, CI settings (CRITICAL,HIGH, --ignore-unfixed): 39 findings → 0.
Only one finding remains at any severity, down from 39 HIGH plus the moderate/low tail.

Needs a version bump to deploy. package.json is deliberately left at 2.8.9-homelab.2release.yml gates on the version having no matching GitHub Release, so nothing ships until you bump it. Bump to 2.8.9-homelab.3 when you decide to deploy.


What is actually fixed

Package Before After Advisories cleared
next 16.2.2 16.2.12 CVE-2026-64642 (middleware/proxy bypass in App Router), CVE-2026-64645 (SSRF in rewrites), CVE-2026-64649 (SSRF in Server Actions), CVE-2026-64641 (DoS in Server Actions), CVE-2026-45109 (info disclosure via middleware fix bypass), CVE-2026-44578 (SSRF via WebSocket upgrade), CVE-2026-44579 (DoS via POST), CVE-2026-44573 (info disclosure), CVE-2026-44574 (authz bypass via query params), CVE-2026-44575 (unauthorized access via middleware), GHSA-8h8q-6873-q5fj + GHSA-q4gf-8mx6-v5v3 (RSC DoS) — 12 total
postcss 8.4.31 and 8.5.9 8.5.24 CVE-2026-45623, GHSA-r28c-9q8g-f849 (path traversal in source-map auto-loading)
sharp 0.34.5 0.35.3 GHSA-f88m-g3jw-g9cj (inherited libvips CVEs)
esbuild 0.28.0 0.28.1 GHSA-g7r4-m6w7-qqqr (LOW)
ip-address 10.1.0 10.3.1 CVE-2026-42338 (MODERATE) — reached via socks-proxy-agent, a real runtime path

postcss and sharp both needed pnpm.overrides: Next pins postcss to exactly 8.4.31 and declares sharp as ^0.34.5, so neither moves on its own. esbuild and ip-address moved with a plain pnpm update — no new overrides needed.

Root cause fix: devDependencies no longer ship in the production image

The schema-deps stage ran a full pnpm install and the runner copied that node_modules in for the startup drizzle-kit push. vitest's entire tree shipped to production. That stage now installs with --prod, which removed these on its own — no version bumps involved:

Package Findings removed Came from
fast-uri 3.1.0 4 HIGH ajv@commitlint/cli
brace-expansion 1.1.13 2 HIGH minimatchcommit-and-tag-version
js-yaml 4.1.1 1 HIGH cosmiconfig@commitlint/cli
fast-xml-builder 1.1.4 1 HIGH fast-xml-parsercommit-and-tag-version
vite 7.3.2 1 HIGH vitest

This works because drizzle-kit, drizzle-orm and postgres all live in dependencies, drizzle-kit vendors its own esbuild/tsx, and drizzle.config.ts already guarded its dotenv require in a try/catch for exactly this case. The stage strips the prepare script before installing, because prepare runs husky — a devDependency that --prod correctly does not install. Removing a script does not affect the --frozen-lockfile check, which compares dependency specifiers only.

Side effects: image 866 MB → 671 MB; runner's schema-sync tree from the full dependency graph down to 29 top-level packages.

Consequence to remember: anything required at container startup must now be a real dependency. A new startup requirement living in devDependencies will break deploys, not just builds.

What is NOT fixed, and why

echarts 6.0.0 → 6.1.0 — CVE-2026-45249, MODERATE. Deliberately not bumped.
The only finding left in the image at any severity. It is an XSS in the Lines series tooltip: with no custom tooltip.formatter, a raw HTML string in series.data[i].name reaches an innerHTML sink. This app does not use the ECharts Lines series anywhere — the only "lines" strings in src/ are a local viewMode union in ComparisonChart.tsx, unrelated to series.type. Exposure is nil, and it sits below the CI severity gate. Left alone on purpose: echarts is a minor bump to the charting library that the UI-redesign workstream is actively editing, and a rendering regression would land on them. Your call whether to take it — it is a one-line change once that work settles. No .trivyignore entry added, since it does not fail anything.

vite 7.3.2 — CVE-2026-53571. Still in the lockfile, no longer in the image.
vitest 4.1.4's peer range holds vite at 7.3.2 and neither overrides nor --force re-resolves it, exactly as documented before. It now stops mattering for the gate that counts: vite never reaches the production image. It will still surface in pnpm audit, which is advisory-only in security-audit.yml.

next 16.3 or anything beyond 16.2.12. Not attempted. 16.2.12 is the current latest on the 16.2 line and carries every fix cited in the advisories.

.trivyignore changes

No suppressions were added. One was removed.

  • Removed CVE-2026-53571 (vite). vite is no longer in the image at all, so keeping the entry would only hide a regression — if a devDependency ever leaks back into a production image, Trivy should fail loudly.
  • Kept the esbuild Go-stdlib block and the Alpine zlib / minimatch / node-tar base-image entries, unchanged. Worth knowing: none of those IDs appear in a scan of the current image — esbuild is now 0.28.1 and the node:24-alpine base has moved on. They are retained only so a base-image regression does not turn CI red overnight, and a note to that effect was added inline. They can be pruned in a follow-up if you would rather they fail loudly too.

Verification

Everything below was run against this branch, not inferred.

  • pnpm exec tsc --noEmit — clean.
  • pnpm exec biome check . — clean, 551 files.
  • pnpm exec vitest run2865/2865 pass, 106/106 files (run with TZ=UTC; the one otherwise-failing test, chart-transforms.test.ts, is the known pre-existing timezone-dependent one and passes in UTC, which is what CI runs in).
  • docker build --platform linux/amd64succeeds.
  • End-to-end runtime test, since a green tsc proves nothing about a framework bump: built the image, ran docker-entrypoint.sh against a throwaway Postgres 17 container.
    • drizzle-kit push applied cleanly, 16 tables created.
    • /api/health{"status":"ok","db":"connected"}.
    • /setup → 200 (App Router + src/proxy.ts middleware serving real routes on 16.2.12).
    • /_next/image → 200 on three different assets.
    • Container restarted to prove the schema push is idempotent against an existing schema — the real deploy path. Clean on the second run.
  • Trivy run locally with the CI invocation (--severity CRITICAL,HIGH --ignore-unfixed --ignorefile .trivyignore --exit-code 1) → exit 0. Baseline built from origin/main for a like-for-like comparison: 39 findings.

Risk assessment: the Next.js bump

16.2.2 → 16.2.12 is a patch-level move inside the same minor — much lower risk than the 15.x → 16.x jump the advisories imply. Next's declared dependencies are identical between the two versions apart from @next/* version strings. Still, here is what tests would not catch:

  • src/proxy.ts (middleware). Half the fixed CVEs are middleware/proxy bypasses, so the matcher and request-handling paths are exactly what changed upstream. A behavioural tightening here shows up as auth redirects, not as a test failure. Partly mitigated: /setup and /login were exercised against the running container and routed correctly, but only unauthenticated paths. An authenticated session was not tested. Worth a manual login immediately after deploy.
  • Server Actions / RSC payload handling. CVE-2026-64641 and CVE-2026-64649 patch Server Action request parsing. If any action posts an unusual payload shape, stricter validation could reject it at runtime. Unit tests exercise the handlers directly, not through the Next action pipeline.
  • sharp 0.35.x is a major with breaking changesfailOnError removed, paletteBitDepth removed, jp2k renamed, new limitInputChannels default. Checked Next's image optimizer for each: it uses only sequentialRead, limitInputPixels and sharp.concurrency, all still present. Also moot in practice — see below.
  • postcss 8.4.31 → 8.5.24 crosses Next's exact pin. It only runs at build time; the Docker build succeeded, which is the real test.

Not risks, but worth knowing — both pre-existing, both found while verifying:

  1. sharp is not resolvable at runtime. next build with output: "standalone" traces sharp into /app/node_modules/.pnpm/ but never creates the top-level node_modules/sharp symlink, so require("sharp") throws in the container and Next silently falls back to serving /_next/image unoptimised. Confirmed on the baseline image too — version-independent. The sharp bump is a genuine fix for what ships on disk, but it changes no runtime behaviour, because sharp is not being loaded today.
  2. The container's HEALTHCHECK has been failing all along. wget http://localhost:3000 resolves to [::1], the server binds IPv4 0.0.0.0, so every check gets Connection refused and the container reports unhealthy while serving fine. Reproduced identically on an unmodified origin/main image — not introduced here. Fix is localhost127.0.0.1 in the HEALTHCHECK line. Left out of this PR on purpose to keep a security change single-purpose; worth its own one-line PR.

FORK.md is updated: the "devDependencies ship in the production image" section is rewritten as fixed, and both pre-existing quirks above are recorded.

🤖 Generated with Claude Code

patrickdundas and others added 2 commits July 29, 2026 00:18
Bumps the three runtime packages carrying HIGH-severity advisories in the
inherited lockfile. All are patch- or override-level moves; no API surface
this app uses changed.

- next 16.2.2 -> 16.2.12. Clears 12 HIGH advisories including the App Router
  middleware/proxy bypass (CVE-2026-64642), SSRF in rewrites (CVE-2026-64645)
  and in Server Actions (CVE-2026-64649).
- postcss 8.4.31/8.5.9 -> 8.5.24 via override. Next pins 8.4.31 exactly, so
  the override is the only way to move it. Clears CVE-2026-45623 and
  GHSA-r28c-9q8g-f849.
- sharp 0.34.5 -> 0.35.3 via override. Next declares ^0.34.5 so an override is
  required. Clears the inherited libvips CVEs in GHSA-f88m-g3jw-g9cj.
- esbuild 0.28.0 -> 0.28.1 and ip-address 10.1.0 -> 10.3.1, both plain updates
  within their existing ranges.

Verified: tsc clean, biome clean, 2865/2865 vitest pass, docker build succeeds,
and the built image boots against a live Postgres.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The schema-deps stage ran a full `pnpm install` and the runner copied that
node_modules in for the startup drizzle-kit push, so vitest's entire tree —
vite, jsdom, undici, typescript — landed in production and Trivy flagged it.
Installing with `--prod` removes a whole class of findings permanently.

drizzle-kit, drizzle-orm and postgres all live in `dependencies`, and
drizzle-kit vendors its own esbuild/tsx, so the schema push keeps everything it
needs. drizzle.config.ts already guarded its dotenv require in a try/catch. The
`prepare` script is stripped before installing because it runs husky, a
devDependency that `--prod` correctly does not install; removing a script does
not affect the --frozen-lockfile check.

Proven, not assumed: built the image and ran docker-entrypoint.sh against a
throwaway Postgres. Schema push applied 16 tables, /api/health returned
{"status":"ok","db":"connected"}, /setup rendered 200, and a container restart
re-ran the push idempotently against the existing schema.

Image 866 MB -> 671 MB; schema-sync tree down to 29 top-level packages.

Drops the vite suppression from .trivyignore rather than keeping it: vite is no
longer in the image at all, so a dead suppression would only hide a regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Knip Code Analysis

Found 8 total issues

Category Count
Unused Dependencies 1
Unused Dev Dependencies 3
Unused Exports 4
View details

Run pnpm knip locally to see the full report.

Use pnpm knip:filter pattern to filter results by file path.


Use /** @public */ JSDoc tags to mark intentionally exported symbols.

@github-actions

Copy link
Copy Markdown

🚫 Security audit failed

Critical Failures

Check File Details
External fetch calls have timeouts src/lib/adapters/btn.ts:69 fetch() call without a timeout signal — add signal: AbortSignal.timeout(ms)

Warnings

Check File Details
No swallowed errors in catch blocks src/lib/adapters/iptorrents.ts:108 catch block swallows error without logging or re-throwing
No swallowed errors in catch blocks src/lib/adapters/iptorrents.ts:114 catch block swallows error without logging or re-throwing
No swallowed errors in catch blocks src/lib/adapters/iptorrents.ts:175 catch block swallows error without logging or re-throwing
No swallowed errors in catch blocks src/lib/adapters/iptorrents.ts:181 catch block swallows error without logging or re-throwing

Passed (36/38)

  • ✅ Auth enforcement on protected routes (per-handler)
  • ✅ No dangerous functions (eval, innerHTML, etc.)
  • ✅ No hardcoded secrets in source
  • ✅ Security headers in next.config.ts
  • ✅ Cookie security (httpOnly, sameSite, secure)
  • ✅ No sensitive fields in API responses
  • ✅ No .env files committed to repo
  • ✅ No raw SQL in API routes
  • ✅ No fetch/redirect with unvalidated URLs in routes
  • ✅ Timing-safe comparison for secret values
  • ✅ No raw SQL migration files (schema-first only)
  • ✅ Docker container runs as non-root user
  • ✅ Public routes match proxy allowlist
  • ✅ File delete operations have path traversal defense
  • ✅ Password hashing uses Argon2 (not SHA-256/bcrypt)
  • ✅ Encrypted columns written via encrypt()
  • ✅ TOTP 2FA flow integrity
  • ✅ Emergency lockdown flow integrity
  • ✅ Scrub & delete (nuke) flow integrity
  • ✅ Backup restore flow integrity
  • ✅ Login flow integrity
  • ✅ Auth result checked before proceeding
  • ✅ Backup password inputs bounded before key derivation
  • ✅ Webhook delivery fetch uses redirect: "error"
  • ✅ SESSION_SECRET minimum-length guard in auth/crypto modules
  • ✅ Notification URL validators include SSRF protection
  • ✅ Dockerfile does not COPY sensitive files
  • ✅ No secret env vars in client components
  • ✅ Adapter Cookie headers guard against injection
  • ✅ Adapter files do not log credential values
  • ✅ No console.log in API routes
  • ✅ No TODO/FIXME in security-critical files
  • ✅ JSON.parse wrapped in try-catch
  • ✅ Request body size validation on upload routes
  • ✅ BigInt fields use string serialization
  • ✅ No raw error messages in API responses

Summary: 36/38 checks passed
1 critical failure(s)
— 1 warning(s)

See scripts/security-audit.ts for check definitions and SECURITY.md for the full security architecture.

release.yml only publishes when package.json's version has no matching
GitHub Release, so the dependency CVE fixes need a bump to reach the server.

Ships: next 16.2.2 -> 16.2.12, postcss -> 8.5.24, sharp -> 0.35.3,
ip-address -> 10.3.1, esbuild -> 0.28.1, and the schema-deps stage installing
with --prod so devDependencies no longer ship in the production image.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@patrickdundas
patrickdundas merged commit 67c98ea into main Jul 29, 2026
6 checks passed
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