From 74a8f9bf2471327baf9abe364b705a2e200e7f3d Mon Sep 17 00:00:00 2001 From: Paul Nothaft <53005142+the-luap@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:56:01 +0200 Subject: [PATCH] chore(security): shrink the ML image's CVE surface, override deepmerge-ts (#1083) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three Trivy cleanups off the code-scanning tab. ml/Dockerfile — install no runtime apt packages at all. Neither libgl1 nor libglib2.0-0 is needed: opencv-python-headless 4.14 bundles what it needs and `ldd .../cv2/cv2*.so` resolves fully on a bare slim base. The old comment claimed the headless wheel still links libGL, which was true of much older wheels. libgl1 was dragging in 36 transitive packages (mesa, LLVM, X11) for a service that never opens a display. Measured with `trivy image` on locally built variants: before: 165 findings — 88 low / 49 med / 19 high / 6 crit without libgl1: 133 findings — 58 low / 48 med / 19 high / 5 crit without either: 123 findings — 57 low / 46 med / 13 high / 4 crit 42 findings gone, image 1.05GB -> 774MB. Not one of the 165 had an upstream fix available, so not installing the packages is the only lever there is. docker-build.yml — set ignore-unfixed on all four Trivy steps. All 123 remaining ML findings are unfixed base-OS CVEs; Debian has them resolved in sid and pending backport to trixie, and apt-get upgrade -y behind CACHEBUST picks each one up automatically. Reporting them buries anything actionable, and suppressing them is the precondition for ever setting exit-code: 1. backend — deepmerge-ts <8.0.0 has a stack-exhaustion advisory (CVE-2026-40345, high) reached via mailparser -> html-to-text, which pins ^7.1.5 so npm cannot get there alone. Not reachable in our code: html-to-text only feeds deepmerge-ts its options object (html-to-text.mjs:1468, :1442), never parsed email content. npm audit goes 3 high -> 0. The lockfile also picks up the version field release-please had left at 3.103.1-beta.0, plus some "peer": true metadata npm 11.6 recomputes. Co-authored-by: Paul Nothaft --- .github/workflows/docker-build.yml | 32 ++++++++++++++++++++++++++++++ backend/package-lock.json | 29 ++++++++++++++++++++++----- backend/package.json | 3 ++- ml/Dockerfile | 21 +++++++++++++++++--- 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index b250941f..04bc614e 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -212,6 +212,14 @@ jobs: format: 'sarif' output: 'trivy-backend-${{ env.PLATFORM_PAIR }}.sarif' severity: 'CRITICAL,HIGH' + # Base-image CVEs with no released fix are not actionable: the + # Dockerfiles already run `apt-get upgrade -y` behind a CACHEBUST, + # so a fix lands in the next build automatically. Reporting them + # buries the findings someone can actually do something about -- + # the ML image alone contributed 123 unfixable alerts. Dropping + # them is also the precondition for ever setting exit-code: 1, + # which build-backend's comment flags as a deliberate follow-up. + ignore-unfixed: true timeout: '10m' - name: Upload Trivy scan results to GitHub Security tab @@ -474,6 +482,14 @@ jobs: format: 'sarif' output: 'trivy-frontend-${{ env.PLATFORM_PAIR }}.sarif' severity: 'CRITICAL,HIGH' + # Base-image CVEs with no released fix are not actionable: the + # Dockerfiles already run `apt-get upgrade -y` behind a CACHEBUST, + # so a fix lands in the next build automatically. Reporting them + # buries the findings someone can actually do something about -- + # the ML image alone contributed 123 unfixable alerts. Dropping + # them is also the precondition for ever setting exit-code: 1, + # which build-backend's comment flags as a deliberate follow-up. + ignore-unfixed: true timeout: '10m' - name: Upload Trivy scan results to GitHub Security tab @@ -716,6 +732,14 @@ jobs: format: 'sarif' output: 'trivy-aio-${{ env.PLATFORM_PAIR }}.sarif' severity: 'CRITICAL,HIGH' + # Base-image CVEs with no released fix are not actionable: the + # Dockerfiles already run `apt-get upgrade -y` behind a CACHEBUST, + # so a fix lands in the next build automatically. Reporting them + # buries the findings someone can actually do something about -- + # the ML image alone contributed 123 unfixable alerts. Dropping + # them is also the precondition for ever setting exit-code: 1, + # which build-backend's comment flags as a deliberate follow-up. + ignore-unfixed: true timeout: '10m' - name: Upload Trivy scan results to GitHub Security tab @@ -1109,6 +1133,14 @@ jobs: format: 'sarif' output: 'trivy-ml-${{ env.PLATFORM_PAIR }}.sarif' severity: 'CRITICAL,HIGH' + # Base-image CVEs with no released fix are not actionable: the + # Dockerfiles already run `apt-get upgrade -y` behind a CACHEBUST, + # so a fix lands in the next build automatically. Reporting them + # buries the findings someone can actually do something about -- + # the ML image alone contributed 123 unfixable alerts. Dropping + # them is also the precondition for ever setting exit-code: 1, + # which build-backend's comment flags as a deliberate follow-up. + ignore-unfixed: true timeout: '10m' - name: Upload Trivy scan results to GitHub Security tab diff --git a/backend/package-lock.json b/backend/package-lock.json index 91e18660..99c734de 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "picpeak-backend", - "version": "3.103.1-beta.0", + "version": "3.107.1-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "picpeak-backend", - "version": "3.103.1-beta.0", + "version": "3.107.1-beta.0", "dependencies": { "@aws-sdk/client-s3": "^3.850.0", "@aws-sdk/lib-storage": "^3.850.0", @@ -324,6 +324,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1000.0.tgz", "integrity": "sha512-7kPy33qNGq3NfwHC0412T6LDK1bp4+eiPzetX0sVd9cpTSXuQDKpoOFnB0Njj6uZjJDcLS3n2OeyarwwgkQ0Ow==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", @@ -1052,6 +1053,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -3946,6 +3948,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4560,6 +4563,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", @@ -5367,9 +5371,19 @@ } }, "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.1.tgz", + "integrity": "sha512-szCXE7YLCvLKR9bFPJcvsezOShdalctSvrgN/LM/QGUEPZQajwjmsMObZ6/DuANT5lxzM/wtO8Feubwdkz8myA==", + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/deepmerge-ts" + } + ], "license": "BSD-3-Clause", "engines": { "node": ">=16.0.0" @@ -5753,6 +5767,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6000,6 +6015,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -6967,6 +6983,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -9884,6 +9901,7 @@ "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz", "integrity": "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==", "license": "MIT", + "peer": true, "dependencies": { "crypto-js": "^4.2.0", "fontkit": "^2.0.4", @@ -10978,6 +10996,7 @@ "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.12.0.tgz", "integrity": "sha512-b1YMh3+DHZp59DLna3qVwQ5iOla/nrI6mLBNW02XxU77M3046Df6VLkoaJyFz20VsGIG5kkp+FK0kg4K4HnUFw==", "license": "MIT", + "peer": true, "dependencies": { "parseley": "~0.13.1" }, diff --git a/backend/package.json b/backend/package.json index 0b8f469e..aef5cfe1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -96,6 +96,7 @@ "@tootallnate/once": ">=3.0.1", "ip-address": ">=10.3.1", "uuid": "^11.1.1", - "nodemailer": "^9.0.1" + "nodemailer": "^9.0.1", + "deepmerge-ts": ">=8.0.1" } } diff --git a/ml/Dockerfile b/ml/Dockerfile index b076e96f..86f70a8b 100644 --- a/ml/Dockerfile +++ b/ml/Dockerfile @@ -73,11 +73,26 @@ ARG CACHEBUST=1 RUN echo "cachebust=${CACHEBUST}" \ && apt-get update \ && apt-get upgrade -y \ - # libGL and libglib are opencv-python-headless's remaining shared-library - # deps. The headless wheel drops the GUI toolkits but still links libGL. - && apt-get install -y --no-install-recommends libgl1 libglib2.0-0 \ && rm -rf /var/lib/apt/lists/* +# No runtime apt packages at all — deliberately. +# +# opencv-python-headless 4.14 needs neither libgl1 nor libglib2.0-0. The +# wheel bundles what it needs; `ldd .../cv2/cv2*.so` resolves fully on a +# bare python:3.12-slim. Both were installed here on the assumption that +# the headless build still links libGL, which was true of much older +# wheels and is not true of this one. +# +# What they cost: libgl1 alone pulls 36 transitive packages (mesa, LLVM, +# X11) into a service that never opens a display, and libglib2.0-0t64 +# carries a critical plus six highs. Together they accounted for 42 of +# this image's Trivy findings — none of which have an upstream fix, so +# not installing them is the only lever that exists. +# +# Before re-adding either, confirm it is actually needed: build without +# it and run `ml/tests` plus a real FaceDetectorYN.detect() call, since +# the API tests stub the pipeline and will pass either way. + WORKDIR /app COPY requirements.txt .