fix(security): clear inherited dependency CVEs and stop shipping devDependencies in the image - #6
Merged
Merged
Conversation
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>
Knip Code AnalysisFound 8 total issues
View details
Use |
|
🚫 Security audit failed Critical Failures
Warnings
Passed (36/38)
Summary: 36/38 checks passed See |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
What is actually fixed
nextpostcsssharpesbuildip-addresssocks-proxy-agent, a real runtime pathpostcssandsharpboth neededpnpm.overrides: Next pinspostcssto exactly8.4.31and declaressharpas^0.34.5, so neither moves on its own.esbuildandip-addressmoved with a plainpnpm update— no new overrides needed.Root cause fix: devDependencies no longer ship in the production image
The
schema-depsstage ran a fullpnpm installand the runner copied thatnode_modulesin for the startupdrizzle-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:fast-uri3.1.0ajv←@commitlint/clibrace-expansion1.1.13minimatch←commit-and-tag-versionjs-yaml4.1.1cosmiconfig←@commitlint/clifast-xml-builder1.1.4fast-xml-parser←commit-and-tag-versionvite7.3.2vitestThis works because
drizzle-kit,drizzle-ormandpostgresall live independencies, drizzle-kit vendors its ownesbuild/tsx, anddrizzle.config.tsalready guarded itsdotenvrequire in a try/catch for exactly this case. The stage strips thepreparescript before installing, becauseprepareruns husky — a devDependency that--prodcorrectly does not install. Removing a script does not affect the--frozen-lockfilecheck, 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 indevDependencieswill break deploys, not just builds.What is NOT fixed, and why
echarts6.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 inseries.data[i].namereaches aninnerHTMLsink. This app does not use the ECharts Lines series anywhere — the only"lines"strings insrc/are a localviewModeunion inComparisonChart.tsx, unrelated toseries.type. Exposure is nil, and it sits below the CI severity gate. Left alone on purpose:echartsis 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.trivyignoreentry added, since it does not fail anything.vite7.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
overridesnor--forcere-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 inpnpm audit, which is advisory-only insecurity-audit.yml.next16.3 or anything beyond 16.2.12. Not attempted. 16.2.12 is the currentlateston the 16.2 line and carries every fix cited in the advisories..trivyignorechangesNo suppressions were added. One was 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.node:24-alpinebase 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 run— 2865/2865 pass, 106/106 files (run withTZ=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/amd64— succeeds.tscproves nothing about a framework bump: built the image, randocker-entrypoint.shagainst a throwaway Postgres 17 container.drizzle-kit pushapplied cleanly, 16 tables created./api/health→{"status":"ok","db":"connected"}./setup→ 200 (App Router +src/proxy.tsmiddleware serving real routes on 16.2.12)./_next/image→ 200 on three different assets.--severity CRITICAL,HIGH --ignore-unfixed --ignorefile .trivyignore --exit-code 1) → exit 0. Baseline built fromorigin/mainfor 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:/setupand/loginwere 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.sharp0.35.x is a major with breaking changes —failOnErrorremoved,paletteBitDepthremoved,jp2krenamed, newlimitInputChannelsdefault. Checked Next's image optimizer for each: it uses onlysequentialRead,limitInputPixelsandsharp.concurrency, all still present. Also moot in practice — see below.postcss8.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:
sharpis not resolvable at runtime.next buildwithoutput: "standalone"traces sharp into/app/node_modules/.pnpm/but never creates the top-levelnode_modules/sharpsymlink, sorequire("sharp")throws in the container and Next silently falls back to serving/_next/imageunoptimised. 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.HEALTHCHECKhas been failing all along.wget http://localhost:3000resolves to[::1], the server binds IPv40.0.0.0, so every check getsConnection refusedand the container reportsunhealthywhile serving fine. Reproduced identically on an unmodifiedorigin/mainimage — not introduced here. Fix islocalhost→127.0.0.1in theHEALTHCHECKline. Left out of this PR on purpose to keep a security change single-purpose; worth its own one-line PR.FORK.mdis 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