fix(security): patch HIGH/CRITICAL container image vulnerabilities #74

Merged
eliott merged 1 commit from security/2026-10-image-patching into main 2026-10-28 09:31:59 +01:00
Owner

Why

The vulan/Grype scan of the running rollnote-api container found 81 HIGH/CRITICAL findings. 47 of them came from esbuild binaries (Go 1.20.7 / 1.23.12 stdlib) that should never have been in the runtime image.

What changed

Dev tooling leaking into prod (.pnpmfile.cjs, new)

  • better-auth declares drizzle-kit as an optional peer. pnpm resolved that peer to our drizzle-kit devDependency and kept it in pnpm install --prod, so the runtime shipped drizzle-kit, tsx, @esbuild-kit/* and three esbuild binaries (0.18.20, 0.25.12, 0.28.0).
  • A readPackage hook drops that peer. Only better-auth's CLI schema generator uses it, and nothing in better-auth's dist or in src/ imports it at runtime. pnpm why --prod esbuild drizzle-kit tsx now returns nothing.
  • The Dockerfile copies .pnpmfile.cjs with the manifests, because its checksum is in the lockfile. .fallowrc.json lists it as an entry, otherwise the fallow audit flags it as an unused file.

Direct deps

  • better-auth, @better-auth/passkey: ~1.6.33. @better-auth/expo: 1.6.33. The tilde keeps them on the 1.6 line, because ^ resolved to 1.7.13.
  • adm-zip ^0.6.1. It now ships its own types, so @types/adm-zip is dropped. It also rejects archives with duplicate entry names; see the release notes.
  • nodemailer ^10.2.1 (8 → 10). The breaking changes are Node ≥ 20 and TLS checks when fetching remote attachments/OAuth; we only call createTransport + sendMail.
  • zod ^4.6.7.

Transitive deps (overrides in pnpm-workspace.yaml, where this repo keeps its pnpm settings)

  • Nest 11 / express 5.2.1 pin these, so overrides are needed: multer@2 → ^2.5.0, body-parser@2 → ^2.4.0, proxy-addr@2 → ^2.0.8, js-yaml@4 → ^4.3.2, brace-expansion@2 → ^2.1.7.
  • Updated within their ranges: qs 6.16.1 and @simplewebauthn/server 13.3.3 (Medium/Low).

Image

  • NODE_IMAGE node:24.21-alpine → node:24.22-alpine (24.22.1, Alpine 3.24.3, zlib 1.3.2-r1).
  • The runtime stage removes npm, npx, corepack and yarn. The app starts with node dist/src/main.js and never uses them. This clears the npm-bundled tar / undici / ip-address / brace-expansion findings.

Scan: before → after

before (prod container) after (local podman build)
Grype --only-fixed, HIGH/CRITICAL 81 0
Grype --only-fixed, any severity — 0

Verification (local)

  • pnpm lint, tsc --noEmit, pnpm build: pass.
  • pnpm openapi:generate: no drift.
  • fallow audit 2.88.3: no issues.
  • pnpm test: 302/302 pass.
  • pnpm test:int (testcontainers on rootless podman): 34 suites, 333/333 pass.
  • podman build: OK. The runtime node_modules holds no esbuild, drizzle-kit or tsx, and /usr/local/bin has only node.
  • Smoke boot of the image (read-only rootfs) against a throwaway Postgres 18.6: migrations and seed run, Nest starts, and /health returns {"status":"ok"}.

What to check after deploy

  • Coolify pulls node:24.22-alpine, and the build log shows the rm -rf …npm… step.
  • Boot log: migrations + seed OK, healthcheck green.
  • Auth (better-auth 1.6.12 → 1.6.33): email/password sign-in, passkey sign-in, 2FA OTP, and Expo app login.
  • Email (nodemailer 10): trigger a password reset or verification email and confirm it arrives.
  • Import (adm-zip 0.6, multer 2.5): import a ZIP made by the current export.

Follow-ups (not done here)

  • NestJS 12 (@nestjs/* 12.x, @nestjs/swagger 12 with js-yaml 5, multer/express bumped upstream). That is a framework major; once on it, the multer/body-parser/proxy-addr/js-yaml overrides can go.
  • better-auth 1.7.x (minor, but may need schema/migration work). Keep the front on the same line.
  • drizzle-kit still pulls the deprecated @esbuild-kit/* (dev only, no longer in the image).
  • typescript and @types/node / @types/pg still reach the prod tree as peers of @thallesp/nestjs-better-auth and drizzle-orm. They are harmless (no binaries, no findings) and can be dropped with the same pnpmfile hook if wanted.
  • Base images are pinned by minor tag; the next Alpine/npm CVE needs another tag bump.

https://claude.ai/code/session_011Qbv9zPFHL8Ax1zQWXSov3

## Why The vulan/Grype scan of the running `rollnote-api` container found **81 HIGH/CRITICAL findings**. 47 of them came from esbuild binaries (Go 1.20.7 / 1.23.12 stdlib) that should never have been in the runtime image. ## What changed **Dev tooling leaking into prod (`.pnpmfile.cjs`, new)** - `better-auth` declares `drizzle-kit` as an optional peer. pnpm resolved that peer to our `drizzle-kit` devDependency and kept it in `pnpm install --prod`, so the runtime shipped `drizzle-kit`, `tsx`, `@esbuild-kit/*` and three esbuild binaries (0.18.20, 0.25.12, 0.28.0). - A `readPackage` hook drops that peer. Only better-auth's CLI schema generator uses it, and nothing in better-auth's dist or in `src/` imports it at runtime. `pnpm why --prod esbuild drizzle-kit tsx` now returns nothing. - The Dockerfile copies `.pnpmfile.cjs` with the manifests, because its checksum is in the lockfile. `.fallowrc.json` lists it as an entry, otherwise the fallow audit flags it as an unused file. **Direct deps** - `better-auth`, `@better-auth/passkey`: `~1.6.33`. `@better-auth/expo`: `1.6.33`. The tilde keeps them on the 1.6 line, because `^` resolved to 1.7.13. - `adm-zip` `^0.6.1`. It now ships its own types, so `@types/adm-zip` is dropped. It also rejects archives with duplicate entry names; see the release notes. - `nodemailer` `^10.2.1` (8 → 10). The breaking changes are Node ≥ 20 and TLS checks when fetching remote attachments/OAuth; we only call `createTransport` + `sendMail`. - `zod` `^4.6.7`. **Transitive deps (`overrides` in `pnpm-workspace.yaml`, where this repo keeps its pnpm settings)** - Nest 11 / express 5.2.1 pin these, so overrides are needed: `multer@2` → ^2.5.0, `body-parser@2` → ^2.4.0, `proxy-addr@2` → ^2.0.8, `js-yaml@4` → ^4.3.2, `brace-expansion@2` → ^2.1.7. - Updated within their ranges: `qs` 6.16.1 and `@simplewebauthn/server` 13.3.3 (Medium/Low). **Image** - `NODE_IMAGE` `node:24.21-alpine` → `node:24.22-alpine` (24.22.1, Alpine 3.24.3, zlib 1.3.2-r1). - The runtime stage removes npm, npx, corepack and yarn. The app starts with `node dist/src/main.js` and never uses them. This clears the npm-bundled `tar` / `undici` / `ip-address` / `brace-expansion` findings. ## Scan: before → after | | before (prod container) | after (local `podman build`) | |---|---|---| | Grype `--only-fixed`, HIGH/CRITICAL | 81 | **0** | | Grype `--only-fixed`, any severity | — | 0 | ## Verification (local) - `pnpm lint`, `tsc --noEmit`, `pnpm build`: pass. - `pnpm openapi:generate`: no drift. - `fallow audit` 2.88.3: no issues. - `pnpm test`: 302/302 pass. - `pnpm test:int` (testcontainers on rootless podman): 34 suites, 333/333 pass. - `podman build`: OK. The runtime `node_modules` holds no esbuild, drizzle-kit or tsx, and `/usr/local/bin` has only `node`. - Smoke boot of the image (read-only rootfs) against a throwaway Postgres 18.6: migrations and seed run, Nest starts, and `/health` returns `{"status":"ok"}`. ## What to check after deploy - Coolify pulls `node:24.22-alpine`, and the build log shows the `rm -rf …npm…` step. - Boot log: migrations + seed OK, healthcheck green. - **Auth** (better-auth 1.6.12 → 1.6.33): email/password sign-in, passkey sign-in, 2FA OTP, and Expo app login. - **Email** (nodemailer 10): trigger a password reset or verification email and confirm it arrives. - **Import** (adm-zip 0.6, multer 2.5): import a ZIP made by the current export. ## Follow-ups (not done here) - NestJS 12 (`@nestjs/*` 12.x, `@nestjs/swagger` 12 with js-yaml 5, multer/express bumped upstream). That is a framework major; once on it, the multer/body-parser/proxy-addr/js-yaml overrides can go. - better-auth 1.7.x (minor, but may need schema/migration work). Keep the front on the same line. - `drizzle-kit` still pulls the deprecated `@esbuild-kit/*` (dev only, no longer in the image). - `typescript` and `@types/node` / `@types/pg` still reach the prod tree as peers of `@thallesp/nestjs-better-auth` and `drizzle-orm`. They are harmless (no binaries, no findings) and can be dropped with the same pnpmfile hook if wanted. - Base images are pinned by minor tag; the next Alpine/npm CVE needs another tag bump. https://claude.ai/code/session_011Qbv9zPFHL8Ax1zQWXSov3
- Keep drizzle-kit out of the runtime image: better-auth's optional
  drizzle-kit peer was resolved to our devDependency and survived
  `pnpm install --prod`, shipping drizzle-kit, tsx and three esbuild
  binaries (Go 1.20/1.23 stdlib CVEs). A .pnpmfile.cjs readPackage hook
  drops that peer; nothing at runtime imports drizzle-kit.
- Bump direct deps: better-auth/@better-auth/* 1.6.33 (stays on 1.6),
  adm-zip 0.6.1 (ships its own types, drop @types/adm-zip),
  nodemailer 10.2.1, zod 4.6.7.
- Override transitive deps pinned by Nest 11 / express 5.2: multer 2.5.0,
  body-parser 2.4.0, proxy-addr 2.0.8, js-yaml 4.3.2, brace-expansion 2.1.7;
  refresh qs and @simplewebauthn/server in range.
- Node image 24.21-alpine -> 24.22-alpine (zlib 1.3.2-r1) and remove
  npm/corepack/yarn from the runtime stage (app starts with `node`).

Grype (--only-fixed) on the built image: 81 findings -> 0.

Claude-Session: https://claude.ai/code/session_011Qbv9zPFHL8Ax1zQWXSov3
eliott merged commit 17e465b704 into main 2026-10-28 09:31:59 +01:00
eliott deleted branch security/2026-10-image-patching 2026-10-28 09:31:59 +01:00
Sign in to join this conversation.
No reviewers
No labels
run-ci
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
RollNote/rollnote-api!74
No description provided.