Skip to content

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

Closed

Description

@hivecommons-hive

Security Finding

Severity: medium
Type: unsafe-pattern (third-party beacon in published pages)

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:

.replace(
  new RegExp(`!\\[([^\\]]*)\\]\\((${REMOTE_DESTINATION}[^\\)]+)\\)`, 'g'),
  (_, alt, url) => {
    const asset = projectAsset(url);
    return asset ? `![${alt}](${asset})` : `[${alt}](${url})`;
  },
)

That pattern only matches the inline image form ![alt](dest). CommonMark
has three more image forms whose destination lives in a link-reference
definition elsewhere in the document — full ![alt][label], collapsed
![alt][] and shortcut ![alt] — and none of them are matched. The
destination is never inspected, never demoted and never rewritten, so the
compiled page renders <img src="https://third-party.example/..."> exactly as
submitted.

findActiveContent() (scripts/lib/mdx-active-content.mjs), which
scripts/validate-architectures.mjs runs over every generated page, reports
script-capable schemes and disallowed elements — not remote resource
references — so nothing downstream catches it either.

Both producers share this transform and are therefore both affected:
scripts/import-architectures.mjs (unattended daily import of
third-party cncf/architecture Markdown) and
scripts/import-architecture-issue.mjs (community submission issue form).
escapeMdx() neutralises < and {, but leaves !, [ and ] alone, so
the reference form survives the submission path intact.

Reproduction

Submitting a reference-architecture issue whose "Describe your organisation"
answer is

We are a demo. ![tracker][beacon]

[beacon]: https://evil.example/pixel.png

produces docs/architectures/<id>.md containing those two lines verbatim, and
npm run validate:architectures reports Validated 9 architecture records
with no error. The same text written inline as
![tracker](https://evil.example/pixel.png) is correctly demoted to
[tracker](https://evil.example/pixel.png).

Directly against the library:

cleanMarkdown('![alt][beacon]\n\n[beacon]: https://evil.example/pixel.png', 'demo')
// -> '![alt][beacon]\n\n[beacon]: https://evil.example/pixel.png'   (unchanged)
cleanMarkdown('![alt](https://evil.example/pixel.png)', 'demo')
// -> '[alt](https://evil.example/pixel.png)'                        (demoted)

Impact

Any submitter — or anyone whose content reaches cncf/architecture — can place
an <img> on a published cncf/endusers page pointed at a host they control.
Every visitor's browser fetches it on page load, handing that host the
visitor's IP address, User-Agent and Referer (which names the page being
read). That is precisely the hot-linking and visitor-tracking the inline rule
exists to prevent, reachable by choosing a different Markdown spelling of the
same image. A host that serves the beacon can also stop serving it, or serve
different bytes later, since nothing is mirrored.

The same gap means a reference-style image pointing at a genuine cncf/artwork
URL is never rewritten to its mirrored /img/cncf-projects/... path, and a
relative reference-style image is never scoped to
/img/architectures/<id>/..., so both render broken instead of local.

Recommendation

Resolve reference-style images against the document's link-reference
definitions before the existing image rules run, so every image form reaches
the same remote-demotion and local-path rewrite. Concretely, in
scripts/lib/architecture-content.mjs:

  • collect link-reference definitions ([label]: destination, with the optional
    <...> destination form and an optional title), normalising labels the way
    CommonMark does (trim, lowercase, collapse internal whitespace);
  • rewrite ![alt][label], ![alt][] and ![alt] to ![alt](destination)
    when a definition matches, skipping a match already followed by ( (that is
    the inline form the existing rules handle);
  • fail closed for a destination containing whitespace or parentheses — which
    the downstream [^\)]+ patterns cannot express — by dropping the ! so the
    construct renders as a link reference rather than an image.

Leaving the definition line in place is correct: a definition renders as
nothing, and it may still be the target of a text link elsewhere in the page.

Cover it in tests/architecture-content-clean-markdown.test.mjs with the full,
collapsed and shortcut forms for a remote destination, a cncf/artwork
destination and a relative destination.

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


🐝 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. added
    securityApproved by a Hive merger/owner for auto-merge on green CI
    agent/sec-checkCreated by Hive for agent-filed issue provenance
    🐝 covered by PRHive verified that an open PR references or claims this issue; still actionable until confirmed
    on Oct 9, 2026
  2. 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