diff --git a/CHANGELOG.md b/CHANGELOG.md index 69dfb6b15..f94ce3040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ ## [Unreleased] +### Security + +- The PDF renderer behind the document vault moves to a release that closes a + script-execution flaw. A crafted PDF could get JavaScript to run inside the + renderer, and the vault feeds it every PDF that is uploaded. Two things + limited what that reached, and neither is a substitute for the fix: the + renderer runs server-side only, because a PDF is displayed through the + browser's own viewer rather than this library, and it already ran with font + evaluation turned off. A second copy of the same library, pulled in by a + development dependency, is pinned past the flaw as well, since the runtime + image ships every copy it finds rather than only the one that gets used. + +- The container image now runs the version of the PDF renderer the project + pins. The build had been taking whichever copy the filesystem listed first, + and more than one copy reaches the image, so the pinned version was not + reliably the one that ran. It is selected by name now, and a copy that never + arrived fails the build instead of quietly leaving another one in its place. + ## [1.37.0] — 2026-08-07 ### Added diff --git a/Dockerfile b/Dockerfile index 5eeb810ad..24982844a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,13 @@ ENV NEXT_TELEMETRY_DISABLED=1 # starts fresh and therefore does not inherit this setting. RUN NODE_OPTIONS=--max-old-space-size=8192 pnpm build +# Record which pdfjs-dist the top-level dependency actually resolves to. pnpm +# links a direct dependency at `node_modules/`, so this reads the exact +# pinned copy rather than guessing. The runner stage hoists that version by +# name; see the note there for why guessing was wrong. +RUN node -p "require('/app/node_modules/pdfjs-dist/package.json').version" \ + > /app/.pdfjs-version + # ── Stage 3: Production runner ───────────────────────────── FROM node:22-alpine@sha256:16e22a550f3863206a3f701448c45f7912c6896a62de43add43bb9c86130c3e2 AS runner # `tzdata` is required so Europe/Berlin schedules (pg-boss cron, locale-aware @@ -116,13 +123,30 @@ RUN set -e; \ # path breaks in the standalone image), so the server does a runtime bare # `import('pdfjs-dist/legacy/build/pdf.mjs')`. Node resolves that up to # /app/node_modules, so hoist the real pnpm-store copy to the top level too. +# +# Hoist the version the app pins, by name. More than one pdfjs-dist reaches the +# store — `pdf-parse` carries its own, older copy for the text-extraction path — +# and this step used to take whatever `find | head -1` returned first. That is +# readdir order, so it could hoist the OLDER copy and the version named in +# package.json would never run in the image: the renderer resolving one version +# while every local check exercised another. Name the version and fail the build +# when it is absent, instead of degrading to whichever copy comes back first. +COPY --from=builder /app/.pdfjs-version /tmp/.pdfjs-version RUN set -e; \ - PDFJS_DIR="$(find /app/node_modules/.pnpm -maxdepth 4 -type d -path '*pdfjs-dist@*/node_modules/pdfjs-dist' 2>/dev/null | head -1)"; \ - if [ -n "$PDFJS_DIR" ]; then \ - ln -sfn "$PDFJS_DIR" /app/node_modules/pdfjs-dist; \ - chown -h nextjs:nodejs /app/node_modules/pdfjs-dist; \ - echo "pdfjs-dist hoisted: $PDFJS_DIR"; \ - else echo "WARN: pdfjs-dist not found; PDF rasterization will degrade to local text"; fi + PDFJS_VERSION="$(cat /tmp/.pdfjs-version)"; \ + PDFJS_DIR="/app/node_modules/.pnpm/pdfjs-dist@${PDFJS_VERSION}/node_modules/pdfjs-dist"; \ + if [ ! -d "$PDFJS_DIR" ]; then \ + echo "ERROR: pinned pdfjs-dist@${PDFJS_VERSION} did not reach the traced image."; \ + echo "Present instead:"; \ + find /app/node_modules/.pnpm -maxdepth 4 -type d -path '*pdfjs-dist@*/node_modules/pdfjs-dist' 2>/dev/null; \ + exit 1; \ + fi; \ + ln -sfn "$PDFJS_DIR" /app/node_modules/pdfjs-dist; \ + chown -h nextjs:nodejs /app/node_modules/pdfjs-dist; \ + rm -f /tmp/.pdfjs-version; \ + RESOLVED="$(node -p "require('/app/node_modules/pdfjs-dist/package.json').version")"; \ + [ "$RESOLVED" = "$PDFJS_VERSION" ] || { echo "ERROR: hoisted pdfjs-dist is $RESOLVED, expected $PDFJS_VERSION"; exit 1; }; \ + echo "pdfjs-dist hoisted: $PDFJS_DIR (resolves as $RESOLVED)" # Copy Prisma for migrations (schema, migration SQL, config, engines) COPY --from=builder /app/prisma ./prisma diff --git a/package.json b/package.json index 4aa16d7cd..97506c04f 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "knip": "^6.16.1", "lefthook": "^2.1.10", "pdf-parse": "^2.4.5", - "pdfjs-dist": "6.1.200", + "pdfjs-dist": "6.2.108", "pg": "^8.22.0", "prettier": "^3.9.1", "prettier-plugin-tailwindcss": "^0.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbe7c3e86..92dc14bec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,7 @@ overrides: fast-uri@<3.1.5: ^3.1.5 ip-address@<10.3.1: ^10.3.1 sharp@<0.35.0: ^0.35.3 + pdfjs-dist@>=5.6.83 <6.2.108: 6.2.108 importers: @@ -234,8 +235,8 @@ importers: specifier: ^2.4.5 version: 2.4.5 pdfjs-dist: - specifier: 6.1.200 - version: 6.1.200 + specifier: 6.2.108 + version: 6.2.108 pg: specifier: ^8.22.0 version: 8.22.0 @@ -1223,12 +1224,6 @@ packages: resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} engines: {node: '>=18'} - '@napi-rs/canvas-android-arm64@0.1.100': - resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - '@napi-rs/canvas-android-arm64@0.1.80': resolution: {integrity: sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==} engines: {node: '>= 10'} @@ -1241,12 +1236,6 @@ packages: cpu: [arm64] os: [android] - '@napi-rs/canvas-darwin-arm64@0.1.100': - resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - '@napi-rs/canvas-darwin-arm64@0.1.80': resolution: {integrity: sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==} engines: {node: '>= 10'} @@ -1259,12 +1248,6 @@ packages: cpu: [arm64] os: [darwin] - '@napi-rs/canvas-darwin-x64@0.1.100': - resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - '@napi-rs/canvas-darwin-x64@0.1.80': resolution: {integrity: sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==} engines: {node: '>= 10'} @@ -1277,12 +1260,6 @@ packages: cpu: [x64] os: [darwin] - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100': - resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.80': resolution: {integrity: sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==} engines: {node: '>= 10'} @@ -1295,13 +1272,6 @@ packages: cpu: [arm] os: [linux] - '@napi-rs/canvas-linux-arm64-gnu@0.1.100': - resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@napi-rs/canvas-linux-arm64-gnu@0.1.80': resolution: {integrity: sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==} engines: {node: '>= 10'} @@ -1316,13 +1286,6 @@ packages: os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-arm64-musl@0.1.100': - resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - '@napi-rs/canvas-linux-arm64-musl@0.1.80': resolution: {integrity: sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==} engines: {node: '>= 10'} @@ -1337,13 +1300,6 @@ packages: os: [linux] libc: [musl] - '@napi-rs/canvas-linux-riscv64-gnu@0.1.100': - resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==} - engines: {node: '>= 10'} - cpu: [riscv64] - os: [linux] - libc: [glibc] - '@napi-rs/canvas-linux-riscv64-gnu@0.1.80': resolution: {integrity: sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==} engines: {node: '>= 10'} @@ -1358,13 +1314,6 @@ packages: os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-x64-gnu@0.1.100': - resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - '@napi-rs/canvas-linux-x64-gnu@0.1.80': resolution: {integrity: sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==} engines: {node: '>= 10'} @@ -1379,13 +1328,6 @@ packages: os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-x64-musl@0.1.100': - resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - '@napi-rs/canvas-linux-x64-musl@0.1.80': resolution: {integrity: sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==} engines: {node: '>= 10'} @@ -1400,24 +1342,12 @@ packages: os: [linux] libc: [musl] - '@napi-rs/canvas-win32-arm64-msvc@0.1.100': - resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - '@napi-rs/canvas-win32-arm64-msvc@1.0.2': resolution: {integrity: sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@napi-rs/canvas-win32-x64-msvc@0.1.100': - resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - '@napi-rs/canvas-win32-x64-msvc@0.1.80': resolution: {integrity: sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==} engines: {node: '>= 10'} @@ -1430,10 +1360,6 @@ packages: cpu: [x64] os: [win32] - '@napi-rs/canvas@0.1.100': - resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==} - engines: {node: '>= 10'} - '@napi-rs/canvas@0.1.80': resolution: {integrity: sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==} engines: {node: '>= 10'} @@ -5696,9 +5622,6 @@ packages: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} - node-readable-to-web-readable-stream@0.4.2: - resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==} - node-releases@2.0.51: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} @@ -5918,12 +5841,8 @@ packages: resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==} engines: {node: '>=20.16.0 || >=22.3.0'} - pdfjs-dist@5.6.205: - resolution: {integrity: sha512-tlUj+2IDa7G1SbvBNN74UHRLJybZDWYom+k6p5KIZl7huBvsA4APi6mKL+zCxd3tLjN5hOOEE9Tv7VdzO88pfg==} - engines: {node: '>=20.19.0 || >=22.13.0 || >=24'} - - pdfjs-dist@6.1.200: - resolution: {integrity: sha512-o8MolyzirkkLrcdsae/HEOiIcXWI7DS5zGpvqW8xTC2YUsW30rltFw2bDGvw/fskUdEMrQm2br68jzDS5BH2vw==} + pdfjs-dist@6.2.108: + resolution: {integrity: sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==} engines: {node: '>=22.13.0 || >=24'} pend@1.2.0: @@ -8354,117 +8273,69 @@ snapshots: strict-event-emitter: 0.5.1 optional: true - '@napi-rs/canvas-android-arm64@0.1.100': - optional: true - '@napi-rs/canvas-android-arm64@0.1.80': optional: true '@napi-rs/canvas-android-arm64@1.0.2': optional: true - '@napi-rs/canvas-darwin-arm64@0.1.100': - optional: true - '@napi-rs/canvas-darwin-arm64@0.1.80': optional: true '@napi-rs/canvas-darwin-arm64@1.0.2': optional: true - '@napi-rs/canvas-darwin-x64@0.1.100': - optional: true - '@napi-rs/canvas-darwin-x64@0.1.80': optional: true '@napi-rs/canvas-darwin-x64@1.0.2': optional: true - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100': - optional: true - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.80': optional: true '@napi-rs/canvas-linux-arm-gnueabihf@1.0.2': optional: true - '@napi-rs/canvas-linux-arm64-gnu@0.1.100': - optional: true - '@napi-rs/canvas-linux-arm64-gnu@0.1.80': optional: true '@napi-rs/canvas-linux-arm64-gnu@1.0.2': optional: true - '@napi-rs/canvas-linux-arm64-musl@0.1.100': - optional: true - '@napi-rs/canvas-linux-arm64-musl@0.1.80': optional: true '@napi-rs/canvas-linux-arm64-musl@1.0.2': optional: true - '@napi-rs/canvas-linux-riscv64-gnu@0.1.100': - optional: true - '@napi-rs/canvas-linux-riscv64-gnu@0.1.80': optional: true '@napi-rs/canvas-linux-riscv64-gnu@1.0.2': optional: true - '@napi-rs/canvas-linux-x64-gnu@0.1.100': - optional: true - '@napi-rs/canvas-linux-x64-gnu@0.1.80': optional: true '@napi-rs/canvas-linux-x64-gnu@1.0.2': optional: true - '@napi-rs/canvas-linux-x64-musl@0.1.100': - optional: true - '@napi-rs/canvas-linux-x64-musl@0.1.80': optional: true '@napi-rs/canvas-linux-x64-musl@1.0.2': optional: true - '@napi-rs/canvas-win32-arm64-msvc@0.1.100': - optional: true - '@napi-rs/canvas-win32-arm64-msvc@1.0.2': optional: true - '@napi-rs/canvas-win32-x64-msvc@0.1.100': - optional: true - '@napi-rs/canvas-win32-x64-msvc@0.1.80': optional: true '@napi-rs/canvas-win32-x64-msvc@1.0.2': optional: true - '@napi-rs/canvas@0.1.100': - optionalDependencies: - '@napi-rs/canvas-android-arm64': 0.1.100 - '@napi-rs/canvas-darwin-arm64': 0.1.100 - '@napi-rs/canvas-darwin-x64': 0.1.100 - '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100 - '@napi-rs/canvas-linux-arm64-gnu': 0.1.100 - '@napi-rs/canvas-linux-arm64-musl': 0.1.100 - '@napi-rs/canvas-linux-riscv64-gnu': 0.1.100 - '@napi-rs/canvas-linux-x64-gnu': 0.1.100 - '@napi-rs/canvas-linux-x64-musl': 0.1.100 - '@napi-rs/canvas-win32-arm64-msvc': 0.1.100 - '@napi-rs/canvas-win32-x64-msvc': 0.1.100 - optional: true - '@napi-rs/canvas@0.1.80': optionalDependencies: '@napi-rs/canvas-android-arm64': 0.1.80 @@ -8707,7 +8578,7 @@ snapshots: fflate: 0.8.2 incur: 0.4.13 papaparse: 5.5.3 - pdfjs-dist: 5.6.205 + pdfjs-dist: 6.2.108 smol-toml: 1.6.1 transitivePeerDependencies: - '@types/node' @@ -12819,9 +12690,6 @@ snapshots: node-forge@1.4.0: {} - node-readable-to-web-readable-stream@0.4.2: - optional: true - node-releases@2.0.51: {} nodemailer@9.0.3: {} @@ -13084,12 +12952,7 @@ snapshots: optionalDependencies: '@napi-rs/canvas': 0.1.80 - pdfjs-dist@5.6.205: - optionalDependencies: - '@napi-rs/canvas': 0.1.100 - node-readable-to-web-readable-stream: 0.4.2 - - pdfjs-dist@6.1.200: + pdfjs-dist@6.2.108: optionalDependencies: '@napi-rs/canvas': 1.0.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 15a8dafd2..c7fabca7c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,6 +12,14 @@ overrides: # rate limiter where nothing of ours screens it. "ip-address@<10.3.1": "^10.3.1" "sharp@<0.35.0": "^0.35.3" + # A crafted PDF could run arbitrary JavaScript through the renderer. Our own + # copy is pinned past it in `devDependencies`, but a second copy arrives + # transitively, and `outputFileTracingIncludes` ships EVERY `pdfjs-dist@*` in + # the store into the standalone image — so an unused transitive copy is still + # a copy in the runtime image. The range is bounded on both ends deliberately: + # `pdf-parse` resolves 5.4.296, which predates the flaw and drives the local + # text-extraction path, so it must not be dragged onto a new major. + "pdfjs-dist@>=5.6.83 <6.2.108": "6.2.108" allowBuilds: "@prisma/engines": true