fix(cli-upload): drop image-size for a built-in PNG/JPEG reader (PER-10427) - #2382
Open
aryanku-dev wants to merge 2 commits into
Open
fix(cli-upload): drop image-size for a built-in PNG/JPEG reader (PER-10427)#2382aryanku-dev wants to merge 2 commits into
aryanku-dev wants to merge 2 commits into
Conversation
…10427) `image-size` is archived upstream and carries three unfixable high-severity advisories (CVE-2025-71329, CVE-2025-71330), so `npm audit` fails for anyone who installs @percy/cli. There is no patched version to move to — every published release through 2.0.2 is affected — and 2.x would also undo the Node 14 support that #2301 pinned ~1.0.2 to keep. The command only ever accepts png, jpg and jpeg (ALLOWED_FILE_TYPES), so a general purpose image parser was always more surface than this needed. Reading the two formats we actually support is about eighty lines and removes the dependency outright. The advisories were reachable here, not just theoretical: `image-size` picks its parser from magic bytes while `percy upload` filters on extension, so an ICNS buffer named `.png` reached the ICNS parser and wedged the event loop — `percy upload` hung indefinitely and did not respond to SIGTERM. Such a file is now skipped with a log line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI enforces 100% coverage; image-size.js left branches 51, 56-59 and 70 unhit. Adds fixtures for the four segment-walk exits that had no test: walking off the chain into non-marker data, a standalone marker preceding the frame header, SOS/EOI reached before any frame, and a file that ends before the frame payload it announced. Also drops the optional chaining on the marker read. The loop bound `offset + 4 <= fileSize` already proves those four bytes exist, so the null arm was unreachable and could never be covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2380 / PER-10427.
Root cause
npm auditfails for anyone installing@percy/clibecausepackages/cli-upload/package.jsondepends onimage-size, which has three high-severity advisories and no patched release:All are CWE-835 (infinite loop) and all share one shape: a zero-valued length field leaves the read offset unchanged, so the parser loops forever and blocks the event loop. Every published version through 2.0.2 is affected, and the upstream repo was archived on 2026-06-03 — the maintainer has said they will not keep fielding these reports on GitHub. There is nothing to upgrade to.
Bumping was not an option for a second reason: #2301 deliberately pinned
~1.0.2to keep Node 14 support, whichimage-size2.x drops.This was reachable, not just theoretical
image-sizeselects its parser from magic bytes, whileupload.jsfilters candidates by extension (ALLOWED_FILE_TYPES). A file namedscreenshot.pngwhose contents begin with the ICNS magic bytes therefore reached the ICNS parser atpackages/cli-upload/src/upload.js:100.Verified against the shipped code — a 64-byte crafted
.pngdropped into an upload directory:Because the loop blocks the event loop, the process cannot handle the signal and needs
SIGKILL. Note this is a local CLI reading the user's own directory, so the practical severity is well below the 8.7 CVSS — but it is a real hang, and it is what makes the audit finding non-dismissable.Why we depended on it in the first place
Worth stating, because it changes how this reads:
image-sizewas never a considered choice for the formats it supports. It arrived in the package's very first commit — 1175e55, "✨ Add @percy/cli-upload package", Mar 2020 — as a one-line convenience to get{ width, height }for three uses that still exist today: the clampedwidths,minHeight, and the<img width height>attributes on the generated root DOM resource.That same first commit already restricted the command to PNG and JPEG, in both places:
Across the package's entire history the regex has been renamed (
ALLOWED_IMAGE_TYPES→ALLOWED_FILE_TYPES) but never broadened —git log -Sover every revision of the file yields exactly one distinct value. So we have carried a ~20-format parser for a two-format command for five years, and the ICNS parser that hangs was never something this command wanted.This PR therefore isn't narrowing scope to dodge a CVE; it aligns the dependency with a constraint that has been in place since day one.
Fix
percy uploadonly ever acceptspng,jpgandjpeg, so a general-purpose parser for ~20 formats was always more surface than this command needed.packages/cli-upload/src/image-size.jsreads the two formats we actually support, with bounded positioned reads:Anything else — a GIF, a truncated file, a crafted ICNS buffer — returns
nulland the file is skipped with a log line instead of crashing or hanging the whole upload.This removes
image-size(and its sole transitive depqueue) from the tree entirely, so the audit finding goes away permanently rather than waiting on an archived project.Behaviour change
Previously a file with an accepted extension but unreadable contents threw and failed the entire
uploadrun. It is now skipped:This mirrors the existing
Skipping unsupported file typepath. Valid PNG/JPEG uploads are unaffected.Testing
packages/cli-upload/test/unit/image-size.test.js— 10 new specs covering PNG and JPEG dimension reads (including a 200x150 JPEG whose frame header sits past kilobyte-scale quantization/Huffman tables, so segment walking is exercised rather than a fixed offset), non-images, truncated input, and termination on all three malformed shapes.upload.test.jsgains an end-to-end regression asserting a crafted ICNS file named.pngis skipped and the build still finalizes. Its fixtures now use real PNG/JPEG bytes — the previous fixture was a GIF written through.toString(), which only survived because GIF headers happen to be UTF-8 safe.End-to-end against a directory holding a 1280x720 PNG, a 640x480 JPEG and the crafted payload: completes in ~6s, reads both real images, skips the crafted one.
🤖 Generated with Claude Code