diff --git a/.osv-scanner.toml b/.osv-scanner.toml index cac5e40a6..7062905f8 100644 --- a/.osv-scanner.toml +++ b/.osv-scanner.toml @@ -16,73 +16,16 @@ # forces a fresh decision. Tracking issues are filed at ship time and named in # each reason. -[[IgnoredVulns]] -id = "GHSA-frvp-7c67-39w9" -# @hono/node-server 1.19.x. Reachable only through @modelcontextprotocol/sdk, -# which is an unused transitive dependency (no source file imports it) and never -# starts a Hono HTTP server, so the advisory's request path is not exercised. -# The only fix is @hono/node-server 2.0.5, a major bump the MCP SDK pins against -# (^1.19.9); forcing it via override risks breaking the SDK at runtime for a -# vulnerability we do not expose. Re-evaluate if the MCP SDK becomes a direct, -# server-hosting dependency. -reason = "Unreachable transitive (unused @modelcontextprotocol/sdk); fix requires a risky major override on a pinned peer dep." -ignoreUntil = 2026-11-30T00:00:00Z - -[[IgnoredVulns]] -id = "GHSA-5p2g-fcmc-qvqq" -# image-size 1.2.1 via html-to-docx@1.8.0. image-size 2.0.3 (2026-09-14) -# fixes both advisories, but html-to-docx pins image-size ^1.0.0 and no fixed -# 1.x exists, so taking the fix is a cross-major override. Exposure: -# image-size only parses images the user themselves embeds into their own -# generated .docx; no untrusted input path. Upgrade trigger: html-to-docx -# moving off image-size 1.x, or a deliberate override to 2.x after checking -# html-to-docx's call site against 2.x's API with a docx-export smoke test. -# Tracking: #2753. -reason = "Fixed in image-size 2.0.3, but html-to-docx pins image-size ^1.0.0 (no fixed 1.x); local-only input path (user's own docx images). Re-evaluate on expiry." -ignoreUntil = 2026-11-30T00:00:00Z - -[[IgnoredVulns]] -id = "GHSA-w3rx-r6r6-pgpr" -# Same package/node as GHSA-5p2g-fcmc-qvqq above; same rationale. -reason = "Fixed in image-size 2.0.3, but html-to-docx pins image-size ^1.0.0 (no fixed 1.x); local-only input path (user's own docx images). Re-evaluate on expiry." -ignoreUntil = 2026-11-30T00:00:00Z - -[[IgnoredVulns]] -id = "GHSA-p7fg-763f-g4gf" -# @anthropic-ai/sdk 0.81.0 nested under @anthropic-ai/claude-agent-sdk@0.2.117, -# which is deliberately exact-pinned (eval-harness stability; the v1.77 wave -# pinned the whole harness after repeated CLI-drift breakage) and declares -# ^0.81.0 (0.x caret = 0.81.x only), so the 0.91.1 fix cannot be reached -# without violating the harness pin. 4.8 MEDIUM. Upgrade trigger: the next -# deliberate claude-agent-sdk bump. Tracking: #2754. -reason = "Fix requires breaking the deliberate eval-harness agent-sdk pin; MEDIUM severity accepted until the next harness bump." -ignoreUntil = 2026-11-30T00:00:00Z - -[[IgnoredVulns]] -id = "GHSA-vfj7-8cjw-p6xm" -# braces 3.0.3 in lib/diagram-render only: @excalidraw/excalidraw -> sass -> -# chokidar -> braces. chokidar is sass's Node file watcher; the diagram-render -# bundle is built for the browser and contains no chokidar/braces code -# (test/osv-config-wiring.test.ts checks the built page for chokidar). No fixed -# braces release exists. Re-evaluate on expiry or when excalidraw drops sass. -reason = "Build-time only (excalidraw -> sass -> chokidar); not in the shipped browser bundle; no fixed release." -ignoreUntil = 2026-11-30T00:00:00Z - -[[IgnoredVulns]] -id = "GHSA-hp3w-g68c-fv3c" -# sprintf-js 1.1.3 (DoS through an unbounded precision specifier in the FORMAT -# string). Reachable only as roarr's logger dependency (global-agent -> roarr), -# which formats its own constant messages; no caller passes user-controlled -# format strings. No fixed release exists. -reason = "No fixed release; only roarr's constant-format logging reaches it." -ignoreUntil = 2026-11-30T00:00:00Z - [[IgnoredVulns]] id = "GHSA-238p-pmpm-9mq7" # katex 0.16.47 in lib/diagram-render (mermaid's math renderer). The advisory # needs prototype pollution to already exist in the page; the render page is -# offline, loads only gstack's own bundle and the user's diagram source. The -# only fix is 0.18.x, outside mermaid 11.16's ^0.16 range. Low severity (2.1). -# Re-evaluate when mermaid accepts katex 0.18. -reason = "Requires pre-existing prototype pollution in an offline single-bundle page; fix is outside mermaid's katex range; low severity." -ignoreUntil = 2026-11-30T00:00:00Z +# offline, loads only gstack's own bundle and the user's diagram source. Low +# severity (2.1). The fix is katex 0.18.2, outside mermaid 11's ^0.16.47 range. +# Mermaid 12 accepts ^0.18, but @excalidraw/mermaid-to-excalidraw 2.2.2 (the +# latest) requires mermaid ^11.12.1, and katex 0.18 renames KaTeX's CSS +# classes, so forcing it means a bundle requalification, not a lockfile bump. +# Upgrade trigger: a mermaid 11.x release that allows katex 0.18 +# (https://github.com/mermaid-js/mermaid/issues/8388), then rebuild the bundle. +reason = "Fix (katex 0.18.2) is outside mermaid 11's ^0.16 range; waiting on mermaid-js/mermaid#8388. Needs pre-existing prototype pollution in an offline single-bundle page; low severity." +ignoreUntil = 2027-01-31T00:00:00Z diff --git a/TODOS.md b/TODOS.md index e8ab35a1b..36676711c 100644 --- a/TODOS.md +++ b/TODOS.md @@ -41,8 +41,8 @@ The 158 older open issues were re-checked against main 5cb5e1c (v1.91.83.0). Of - **/cso** - #2993: tracked under the /cso capacity follow-ups above. **Priority:** P2. - **Dependency security ledgers** (not code defects; keep open until the trigger) - - #2753: `.osv-scanner.toml` ignores image-size (via html-to-docx) until 2026-11-30. Bump it once a fixed release exists, or re-justify at expiry. **Effort:** S. **Priority:** P3. - - #2754: `.osv-scanner.toml` ignores `@anthropic-ai/sdk` 0.81.0 under the pinned claude-agent-sdk until 2026-11-30. Drop it with the next harness bump. **Effort:** S. **Priority:** P3. + - `.osv-scanner.toml` ignores katex 0.16.47 in lib/diagram-render (GHSA-238p-pmpm-9mq7, low) until 2027-01-31. The fix (0.18.2) is outside mermaid 11's range; when mermaid-js/mermaid#8388 ships, override katex, rebuild the bundle and drop the ignore. **Effort:** S. **Priority:** P3. + - html-to-docx 1.8.0 bundles its own image-size 1.x, which the package.json override cannot reach. `make-pdf --to docx` only hands it PNG, JPEG, GIF, WebP or SVG bytes, but with `--allow-network` html-to-docx still downloads and sizes remote images itself. Drop html-to-docx's bundled sizer (a fork or a replacement converter) or validate remote images before conversion. **Effort:** M. **Priority:** P3. - **Tests and CI** - #2756: the periodic lane. Tracked under "Periodic-lane stabilization (#2756)". **Priority:** P2. - #2967: `test/redact-prepush-scan-range.test.ts` (and its rebase twin) run fixture pushes with the inherited global config, so a developer's `core.hooksPath` pre-push runs inside the fixture. Set `GIT_CONFIG_GLOBAL=/dev/null` and `GIT_CONFIG_NOSYSTEM=1` in the helper. **Effort:** S. **Priority:** P3. @@ -965,10 +965,9 @@ wave"). Each was explicitly deferred with rationale, not dropped: - **mermaid 10→11-class major bumps in lib/diagram-render** — the wave's dependency pass cleared 102 of 105 OSV advisories via in-range bumps + - overrides; the residual ignores (image-size no-fix, @anthropic-ai/sdk under - the harness-pinned agent-sdk) carry `ignoreUntil` expiries (~2026-11-30) and - re-justify themselves on expiry. When the agent-sdk pin next moves, drop the - GHSA-p7fg ignore. Effort S. **Priority:** P3. + overrides; the residual ignores carry `ignoreUntil` expiries and + re-justify themselves on expiry. Only the katex ignore remains (see the + dependency security ledgers above). Effort S. **Priority:** P3. - **#2750 split absorption** — the record-scanning Codex JSONL parser (real fix; current Codex streams interleave envelopes so sessions vanish from /retro global) should be absorbed once the author splits it from the diff --git a/bun.lock b/bun.lock index db2f7ae8d..49ba3a0cb 100644 --- a/bun.lock +++ b/bun.lock @@ -34,6 +34,7 @@ "adm-zip": "^0.6.1", "fast-uri": "3.1.8", "hono": "4.13.13", + "image-size": "2.0.4", "ip-address": "10.7.3", "proxy-addr": "2.0.8", "sharp": "0.35.5", @@ -371,7 +372,7 @@ "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], - "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], + "image-size": ["image-size@2.0.4", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-QRUkFFsRV/6fuESxb9Vkq+a0LkSrgKXuc2NEqfikiXxxN/G3tjWt5EVUlMaImRBZRZK/jRBEbYvpPYZL8t08Zw=="], "image-to-base64": ["image-to-base64@2.2.0", "", { "dependencies": { "node-fetch": "^2.6.0" } }, "sha512-Z+aMwm/91UOQqHhrz7Upre2ytKhWejZlWV/JxUTD1sT7GWWKFDJUEV5scVQKnkzSgPHFuQBUEWcanO+ma0PSVw=="], @@ -483,8 +484,6 @@ "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], - "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], - "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], diff --git a/lib/diagram-render/bun.lock b/lib/diagram-render/bun.lock index fa55c8ade..ccf979184 100644 --- a/lib/diagram-render/bun.lock +++ b/lib/diagram-render/bun.lock @@ -14,6 +14,7 @@ }, }, "overrides": { + "chokidar": "4.0.3", "dompurify": "3.4.16", "lodash-es": "4.18.1", "nanoid": "5.1.16", @@ -178,14 +179,8 @@ "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], - "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browser-fs-access": ["browser-fs-access@0.29.1", "", {}, "sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw=="], "canvas-roundrect-polyfill": ["canvas-roundrect-polyfill@0.0.1", "", {}, "sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw=="], @@ -194,7 +189,7 @@ "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="], - "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "clsx": ["clsx@1.1.1", "", {}, "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="], @@ -292,18 +287,12 @@ "es6-promise-pool": ["es6-promise-pool@2.5.0", "", {}, "sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "fractional-indexing": ["fractional-indexing@3.2.0", "", {}, "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "fuzzy": ["fuzzy@0.1.3", "", {}, "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "glur": ["glur@1.1.2", "", {}, "sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA=="], "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], @@ -320,14 +309,6 @@ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jotai": ["jotai@2.11.0", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ=="], @@ -360,8 +341,6 @@ "nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "open-color": ["open-color@1.9.1", "", {}, "sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw=="], @@ -378,8 +357,6 @@ "pica": ["pica@7.1.1", "", { "dependencies": { "glur": "^1.1.2", "inherits": "^2.0.3", "multimath": "^2.0.0", "object-assign": "^4.1.1", "webworkify": "^1.5.0" } }, "sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ=="], - "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "png-chunk-text": ["png-chunk-text@1.0.0", "", {}, "sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw=="], "png-chunks-encode": ["png-chunks-encode@1.0.0", "", { "dependencies": { "crc-32": "^0.3.0", "sliced": "^1.0.1" } }, "sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA=="], @@ -402,7 +379,7 @@ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], @@ -428,8 +405,6 @@ "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], diff --git a/lib/diagram-render/package.json b/lib/diagram-render/package.json index 6c4bc4ef6..8a41abd38 100644 --- a/lib/diagram-render/package.json +++ b/lib/diagram-render/package.json @@ -14,6 +14,7 @@ "react-dom": "18.3.1" }, "overrides": { + "chokidar": "4.0.3", "dompurify": "3.4.16", "nanoid": "5.1.16", "source-map-js": "1.2.2", diff --git a/make-pdf/src/image-size.ts b/make-pdf/src/image-size.ts index e4690c1b8..94d1e02a9 100644 --- a/make-pdf/src/image-size.ts +++ b/make-pdf/src/image-size.ts @@ -21,6 +21,18 @@ export function imageDims(buf: Buffer): ImageDims | null { return pngDims(buf) ?? jpegDims(buf) ?? gifDims(buf) ?? webpDims(buf) ?? svgDims(buf); } +/** + * html-to-docx 1.8.0 sizes every embedded image with its own bundled copy of + * image-size 1.x. The package.json override cannot reach that copy, and its + * ICNS parser loops forever on a zero-length entry (GHSA-w3rx-r6r6-pgpr), so + * DOCX conversion only hands it bytes that are recognizably PNG, JPEG, GIF or + * WebP, or text that opens as SVG markup. + */ +export function docxSafeImage(buf: Buffer): boolean { + if (pngDims(buf) ?? jpegDims(buf) ?? gifDims(buf) ?? webpDims(buf)) return true; + return /^\uFEFF?\s* { progress.end("Rasterizing diagrams for DOCX"); } finalHtml = convertDiagnosticsForDocx(finalHtml); + finalHtml = finalHtml.replace(/]*>/gi, (tag) => { + const src = tag.match(/\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i); + const payload = (src?.[1] ?? src?.[2] ?? src?.[3] ?? "").match(/^data:[^;,]+;base64,([\s\S]+)$/)?.[1]; + if (payload === undefined) return tag; + let base64 = ""; + try { + base64 = decodeURIComponent(payload); + } catch { /* malformed escape: drop it below */ } + if (/^[A-Za-z0-9+/=\s]+$/.test(base64) && docxSafeImage(Buffer.from(base64, "base64"))) return tag; + warn("docx: dropped an image that is not PNG, JPEG, GIF, WebP or SVG"); + return ""; + }); } // ─── --to html: write the self-contained document, no print round-trip ── diff --git a/make-pdf/test/docx-image-size.test.ts b/make-pdf/test/docx-image-size.test.ts new file mode 100644 index 000000000..ffdfbde5f --- /dev/null +++ b/make-pdf/test/docx-image-size.test.ts @@ -0,0 +1,90 @@ +/** + * DOCX image sizing (#2753). package.json overrides image-size to 2.x, which + * fixes GHSA-5p2g-fcmc-qvqq and GHSA-w3rx-r6r6-pgpr for the lockfile, but + * html-to-docx 1.8.0 runs a bundled image-size 1.x copy the override cannot + * reach. The golden document.xml was captured before the override, so any + * change to the Word output shows up as a diff here. The orchestrator only + * hands html-to-docx images whose bytes are PNG, JPEG, GIF, WebP or SVG, so + * the bundled ICNS parser's infinite loop never runs. + */ +import { describe, expect, test } from "bun:test"; +import { execFileSync } from "node:child_process"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; + +import { docxSafeImage } from "../src/image-size"; +import { generate } from "../src/orchestrator"; + +const FIXTURES = path.join(import.meta.dir, "fixtures", "docx-images"); +const EMU_PER_PX = 9525; +const IMAGES = [ + { file: "wide.png", ext: "png", width: 120, height: 48 }, + { file: "tall.jpg", ext: "jpeg", width: 30, height: 90 }, + { file: "square.gif", ext: "gif", width: 64, height: 64 }, +]; + +/** GHSA-w3rx-r6r6-pgpr: valid ICNS magic and a zero-length entry. */ +function icnsLoop(): Buffer { + const buf = Buffer.alloc(24); + buf.write("icns", 0); + buf.writeUInt32BE(24, 4); + buf.write("ic08", 8); + return buf; +} + +function unzip(args: string[]): Buffer { + return execFileSync("unzip", args, { timeout: 30_000, maxBuffer: 16 * 1024 * 1024 }); +} + +describe("make-pdf --to docx image sizing", () => { + test("embeds every image at its pixel size, matching the image-size 1.x output", async () => { + const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "make-pdf-docx-images-")); + try { + const out = path.join(workDir, "out.docx"); + await generate({ input: path.join(FIXTURES, "doc.md"), output: out, to: "docx", quiet: true }); + + const xml = unzip(["-p", out, "word/document.xml"]).toString("utf8"); + const extents = [...xml.matchAll(//g)].map(m => [Number(m[1]), Number(m[2])]); + expect(extents).toEqual(IMAGES.map(img => [img.width * EMU_PER_PX, img.height * EMU_PER_PX])); + + const media = unzip(["-Z1", out]).toString("utf8").split("\n").filter(name => /^word\/media\/.+/.test(name)); + expect(media).toHaveLength(IMAGES.length); + for (const [i, img] of IMAGES.entries()) { + expect(media[i].endsWith(`.${img.ext}`)).toBe(true); + expect(unzip(["-p", out, media[i]]).equals(fs.readFileSync(path.join(FIXTURES, img.file)))).toBe(true); + } + + const normalized = xml.replace(/image-[A-Za-z0-9_-]{21}\.(png|jpeg|gif)/g, "image-ID.$1"); + expect(normalized.trimEnd()).toBe(fs.readFileSync(path.join(FIXTURES, "document.golden.xml"), "utf8").trimEnd()); + } finally { + fs.rmSync(workDir, { recursive: true, force: true }); + } + }); + + test("only PNG, JPEG, GIF, WebP and SVG markup reach html-to-docx's bundled sizer", () => { + for (const img of IMAGES) expect(docxSafeImage(fs.readFileSync(path.join(FIXTURES, img.file)))).toBe(true); + expect(docxSafeImage(Buffer.from('\n'))).toBe(true); + expect(docxSafeImage(icnsLoop())).toBe(false); + expect(docxSafeImage(Buffer.concat([icnsLoop(), Buffer.from('')]))).toBe(false); + expect(docxSafeImage(Buffer.from("BM not an image at all"))).toBe(false); + }); + + test("an ICNS file named .png is dropped with a warning instead of hanging the conversion", () => { + const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "make-pdf-docx-icns-")); + try { + fs.writeFileSync(path.join(workDir, "evil.png"), icnsLoop()); + fs.writeFileSync(path.join(workDir, "doc.md"), "# ICNS\n\n![evil](evil.png)\n\n![inline](data:image/png;base64," + icnsLoop().toString("base64") + ")\n"); + const out = path.join(workDir, "out.docx"); + const script = `import { generate } from ${JSON.stringify(path.join(import.meta.dir, "..", "src", "orchestrator.ts"))}; +await generate({ input: ${JSON.stringify(path.join(workDir, "doc.md"))}, output: ${JSON.stringify(out)}, to: "docx" });`; + const run = Bun.spawnSync([process.execPath, "-e", script], { timeout: 30_000, stdout: "pipe", stderr: "pipe" }); + const stderr = run.stderr.toString(); + expect(run.exitCode).toBe(0); + expect(stderr.match(/docx: dropped an image that is not PNG, JPEG, GIF, WebP or SVG/g)).toHaveLength(2); + expect(unzip(["-p", out, "word/document.xml"]).toString("utf8")).not.toContain(""); + } finally { + fs.rmSync(workDir, { recursive: true, force: true }); + } + }); +}); diff --git a/make-pdf/test/fixtures/docx-images/doc.md b/make-pdf/test/fixtures/docx-images/doc.md new file mode 100644 index 000000000..fd1fdec1a --- /dev/null +++ b/make-pdf/test/fixtures/docx-images/doc.md @@ -0,0 +1,11 @@ +# Image sizing smoke + +A wide PNG: + +![wide](wide.png) + +A tall JPEG and a square GIF: + +![tall](tall.jpg) + +![square](square.gif) diff --git a/make-pdf/test/fixtures/docx-images/document.golden.xml b/make-pdf/test/fixtures/docx-images/document.golden.xml new file mode 100644 index 000000000..e9e4c4c04 --- /dev/null +++ b/make-pdf/test/fixtures/docx-images/document.golden.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + Image sizing smoke + + + + + + + + + A wide PNG: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A tall JPEG and a square GIF: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/make-pdf/test/fixtures/docx-images/square.gif b/make-pdf/test/fixtures/docx-images/square.gif new file mode 100644 index 000000000..3fcc4a7c7 Binary files /dev/null and b/make-pdf/test/fixtures/docx-images/square.gif differ diff --git a/make-pdf/test/fixtures/docx-images/tall.jpg b/make-pdf/test/fixtures/docx-images/tall.jpg new file mode 100644 index 000000000..7bdd8c0a9 Binary files /dev/null and b/make-pdf/test/fixtures/docx-images/tall.jpg differ diff --git a/make-pdf/test/fixtures/docx-images/wide.png b/make-pdf/test/fixtures/docx-images/wide.png new file mode 100644 index 000000000..f388b4ab7 Binary files /dev/null and b/make-pdf/test/fixtures/docx-images/wide.png differ diff --git a/package.json b/package.json index fc80e3503..6a9f4b183 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "adm-zip": "^0.6.1", "fast-uri": "3.1.8", "hono": "4.13.13", + "image-size": "2.0.4", "ip-address": "10.7.3", "proxy-addr": "2.0.8", "sharp": "0.35.5" diff --git a/test/osv-config-wiring.test.ts b/test/osv-config-wiring.test.ts index 69cab8835..442924b45 100644 --- a/test/osv-config-wiring.test.ts +++ b/test/osv-config-wiring.test.ts @@ -50,4 +50,12 @@ describe("osv-scanner config wiring", () => { const page = fs.readFileSync(path.join(ROOT, "lib", "diagram-render", "dist", "diagram-render.html"), "utf8"); expect(page).not.toMatch(/chokidar|FSWatcher/); }); + + test("the chokidar override keeps braces (GHSA-vfj7-8cjw-p6xm, no fixed release) out of lib/diagram-render", () => { + const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "lib", "diagram-render", "package.json"), "utf8")); + expect(pkg.overrides.chokidar).toMatch(/^4\./); + const lock = fs.readFileSync(path.join(ROOT, "lib", "diagram-render", "bun.lock"), "utf8"); + expect(lock).toMatch(/"chokidar": \["chokidar@4\.\d+\.\d+"/); + expect(lock).not.toMatch(/"braces": \["braces@/); + }); }); diff --git a/test/osv-ignore-expiry.test.ts b/test/osv-ignore-expiry.test.ts index c185d3ec3..d88b9fdb5 100644 --- a/test/osv-ignore-expiry.test.ts +++ b/test/osv-ignore-expiry.test.ts @@ -58,18 +58,15 @@ describe('OSV suppression expiry', () => { for (const text of ['"$SCAN_RESULT" = "failure"', 'gh issue create', 'gh issue comment', 'gh issue close']) expect(run).toContain(text); }); - test('the image-size entries name the fixed release and the pin that blocks it (#2753)', () => { - const toml = fs.readFileSync(path.join(import.meta.dir, '..', '.osv-scanner.toml'), 'utf8'); - const entries = toml.split('[[IgnoredVulns]]').slice(1).filter(e => /^id = "GHSA-(?:5p2g-fcmc-qvqq|w3rx-r6r6-pgpr)"/m.test(e)); - expect(entries).toHaveLength(2); - for (const entry of entries) { - expect(entry).not.toContain('No fixed version exists'); - expect(entry).toMatch(/^reason = ".*image-size 2\.0\.3.*\^1\.0\.0.*"$/m); - expect(entry).toContain('ignoreUntil = 2026-11-30T00:00:00Z'); - } - // The stated blocker is the lockfile's: html-to-docx asks for image-size ^1.0.0 and resolves a 1.x. + test('image-size is overridden to a fixed 2.x and its suppressions are gone (#2753)', () => { + const toml = fs.readFileSync(path.join(ROOT, CONFIG), 'utf8'); + expect(toml).not.toMatch(/GHSA-(?:5p2g-fcmc-qvqq|w3rx-r6r6-pgpr)/); + const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); + expect(pkg.overrides['image-size']).toBe('2.0.4'); + // html-to-docx still asks for ^1.0.0; the override is what moves the lockfile off the affected 1.x. const lock = fs.readFileSync(path.join(ROOT, 'bun.lock'), 'utf8'); expect(lock).toMatch(/"html-to-docx": \["html-to-docx@[^"]+", "", \{ "dependencies": \{[^}]*"image-size": "\^1\.0\.0"/); - expect(lock).toMatch(/"image-size": \["image-size@1\.\d+\.\d+"/); + expect(lock).toMatch(/"image-size": \["image-size@2\.0\.4"/); + expect(lock).not.toMatch(/"image-size": \["image-size@1\./); }); });