React Server Components RCE
CVE-2025-55182
Blaster impact assessment, remediation, and verification runbook
This is my single, ordered record of what I checked, what I changed, why I changed it, what I intentionally did not change, and how I verified the final state for Blaster in both dev and prod Kubernetes namespaces.
It assumes:
- Repo:
games/blaster - Runtime: Kubernetes deployments in namespaces
blaster-dev(dev) andblaster(prod) - Image build: GitLab CI + Kaniko
- Delivery: Flux image automation updates the image tags in Kustomize manifests
- Migrations: executed via initContainer
npm run migrateand a custom migration runner reading/app/db/migrations/*.sql
1. Trigger: vendor outreach and risk framing
I received a message warning about CVE-2025-55182, described as a critical remote code execution (RCE) issue affecting React Server Components (RSC), with the following claims:
- Public exploits available and increased threat activity.
- Remediation guidance: upgrade Next.js to one of the patched versions listed in the message.
My first question was: Is my Blaster deployment affected?
I do not assume. For security notices, I verify the version inside the running pods, not only on my workstation.
2. Phase 1: verify facts local and in-cluster
2.1 Workstation: installed dependency versions
Run from the repo root:
node -p "require('next/package.json').version"
node -p "require('react/package.json').version"
node -p "require('react-server-dom-webpack/package.json').version" 2>/dev/null || true
I observed these versions:
14.2.33
18.3.1
If react-server-dom-webpack prints nothing, it is not installed as a direct dependency (it may still exist transitively, depending on the build).
2.2 Kubernetes: verify the version inside the running pods
Dev and prod:
kubectl -n blaster-dev exec -it deploy/blaster-app -- node -p "require('next/package.json').version"
kubectl -n blaster exec -it deploy/blaster-app -- node -p "require('next/package.json').version"
I observed:
14.2.33
14.2.33
2.3 Initial conclusion (risk posture)
Based on what I was actually running at the time of verification:
- Blaster was running Next.js 14.2.33 and React 18.3.1.
- The vendor notice explicitly targeted patched versions in Next 15.x and 16.x lines.
Therefore:
- I did not treat this as an emergency major version upgrade.
- I moved to dependency hygiene and runtime hardening, so my deployed artefacts match the security posture I am claiming.
3. Phase 2: fix dependency drift and pin versions
3.1 Why pinning mattered here
I noticed my package.json originally had:
next:"^14.2.18"which allows upgrades to newer14.xversions during installs.eslint-config-next: pinned to"14.2.18"which can drift fromnext.
This creates two problems:
- Harder to reason about exposure when an advisory lands.
- Tooling versions can diverge across machines and CI.
3.2 Actions taken: pin exact versions
I pinned Next, eslint-config-next, and React packages.
Commands:
npm install --save-exact next@14.2.33
npm install --save-dev --save-exact eslint-config-next@14.2.33
npm install --save-exact react@18.3.1 react-dom@18.3.1
4. Phase 3: interpret npm audit correctly
devDependencies versus runtime
4.1 What npm audit showed
I saw a high severity advisory (example: glob command injection) in the eslint plugin chain.
Key point:
- This was in dev tooling.
- Production dependencies were clean.
I proved it with:
npm audit --omit=dev
I observed:
found 0 vulnerabilities
4.2 What I intentionally did not do (and why)
I did NOT run npm audit fix --force
Reason:
--forcecan introduce breaking changes.- In my case it would have upgraded eslint tooling across majors (the audit output suggested installing
eslint-config-next@16.0.7), which is an unnecessary risk when the issue is dev-only.
I did NOT upgrade Next.js to 15 or 16 just because the email mentioned it.
Reason:
- I verified my runtime version first.
- A major upgrade should be done deliberately, with regression testing and a planned rollout.
5. Phase 4: CI failure after pinning (lockfile drift)
5.1 The failure
My GitLab job failed at npm ci with:
npm ci can only install packages when your package.json and package-lock.json ... are in sync
Examples of mismatches observed:
- lockfile had
dotenv@16.6.1butpackage.jsonrequireddotenv@16.4.7 - lockfile had
tsx@4.20.6butpackage.jsonrequiredtsx@4.19.2 - esbuild mismatch cascaded from the
tsxdependency chain
This is correct behaviour. npm ci is a strict lockfile install.
5.2 The fix
After changing dependency versions, I regenerated and committed package-lock.json.
Commands:
rm -rf node_modules
npm install
# sanity checks
npm ci
npm test
npm run build
git add package.json package-lock.json
git commit -m "Pin deps; update lockfile"
git push
6. Phase 5: runtime hardening to omit devDependencies
6.1 The real problem
My original Dockerfile:
- installs devDependencies in build (fine)
- then copies the entire build stage
/appinto runtime (not fine) - which includes the build stage
node_modules, unintentionally shipping devDependencies into production containers
That contradicts the “prod is clean” conclusion, because the runtime container would still carry dev tooling even if npm audit --omit=dev on my workstation is clean.
6.2 Constraint: I run migrations in the initContainer using tsx
My initContainer runs:
command: ["npm", "run", "migrate"]
My script is:
"migrate": "tsx scripts/migrate.ts"
Therefore the runtime image must include:
tsxat runtimedotenvat runtime (my migration script loads.env.localby default, though Kubernetes supplies env vars)- TS sources:
scripts/andlib/ - migration SQL files:
db/migrations/*.sql
6.3 Updated package.json
Change required:
- Move
dotenvandtsxfromdevDependenciesintodependencies(pin exact versions).
{
"name": "blaster",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "NEXT_TELEMETRY_DISABLED=1 next build",
"start": "next start",
"lint": "next lint",
"test": "npm run lint",
"migrate": "tsx scripts/migrate.ts",
"migrate:status": "tsx scripts/migrate.ts status",
"ci:install": "npm ci --no-audit --no-fund"
},
"dependencies": {
"@clerk/nextjs": "^6.35.2",
"bad-words": "^4.0.0",
"dotenv": "16.4.7",
"next": "14.2.33",
"pg": "^8.13.1",
"phaser": "^3.86.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"swr": "^2.2.5",
"tsx": "4.19.2"
},
"devDependencies": {
"@types/node": "^20",
"@types/pg": "^8.11.10",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8.57.0",
"eslint-config-next": "14.2.33",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
6.4 Updated Dockerfile
Key changes:
- Runtime stage runs
npm ci --omit=dev - Runtime stage copies only what it needs
next.config.mjsis copied explicitly (DockerCOPYdoes not support shell redirection patterns)
# syntax=docker/dockerfile:1.7
FROM node:22-alpine3.20 AS build
WORKDIR /app
ARG APP_ENV=prod
ENV APP_ENV=${APP_ENV}
ARG NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
ENV NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}
ENV NEXT_TELEMETRY_DISABLED=1
COPY package*.json ./
RUN npm ci --include=dev
COPY . .
RUN npm run build
FROM node:22-alpine3.20 AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public
COPY --from=build /app/db ./db
COPY --from=build /app/lib ./lib
COPY --from=build /app/scripts ./scripts
COPY --from=build /app/next.config.mjs ./next.config.mjs
CMD ["npm", "run", "start"]
6.5 CI output consistency
I already defined:
"ci:install": "npm ci --no-audit --no-fund"
So I can update GitLab CI lint and test jobs to use it for consistent flags:
lint:
script:
- npm run ci:install
- npm run lint
test:
script:
- npm run ci:install
- npm run test
This is optional. It does not change semantics, it just makes CI output quieter and consistent.
7. Phase 6: confirm other repo
7.1 .dockerignore
My .dockerignore excludes node_modules, k8s/, and other non-runtime files, but does not exclude:
db/lib/scripts/public/
Therefore the new Dockerfile will still ship migrations and migration runner code.
7.2 next.config.mjs
My next.config.mjs modifies webpack config and should be copied into the runtime image to avoid configuration drift. I copy it explicitly in the Dockerfile runtime stage.
8. Verification: commands and expected results
dev then prod.
8.1 Verify in dev (blaster-dev)
Confirm Next version
kubectl -n blaster-dev exec -it deploy/blaster-app -- sh -lc 'node -p "require(\"next/package.json\").version"'
Expected:
14.2.33
Confirm migrations exist in the container
kubectl -n blaster-dev exec -it deploy/blaster-app -- sh -lc 'ls -la /app/db/migrations | head'
Expected:
- lists
001_init.sqletc.
Confirm migration runner works and DB is up to date
kubectl -n blaster-dev exec -it deploy/blaster-app -- sh -lc 'npm run migrate:status'
Expected:
- a list of migrations 001..008 as ✓
Confirm runtime has the migration deps (by design)
kubectl -n blaster-dev exec -it deploy/blaster-app -- sh -lc 'node -p "require(\"tsx/package.json\").version"; node -p "require(\"dotenv/package.json\").version"'
Expected:
4.19.216.4.7
Confirm runtime does not include dev tooling
kubectl -n blaster-dev exec -it deploy/blaster-app -- sh -lc 'npm ls eslint eslint-config-next @next/eslint-plugin-next --omit=dev || true'
Expected:
(empty)or missing packages
8.2 Verify in prod (blaster)
Confirm initContainer and app container use the same image tag
kubectl -n blaster get deploy blaster-app -o=jsonpath='{.spec.template.spec.initContainers[0].image}{"\n"}{.spec.template.spec.containers[0].image}{"\n"}'
Expected:
- both lines identical (for example
registry.reids.net.au/games/blaster:prod-20251207.181)
Confirm Next version
kubectl -n blaster exec -it deploy/blaster-app -- sh -lc 'node -p "require(\"next/package.json\").version"'
Expected:
14.2.33
Confirm migration deps are pinned as expected
kubectl -n blaster exec -it deploy/blaster-app -- sh -lc 'node -p "require(\"tsx/package.json\").version"; node -p "require(\"dotenv/package.json\").version"'
Expected:
4.19.216.4.7
Confirm migration status
kubectl -n blaster exec -it deploy/blaster-app -- sh -lc 'npm run migrate:status'
Expected:
- a list of migrations 001..008 as ✓ (timestamps will differ from dev)
Confirm runtime does not include dev tooling
kubectl -n blaster exec -it deploy/blaster-app -- sh -lc 'npm ls eslint eslint-config-next @next/eslint-plugin-next --omit=dev || true'
Expected:
(empty)or missing packages
9 Verification checklist
- Next.js version inside dev pods is
14.2.33 - Next.js version inside prod pods is
14.2.33 - Runtime has
tsx@4.19.2anddotenv@16.4.7 -
npm run migrate:statussucceeds in dev and prod -
db/migrationsexists inside the container at/app/db/migrations - Runtime container does not include eslint tooling packages
- initContainer and app container use the same image tag in prod
- GitLab CI runs
npm cisuccessfully (package.json and lockfile are in sync)
10. Summary
- I did not panic-upgrade Blaster to Next 15 or 16.
- I verified the versions inside the running pods first.
- I pinned versions and fixed lockfile drift so CI behaves deterministically.
- I hardened the Docker runtime image so it does not ship devDependencies.
- I verified dev and prod in-cluster with repeatable kubectl commands.
If I later want to remove TypeScript execution from runtime entirely, I will switch migrations to compiled JavaScript and run them with node. That will be a separate and planned improvement.