Skip to content

fix: pin patched Node/Bun/Chrome and raise Node floor for Brotli CVE-2026-111497 - #5

Open
teamkareem wants to merge 3 commits into
mainfrom
cursor/brotli-cve-node-pin-bc39
Open

teamkareem wants to merge 3 commits into
mainfrom
cursor/brotli-cve-node-pin-bc39

Conversation

@teamkareem

@teamkareem teamkareem commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

What

Remediation for CVE-2026-111497 / GHSA-9v8h-qqgm-4m35, a critical out-of-bounds write in Google Brotli's decoder (1.0.3–1.2.0). This PR does three things:

  1. Pins every Docker image and CI job to Node, Bun and Chrome releases that include the fix.
  2. Raises the published Node floor to ^22.23.4 || ^24.22.1 || >=26.12.1 (approved by Kareem).
  3. Enforces that floor at runtime. The CLI refuses to start on an unpatched runtime. hyperframes doctor stays runnable so users can diagnose the problem.

Why

packages/cli/src/docker/Dockerfile.render (the image hyperframes render --docker builds) used the floating node:22-bookworm-slim tag, so a cached or stale base could ship a vulnerable Node. The repo had other floating or vulnerable pins too. The published engines.node: ">=22" also allowed every vulnerable 22.x release, plus Node 23 and 25, which never got the fix. engines is only advisory in npm, so the CLI also needs to check at startup.

How

1. Pins

  • packages/cli/src/docker/Dockerfile.render
    • The base image is now node:22.23.4-bookworm-slim.
    • libbrotli1 is installed explicitly.
    • The build now fails if libbrotli1 is older than 1.0.9-2+deb12u1 (the bookworm-security fix) or chromium is older than 148.
    • chrome-headless-shell@stable is left as-is. It currently resolves to 156, which is patched.
  • Dockerfile.test
    • The base image is now node:22.23.4-bookworm-slim.
    • The Bun install is pinned to bun-v1.4.4.
    • The chrome-headless-shell@148.0.7778.167 pin stays unchanged. It's already patched, so the golden baselines don't need regenerating.
  • examples/k8s-jobs/Dockerfile.example
    • The base image is now node:22.23.4-bookworm-slim.
    • Bun is pinned to bun@1.4.4.
    • chrome-headless-shell moves from 131.0.6778.139 (vulnerable) to 148.0.7778.167, the version Dockerfile.test uses.
  • .github/actions/preflight/action.yml and .github/workflows/{ci,catalog-previews,docs,player-perf,preview-regression,windows-render}.yml: node-version: 22 becomes 22.23.4. Without check-latest, setup-node can pick an older 22.x that's already cached on the runner.
  • .github/workflows/publish.yml: node-version: 24 becomes 24.22.1.

2. Node floor

  • engines.node goes from >=22 to ^22.23.4 || ^24.22.1 || >=26.12.1 in packages/{cli,engine,producer,aws-lambda}/package.json. These are all the published packages that declare engines.
  • Docs stating the minimum now give the new range:
    • root README.md (requirements line and node badge)
    • packages/{cli,engine,producer}/README.md
    • docs/quickstart.mdx
    • docs/packages/cli.mdx
    • docs/guides/{claude-design-hyperframes.md,copilot-cli.mdx,open-design.mdx}
    • skills/hyperframes-cli/SKILL.md

3. Runtime enforcement

  • New packages/cli/src/utils/nodeVersion.ts. This is the single source of truth for SUPPORTED_NODE_RANGE, the range check, and the CVE message. It's a dependency-free module, so the startup check doesn't import doctor and its browser/telemetry dependencies on every call.

  • hyperframes doctor uses the same module. doctor.ts imports the shared check and re-exports SUPPORTED_NODE_RANGE. A test checks that all four package.json engines.node values equal that constant, so code, doctor and engines can't drift apart.

  • packages/cli/src/cli.ts exits early on an unsupported runtime. The check runs right after the --version fast path and before .env loading, telemetry, update checks or any command. On an unsupported runtime it prints this to stderr and exits 1:

    hyperframes: Node ^22.23.4 || ^24.22.1 || >=26.12.1 required (running v22.17.0) — older releases are vulnerable to CVE-2026-111497 (Brotli decoder RCE). Upgrade: https://nodejs.org/en/download
    Run `hyperframes doctor` for details.
    
  • Exemptions: doctor is exempt, and its Node.js row shows ✗ with the same message. --version still prints, because it runs before the check. Everything else is blocked, including bare hyperframes and --help.

  • Bun: Bun reports an emulated process.version, so under Bun the check uses Bun's own fixed release (>= 1.4.4) instead. Otherwise every bun-run invocation would be judged on a meaningless Node number.

Chromium findings

  • Debian's chromium does not use the system libbrotli. Brotli is on the "keepers" list in its debian/scripts/unbundle, so it ships Chromium's own bundled copy.
  • Chromium tags 148.0.7778.0 through 156.0.8078.47 (current stable) all bundle Brotli cfa184a. That revision contains the upstream fix 0ed4268a ("Detect and report invalid stream earlier"). Bookworm's chromium 150.0.7871.100-1~deb12u1 is therefore patched.
  • Chrome 131 bundles Brotli 9801a2c, which predates the fix, so it is vulnerable.

Not changed (by design)

  • brotli@1.3.3 (npm) is a pure-JS port and isn't affected.
  • No Express or body-parser server exists anywhere in the repo, so no Content-Encoding: br 415 middleware was added.
  • The repo has no Python Brotli or brotlicffi dependency.
  • @hyperframes/{core,player,shader-transitions,studio} declare no engines field. Player and shader-transitions are browser packages, so no field was added to any of the four.
  • The startup check covers only the CLI. Library consumers of @hyperframes/engine, @hyperframes/producer and @hyperframes/aws-lambda rely on engines.

Leftovers / follow-ups

  1. The CLI's fallback Chrome download (131.0.6778.85) is fixed separately in fix(cli): require patched Chrome 148+ and rebuild Docker image on Dockerfile change #6.
  2. Users' existing render images are rebuilt automatically after fix(cli): require patched Chrome 148+ and rebuild Docker image on Dockerfile change #6, which adds a Dockerfile hash to the image tag.
  3. CI Bun is unpinned. The oven-sh/setup-bun steps have no bun-version and resolve to the latest release, currently 1.4.4, which is patched. Pin it if reproducibility matters.
  4. Lambda depends on AWS patching. It runs the managed nodejs22.x runtime, so it's patched only once AWS rolls out 22.23.4+. Its @sparticuz/chromium@148.0.0 is on a patched Chromium line, though I didn't verify the bundled Brotli inside that specific artifact.
  5. Future security bumps are manual. Exact pins and the range constant mean later Node security releases need a manual bump. Renovate or Dependabot could automate the pins.
  6. No escape hatch. The startup check has no override environment variable, by design. Contributors on older local Node must upgrade to run the CLI from source.
  7. Naming mismatch. GitHub's advisory calls this "Node2Shell", not "HTTP2Shell". The IDs match.

Test plan

  • Confirmed node:22.23.4-bookworm-slim exists on Docker Hub (amd64 and arm64), Bun 1.4.4 is on npm and GitHub, and 22.23.4 and 24.22.1 are in the actions/node-versions manifest.
  • Confirmed libbrotli1_1.0.9-2+deb12u1 is published in bookworm-security and chromium 150.0.7871.100-1~deb12u1 is in bookworm.
  • Confirmed the chrome-headless-shell 148.0.7778.167 linux64 zip returns HTTP 200.
  • Tested the dpkg --compare-versions guards: they pass for patched versions and fail for older ones.
  • All edited workflow and action YAML parses.
  • vitest run src/utils/nodeVersion.test.ts src/commands/doctor.test.ts: 44/44 pass.
    • Range boundaries: 22.23.3, 24.22.0 and 26.12.0 fail, and 22.23.4, 24.22.1, 26.12.1 and 27.x pass.
    • Odd majors 23 and 25 fail.
    • The Bun 1.4.3/1.4.4 boundary is covered.
    • The gate blocks render, a bare invocation and --help, and lets doctor through.
    • A wiring test imports cli.ts with a faked process.version = v22.17.0 and asserts process.exit(1) and the CVE message.
    • engines.node matches in all four packages.
  • End to end with a real Node 22.17.0 binary (node --import tsx src/cli.ts …):
    • render and --help exit 1 with the message.
    • --version exits 0.
    • doctor runs and shows ✗ Node.js v22.17.0 with the CVE hint.
    • On Node 22.23.4, doctor shows ✓ Node.js v22.23.4.
  • oxlint and oxfmt --check are clean on the changed files, and a strict tsc typecheck of the new and changed modules passes.
  • Not run: docker build of any image, and bun run build / bun run test. Installing Docker and Bun was blocked in the agent environment, so tests ran through an isolated vitest install. The PR's CI regression job builds Dockerfile.test, but Dockerfile.render still needs a manual docker build.
Open in Web Open in Cursor 

Co-authored-by: Kareem <teamkareem@users.noreply.github.com>
…111497)

Co-authored-by: Kareem <teamkareem@users.noreply.github.com>
@cursor cursor Bot changed the title fix(docker,ci): pin patched Node/Bun/Chrome for Brotli CVE-2026-111497 fix: pin patched Node/Bun/Chrome and raise Node floor for Brotli CVE-2026-111497 Oct 26, 2026
Co-authored-by: Kareem <teamkareem@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

Fallow audit report

Found 2 findings.

Dead code (1)
Severity Rule Location Description
major fallow/unused-export packages/cli/src/utils/nodeVersion.ts:4 Export 'MIN_BUN_VERSION' is never imported by other modules
Health (1)
Severity Rule Location Description
minor fallow/high-crap-score packages/cli/src/commands/doctor.ts:247 'run' has CRAP score 37.1 (threshold: 30.0, cyclomatic 11)

Generated by fallow.

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.

2 participants