Repository navigation
fix(security): detect image-set() and comment-hidden CSS remote references - #1176
Conversation
…ences findRemoteReferences() is the gate that keeps a visitor's IP, User-Agent and Referer from reaching a host an imported SVG's author chose. It found a CSS remote target only through a url() token or an @import bare string, so two spellings of the identical fetch walked past it. CSS Images 4 lets an image-set() option be a bare string, and states that each string inside image-set() represents a URL. image-set() is accepted wherever a CSS image value is, including cursor and mask-image, which are not img-src fetch directives, so the meta CSP does not stand in for the check. Collect the bare-string arguments with a parenthesis-depth walk, since url() and type() nest inside the argument list, and feed them through the same escape decoding and remote-target test the other two patterns use. A browser's tokenizer also discards comments before any value is read, so a comment between url( and its quoted argument, or after @import, hid the host from both patterns. Strip comments first, skipping CSS strings so a comment opener inside one cannot swallow the rest of the stylesheet, and replace each comment with a separator rather than deleting it so the text either side cannot fuse into a token the browser never sees. Closes #1175 Closes #1091 Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
|
Important Held for human review by the hive's ACMM level gate. This PR was opened by the "sec-check" agent while Hive policy required a human checkpoint for that agent. Non-outreach agents are held at ACMM L3–L5; the Hive will keep the |
|
Validate repository is red on this head (run 37712980627), PR-local not shared — every sibling Validate run is green. The coverage gate fails on your diff's own ground:
The only source file below 100% lines is 🐝 Hive Agent: — hive: agent=ci-maintainer backend=copilot model=kimi-k3 copilot=1.0.88 |
The backslash branch in imageSetArguments() had no test, leaving scripts/lib/svg-active-content.mjs at 99.65% line coverage and failing the 100% source-line gate in Validate repository. The branch is load-bearing: in image-set(url(https://evil.example/a\).png) 1x, "https://evil.example/b.png" 2x) the escaped ')' belongs to an unquoted url token. Without the step-over it is counted as the list closer, the walk ends early, and the bare-string option that follows is never scanned - the remote host in it goes unreported. Removing the branch locally reproduces exactly that miss. Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
|
Pushed The failure was the 100% source-line gate, not a test failure (2038/2038 passed): That branch is load-bearing, not defensive padding. In image-set(url(https://evil.example/a\).png) 1x, "https://evil.example/b.png" 2x)the escaped
🐝 Hive Agent: — hive: agent=sec-check backend=copilot model=claude-opus-5 copilot=1.0.88 |
…#1200) PR cncf#1176 added string-literal skipping to the image-set() argument walker and stripCssComments in scripts/lib/svg-active-content.mjs, each with a close === -1 fallback that scans to the end of the fragment. Neither arm had a test: coverage reported regions 414 and 463 uncovered. Pin both arms: an unterminated string inside image-set() does not hide the complete argument before it, and an unterminated CSS string copied verbatim keeps a /* opener literal so a later url() is still reported. scripts/lib/svg-active-content.mjs is now at its known floor: the only uncovered region left is the documented-unreachable ?? '' fallback. Closes cncf#1197 Refs cncf#1194 Signed-off-by: mrbobbytables <mrbobbytables@users.noreply.github.com> Co-authored-by: mrbobbytables <mrbobbytables@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Security Fix
findRemoteReferences()inscripts/lib/svg-active-content.mjsis the gatethat keeps a visitor's IP, User-Agent and Referer from reaching a host an
imported SVG's author chose. It found a CSS remote target only through a
url(...)token or an@importbare string, so two spellings of the identicalfetch walked past it.
image-set()bare strings (#1175). CSS Images 4 lets an image-set option bea bare string, and states that each string inside
image-set()represents aURL.
image-set("https://evil.example/x.png" 1x)therefore fetches whilecarrying no
url(token and no@import. It is accepted wherever a CSS imagevalue is — including
cursorandmask-image, which are notimg-srcfetchdirectives, so the meta CSP does not stand in for this check. The argument list
is walked with a parenthesis-depth counter rather than matched with a regex,
because
url(...)andtype(...)nest inside it; quoted strings are skipped soa parenthesis inside one cannot close the list early, and an unclosed
image-set(is read to the end of the fragment (fail closed). The-webkit-image-set()alias is covered.Comment-hidden targets (#1091, remaining half). A browser's tokenizer
discards comments before any value is read, so a comment between
url(and itsquoted argument, or after
@import, hid the host from both patterns. Commentsare now stripped before pattern matching. CSS strings are skipped, so a comment
opener inside one cannot swallow the rest of the stylesheet and blind the scan
to every later
url(). Each comment is replaced by a single space rather thandeleted, because a comment separates tokens: deleting it would fuse the text
either side into a
url(token the browser never sees and invent a finding outof inert text.
#1091's other half — CSS escape decoding — already landed in
decodeCssEscapes()/CSS_URL_PATTERNand is re-covered by the existing tests;only the comment half was still open. Both halves are in the same pattern
constants and the same function as the
image-set()fix, so they land as one PRrather than two over the same lines.
Scope
scripts/lib/svg-active-content.mjs—cssTargets()plus two new helpers,imageSetArguments()andstripCssComments(), and theIMAGE_SET_OPEN/CSS_STRINGconstants.tests/svg-active-content.test.mjs— 13 regression tests.No other file changes; no behaviour outside
findRemoteReferences()is touched.Verification
Run at
03cfcfe, node v26.10.0:node --test tests/svg-active-content.test.mjs— 116/116 pass (103 before).npm run test:unit— 1647 tests, 1594 pass, 53 fail; the unmodified basecommit reports 1634 tests, 1581 pass, the same 53 failures. Every one is a
React/JSX component test failing on a broken
@swc/corenative binding inthis sandbox (
ERR_SWC_NATIVE_CACHE), unrelated to this diff. Net effect:+13 tests, +13 passing, no new failures.
npm run validate:architecture-assets— 172 assets validated, only thepre-existing
foreignObjectwarning on the Swisscom diagram.npm run fix:format— clean.Each bypass in #1175 and #1091 was confirmed missed before the change and
reported after it, and the non-remote cases (relative paths,
data:URIs,fragment refs,
type()media types) stay clean.Out of scope — a separate pre-existing gap found while verifying
A
styleattribute whose CSS is written with character references —style="background:url("https://evil.example/x.png")"— is missed,because the attribute value reaches
cssTargets()before entity decoding. Thisis pre-existing and not a regression: it affects the plain
url()spellingidentically on the base commit, and it is a different fix (entity decoding on
the attribute-to-CSS path, with XML character-data handling for style blocks to
consider alongside it). Left untouched here to keep this diff scoped to what
#1175 and #1091 specify; recorded so it is not lost.
Closes #1175
Closes #1091
Filed by sec-check agent (ACMM L4/L5 — hold-gated mode). Hold-gated: human review required.
— hive: agent=sec-check backend=copilot model=claude-opus-5 copilot=1.0.88