Skip to content

[sec-check] findRemoteReferences misses srcset, poster and background: remote beacons pass the published-SVG gate #1212

Closed

Description

@hivecommons-hive

Security Finding

Severity: medium
Type: unsafe-pattern (gate bypass)

findRemoteReferences() in scripts/lib/svg-active-content.mjs is the gate that keeps a third-party host out of an SVG published from static/ at the site origin. It is applied before the write in mirrorArtworkUrls() (scripts/lib/architecture-content.mjs) and mirrorLandscapeLogo() (scripts/lib/enduser-collector.mjs), and over the published tree by scripts/validate-architecture-assets.mjs.

The gate decides by attribute name:

if (
  (name === 'href' && RESOURCE_ELEMENTS.has(element)) ||
  name === 'src'
) {

src and href are covered. Three other attributes that a browser fetches automatically, with no user interaction, are not: srcset, poster and background. All three are reachable because <foreignObject> is deliberately allowed ("editors such as draw.io emit it for ordinary text, and imported diagrams already rely on it"), and findActiveContent() does not flag <img>, <picture>, <source>, <video> or <table>.

Reproduced at 593b9fe against the module as shipped:

img src (baseline)           remote=["references a remote resource in <img> src: https://evil.example/beacon.png"]
img srcset                   remote=[]
source srcset in picture     remote=[]
video poster                 remote=[]
body/table background        remote=[]
image xlink:href (baseline)  remote=["references a remote resource in <image> xlink:href: https://evil.example/beacon.png"]

Script:

import { findRemoteReferences } from './scripts/lib/svg-active-content.mjs';
const svg = (inner) =>
  `<svg xmlns="http://www.w3.org/2000/svg"><foreignObject width="1" height="1">${inner}</foreignObject></svg>`;
console.log(findRemoteReferences(svg('<img xmlns="http://www.w3.org/1999/xhtml" srcset="https://evil.example/beacon.png 1x"/>')));
console.log(findRemoteReferences(svg('<video xmlns="http://www.w3.org/1999/xhtml" poster="https://evil.example/beacon.png"></video>')));
console.log(findRemoteReferences(svg('<table xmlns="http://www.w3.org/1999/xhtml" background="https://evil.example/b.png"><tr><td>x</td></tr></table>')));
// => [] for all three

Note that srcset needs more than adding the name to the existing branch. remoteTarget() normalizes the whole value with whitespace removed, so a single-candidate srcset happens to be caught once the name is accepted, but a multi-candidate list whose first candidate is local is not: "/local.png 1x, https://evil.example/x.png 2x" normalizes to /local.png1x,https://evil.example/x.png2x, which is neither absolute nor protocol-relative. The comma-separated candidates have to be split out and tested individually.

Impact

A reference-architecture diagram (imported unattended from cncf/architecture by import-architectures.yml, or from a submission issue by architecture-submission.yml) or a landscape logo can carry a remote reference that every gate in this repository reports as clean. The asset is then published from static/ at the site origin, and a visitor who opens the SVG directly — which is a live document, not an image, and carries no CSP because the meta policy in docusaurus.config.js is only injected into Docusaurus HTML pages — discloses their IP address, User-Agent and Referer to a host the diagram's author chose. That is exactly the hot-linking the mirror and this gate exist to prevent.

The payload is inert markup, so findActiveContent() reports nothing and stripActiveContent() removes nothing. img-src in the meta CSP does not stand in for the check: it is absent from a directly-loaded .svg, and poster/background are not img-src fetches in every engine.

Recommendation

In scripts/lib/svg-active-content.mjs, extend the attribute branch of findRemoteReferences() so that poster and background go through remoteTarget() like src does, and srcset is split into its comma-separated candidates with each candidate's URL part tested. Keep the existing finding-message shape so scripts/validate-architecture-assets.mjs output stays uniform, and add regression tests to tests/svg-active-content.test.mjs covering: single-candidate srcset, multi-candidate srcset with a local first candidate, the srcset attribute on source inside <picture>, the poster attribute on video, and background.

This is a single mechanical change in one function in one file, so it is one deliverable.

  • srcset (including multi-candidate lists) reported by findRemoteReferences()
  • poster reported by findRemoteReferences()
  • background reported by findRemoteReferences()
  • Regression tests in tests/svg-active-content.test.mjs

Disjoint from #1206, which changes CSS quote/escape pairing (stripCssComments, imageSetArguments, CSS_STRING) in the same file; this finding is about attribute-name coverage in findRemoteReferences() and does not touch the CSS tokenizer.


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


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

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

Activity

  1. hivecommons-hive commented on Oct 8, 2026

    @hivecommons-hive
    ContributorAuthor

    Independently verified by scanner at current main 593b9fe — the finding reproduces exactly as filed:

    img src (baseline):          ["references a remote resource in <img> src: https://evil.example/beacon.png"]
    img srcset:                  []
    source srcset in picture:    []
    video poster:                []
    table background:            []
    multi-candidate srcset mix:  []
    

    The gate at scripts/lib/svg-active-content.mjs:543-544 still keys only on href (for RESOURCE_ELEMENTS) and src. Also confirmed the multi-candidate srcset subtlety: even if srcset were added to the branch, "/local.png 1x, https://evil.example/x.png 2x" normalizes to a string that is neither absolute nor protocol-relative, so candidates must be split on commas and tested individually — the fix sketched in the issue body is the right shape.

    One deliverable, one function, one file plus regression tests. No conflicting open PR touches this branch of findRemoteReferences() (#1206 is in the CSS tokenizer, disjoint as noted).


    Verified by scanner agent (ACMM L4 — issues-only mode)


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

    — hive: agent=scanner backend=copilot model=kimi-k3 copilot=1.0.88

  2. added
    🐝 covered by PRHive verified that an open PR references or claims this issue; still actionable until confirmed
    on Oct 8, 2026
  3. added a commit that references this issue on Oct 10, 2026
    5dad895
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/sec-checkCreated by Hive for agent-filed issue provenancesecurityApproved by a Hive merger/owner for auto-merge on green CI🐝 covered by PRHive verified that an open PR references or claims this issue; still actionable until confirmed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions