Skip to content

fix: add Dockerfile.multistage as a CVE-hardened build option#239

Open
treymujak wants to merge 1 commit into
stephengpope:buildfrom
treymujak:fix/dockerfile-cve-hardening
Open

fix: add Dockerfile.multistage as a CVE-hardened build option#239
treymujak wants to merge 1 commit into
stephengpope:buildfrom
treymujak:fix/dockerfile-cve-hardening

Conversation

@treymujak

Copy link
Copy Markdown

Summary

Adds Dockerfile.multistage as an opt-in, CVE-hardened alternative to the
default Dockerfile. The existing Dockerfile is untouched. Users who want
the hardened image build with:

docker build -f Dockerfile.multistage -t nca-toolkit:secured .

Vulnerability reduction

Scanner counts against the same image config, before and after:

Severity Default Dockerfile Multistage Reduction
Critical 5 0 100%
High 15 3 80%
Medium 15 7 53%
Low 72 24 67%
Total 107 34 68%

Image size: 4.82 GB → 3.08 GB uncompressed.

Critical CVEs closed

All five are upstream-unfixed (no patched package available). The multistage
build closes them by removing the affected package from the runtime image,
not by upgrading.

CVE Package CVSS How it's closed
CVE-2026-7210 python3.13 9.8 Pulled in by ninja-build as a python3 transitive. Multi-stage keeps ninja in the builder; runtime image has only python 3.10 (the base image).
CVE-2026-42010 gnutls28 9.8 ffmpeg rebuilt without --enable-gnutls. SRT switched to the openssl backend. gnutls28 not installed in runtime.
CVE-2026-33845 gnutls28 9.1 Same as above.
CVE-2026-40393 mesa 9.8 Pulled in by Playwright/Chromium. Playwright not installed in this image (see tradeoff below).
CVE-2026-5121 libarchive 9.8 Pulled in by build tooling. Multi-stage keeps that tooling in the builder; not present in runtime.

Why these were realistically exploitable

These aren't theoretical. Each one has a plausible reach path in this app:

  • gnutls (CVE-2026-42010, CVE-2026-33845). Both are CVSS 9.8 TLS-protocol
    bugs. ffmpeg was linked against gnutls and any endpoint that feeds a URL
    through ffmpeg (e.g. transcribe/caption with a remote source) could trigger
    a TLS handshake against an attacker-controlled host. RCE-class.
  • python3.13 (CVE-2026-7210). CVSS 9.8 on the interpreter itself. The app
    runs on python 3.10, so 3.13 wasn't invoked deliberately — but any command
    injection or shell-out chain that resolves python3 could land on it. Dead
    weight that widened the blast radius.
  • mesa (CVE-2026-40393). Pulled in by Chromium. Triggerable through
    Playwright rendering a malicious page — exactly what
    /v1/image/screenshot_webpage does. URL → server-side browser → RCE.
  • libarchive (CVE-2026-5121). Parsing bug → RCE on malicious archives.
    No direct archive-extract endpoint, but the package's presence in the
    runtime image meant any future code path (or transitive tool invocation)
    that handed it user input would inherit the vuln.

All five share a common shape: the package didn't need to be in the runtime
image. Multi-stage builds + dropping Playwright remove them rather than
waiting for upstream fixes.

What else changes

  • ffmpeg built without --enable-libtheora (CVE-2026-5673). Re-add the flag
    if Theora output is needed.
  • wheel, setuptools, jaraco.context pinned at install time to clear
    CVE-2026-24049, CVE-2026-23949 and related.
  • apt-get -y upgrade runs in the runtime stage to pick up the latest
    Debian trixie patches at build time.

Tradeoff

POST /v1/image/screenshot_webpage will fail on this image — Playwright +
Chromium aren't installed. All other endpoints behave identically. This is
the price for closing CVE-2026-40393 and the nss/cups Critical+High cluster
that Chromium drags in.

docs/secure-docker.md documents this and points users at the default
Dockerfile if they need the screenshot endpoint.

Why additive, not a replacement

The default Dockerfile keeps Playwright and the screenshot endpoint working
out of the box, which matters for existing users. This PR doesn't take that
away — it just gives operators who don't need screenshots a hardened option.

Test plan

  • docker build -f Dockerfile.multistage -t nca-toolkit:secured .
  • docker run and hit POST /v1/toolkit/test — auth + queue path works
  • Run an ffmpeg-backed endpoint (e.g. /v1/media/transcode) to confirm
    compiled ffmpeg works without gnutls/libtheora
  • Confirm POST /v1/image/screenshot_webpage fails on this image
    (documented tradeoff)
  • Re-run scanner against built image, confirm counts match the table
    above

Adds an alternative multi-stage Dockerfile that keeps the default Dockerfile
untouched. Users who want a smaller, more locked-down image can opt in via:

    docker build -f Dockerfile.multistage -t nca-toolkit:secured .

Compared to the default Dockerfile:
- Multi-stage split: compilers and -dev headers stay in the builder stage and
  never reach the final image.
- Playwright + Chromium not installed (disables /v1/image/screenshot_webpage
  on this image; all other endpoints unchanged). Closes the nss/cups CVE
  cluster pulled in by Chromium.
- ffmpeg built without --enable-gnutls; SRT uses the openssl backend. Closes
  the gnutls CVE cluster.
- libtheora dropped from the ffmpeg build.
- wheel, setuptools, jaraco.context pinned to patched versions at pip layer.
- apt-get -y upgrade in the runtime stage to pick up the latest trixie patches.

docs/secure-docker.md documents the tradeoff and when to prefer the default
Dockerfile instead.
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