Skip to content

fix(security): escape MDX syntax in reference-architecture submissions - #812

Merged
mrbobbytables merged 1 commit into
mainfrom
sec/mdx-escape-issue-import
Sep 29, 2026
Merged

mrbobbytables merged 1 commit into
mainfrom
sec/mdx-escape-issue-import

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Security Fix

Closes #808.

Pages under docs/ compile as MDX: this repo pins @docusaurus/core
3.10.2 and sets no markdown.format in docusaurus.config.js, so the
Docusaurus 3 default (mdx) applies. scripts/import-architecture-issue.mjs
wrote a submitter's free-text answers into docs/architectures/<id>.md
verbatim, so everything a submitter typed reached a JavaScript compiler.

.github/workflows/architecture-submission.yml runs that importer and then
npm run build on the result, in a job holding contents: write,
pull-requests: write and issues: write, before
peter-evans/create-pull-request commits the working tree.

What changes

New scripts/lib/mdx-escape.mjs exporting escapeMdx() / unescapeMdx(),
applied to the submitted answers in renderSection().

The escaping is deliberately not in the shared cleanMarkdown():
renderProjectsSection() emits real <CNCFProjectCard /> markup and the
sections are joined before cleanMarkdown() runs, so escaping there would
destroy the cards. Nothing on the cncf/architecture importer's path changes.

Outside fenced code blocks and inline code spans, escapeMdx():

  • replaces { / } with &#123; / &#125;,
  • replaces < with &lt;, preserving <https://…> and <user@host> autolinks,
  • entity-escapes the first letter of a line-initial import / export
    (&#105;mport, &#101;xport), which compiles with no top-level ESM
    statement while still displaying as import / export.

Every substitution is a character reference that renders as the character the
author typed, so escaped prose displays as written. MDX evaluates nothing
inside code, so fences and code spans are passed through untouched and an
architecture description can still show a YAML or JSON snippet with braces
intact. The catalog summary is run back through unescapeMdx() because it is
stored as JSON and rendered as a plain text node, where a character reference
would show through literally.

jsxElement() and the artwork mirror were already safe and are untouched.

Verification

Before, the importer's output built to <p>Normal-looking prose. <!-- -->4</p>
— 2 + 2 evaluated in Node inside the build. After, the same submission
builds to <p>Normal-looking prose. {2 + 2}</p> and
<p>&lt;span onClick={() => alert(1)}>click&lt;/span></p>: the author's
characters, displayed, with nothing for the compiler to execute.

  • npm run check — exit 0
  • npm run test:unit — 1452 tests, 1450 pass, 0 fail
  • npm run test:unit:coverage:check — exit 0; scripts/lib/mdx-escape.mjs
    at 100.00% lines / 98.72% regions
  • npm run build:production — exit 0

30 new tests (24 unit in tests/mdx-escape.test.mjs, 6 integration through
the existing importer sandbox). Mutation-checked both directions: dropping
the escapeMdx() call fails the 3 injection tests, and dropping the
fenced-code carve-out fails the 2 verbatim-code tests.


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

Pages under docs/ compile as MDX: @docusaurus/core 3 defaults
markdown.format to `mdx` and docusaurus.config.js sets no override. The
reference-architecture issue importer wrote submitters' free-text answers
into docs/architectures/<id>.md verbatim, so `{...}` reached the compiler
as a JavaScript expression, `<Tag onClick={...}>` as a live React element,
and a block-initial `import`/`export` as a top-level ESM statement.

All three executed in Node during the `npm run build` step of
architecture-submission.yml, before create-pull-request committed the
working tree, and the JSX props would have fired in visitors' browsers
once the generated page merged.

Escape the answers in renderSection() rather than in the shared
cleanMarkdown(): the sections are joined with the generated
<CNCFProjectCard /> markup before that helper runs, so escaping the joined
text would destroy the cards.

MDX evaluates nothing inside a fenced code block or an inline code span, so
the new escaper leaves those verbatim and an architecture description can
still show a YAML or JSON snippet. Every substitution is a character
reference that renders as the original character, so escaped prose displays
as written; the catalog summary is unescaped again because it is stored as
JSON and rendered as a plain text node.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: hivecommons-hive[bot] <hivecommons-hive@hive.kubestellar.io>
@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 automatically remove the hold label once current policy no longer requires a level hold for "sec-check". If this is an outreach PR, a human must review it and remove the label.

@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

CI note (ci-maintainer): the red Reference architecture submission check on the current head is not caused by this PR's diff. The branch predates #815 (merged 2026-09-28T22:40Z), so it still carries the invalid workflow version with a job-level if: referencing env.SUBMISSION_LABEL; GitHub fails workflow validation on every push to the branch (zero jobs, no log). Default branch is clean. Rebasing/merging main into this branch clears the check; no change to this PR's files is needed.

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

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sec-check] Reference-architecture issue answers are compiled as MDX, giving submitters build-time code execution and stored XSS

1 participant