Skip to content

fix(security): resolve reference-style images before the remote-image demotion in cleanMarkdown - #1229

Closed
hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
sec/fix-image-reference-beacons
Closed

hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
sec/fix-image-reference-beacons

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Security Fix

cleanMarkdown() in scripts/lib/architecture-content.mjs demotes a remote
image to a plain link so a published architecture page never hot-links a
third-party host. Its pattern matched only the inline form ![alt](dest).
CommonMark's three reference forms — full ![alt][label], collapsed
![alt][] and shortcut ![alt] — keep their destination in a link-reference
definition elsewhere in the document, so none of them were inspected, demoted
or rewritten and the compiled page rendered the submitted
<img src="https://third-party.example/..."> verbatim. Every visitor's browser
fetched it on page load, handing that host their IP, User-Agent and Referer.

findActiveContent() reports script-capable schemes and disallowed elements,
not remote resource references, so scripts/validate-architectures.mjs did not
catch it either. Both producers share this transform, so the unattended daily
cncf/architecture import and the community submission issue form were both
affected — escapeMdx() neutralises < and { but leaves !, [ and ]
alone.

What this changes

inlineImageReferences() resolves each reference form against the document's
link-reference definitions before the existing image rules run, so every image
form reaches the same remote-demotion and the same local-path rewrite:

  • a remote destination is demoted to a plain link, in all three reference forms
    and for every spelling the inline rule already covers (absolute,
    protocol-relative, uppercase scheme, angle-bracketed);
  • a cncf/artwork destination now reaches its mirrored /img/cncf-projects/...
    path, and a relative one its /img/architectures/<id>/... scope, instead of
    rendering broken;
  • labels are matched the way CommonMark matches them (trimmed, lowercased,
    internal whitespace collapsed);
  • a destination carrying whitespace or parentheses cannot be expressed in the
    [^\)]+ destinations the downstream rules match, so it fails closed: the !
    is dropped and the construct renders as a link reference, which fetches
    nothing.

The definition line is left in place — a definition renders as nothing, and it
may still be the target of a text link on the same page.

Verification

node --test over tests/architecture-content-clean-markdown.test.mjs,
architecture-content-mirror, import-architecture-issue,
import-architecture-issue-project-href, import-architectures,
import-architectures-fallbacks, validate-architectures and
architecture-catalog-contract: 130 pass, 0 fail. Eight new tests cover the
three reference forms, the destination spellings, label normalisation, the
artwork and relative rewrites, the fail-closed paren destination, a reference
with no definition, and a text link reference (which must not become an image).
npx prettier --check clean on both files.

End to end, the submission-issue repro from the issue now generates
[tracker](https://evil.example/pixel.png) where it previously generated
![tracker][beacon]. No checked-in page changes: docs/architectures/colopl.md
is the only page carrying link-reference definitions and they are all text
links.

Scope

Files: scripts/lib/architecture-content.mjs (cleanMarkdown,
new inlineImageReferences) and
tests/architecture-content-clean-markdown.test.mjs. Disjoint from the open
security PRs #1206 and #1213 (scripts/lib/svg-active-content.mjs,
tests/svg-active-content.test.mjs) and #1219
(scripts/lib/mdx-active-content.mjs, tests/mdx-active-content.test.mjs).

Closes #1228


Hold-gated: human review required.

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

… demotion

cleanMarkdown() demotes a remote image to a plain link so a published
architecture page never hot-links a third-party host, but its pattern
matched only the inline form `![alt](dest)`. CommonMark's three
reference forms -- full `![alt][label]`, collapsed `![alt][]` and
shortcut `![alt]` -- keep their destination in a link-reference
definition, so none of them were inspected, demoted or rewritten and the
compiled page rendered the submitted <img> verbatim. Every visitor's
browser then handed that host their IP, User-Agent and Referer.

findActiveContent() reports script-capable schemes and disallowed
elements, not remote resource references, so nothing downstream caught
it either. Both producers share this transform, so the daily
cncf/architecture import and the submission issue form were both
affected; escapeMdx() neutralises < and { but leaves ! and [ alone.

Resolve each reference form against the document's definitions before
the existing image rules run, so every image form reaches the same
demotion and the same local-path rewrite: a cncf/artwork destination now
also reaches its mirrored path and a relative one its
/img/architectures/<id>/ scope instead of rendering broken. A
destination carrying whitespace or parentheses cannot be expressed in
the [^)]+ destinations those rules match, so it fails closed by dropping
the ! and rendering as a link reference, which fetches nothing. The
definition line is left in place: it renders as nothing and may still be
the target of a text link on the same page.

Closes #1228

Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
@hivecommons-hive hivecommons-hive Bot added the hold label Oct 9, 2026
@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

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 outreach agent is always held because it publishes project-facing communication.

Hive will keep the hold label until a human removes it. Operators can make a deliberate one-off release during an ACMM level change with release_level_holds=true, but level changes never release this hold automatically.

@mrbobbytables

Copy link
Copy Markdown
Member

Superseded by #1245, which replaces the regex scanners with parser-based ones (the approach decided on #1194) and covers this fix, with regression tests. Closing in favour of that PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sec-check] Reference-style images bypass the remote-image demotion in cleanMarkdown, publishing third-party beacons on architecture pages

1 participant