Skip to content

fix(mcp): board_escalate options bypass + escape decision-comment markdown (#300) - #301

Merged
dngioidev merged 2 commits into
mainfrom
fix/300-escalate-options-delimiter-markdown
Jul 26, 2026
Merged

fix(mcp): board_escalate options bypass + escape decision-comment markdown (#300)#301
dngioidev merged 2 commits into
mainfrom
fix/300-escalate-options-delimiter-markdown

Conversation

@dngioidev

Copy link
Copy Markdown
Owner

Closes #300

Follow-up from #296's security review (parent epic #174). Fixes both findings on the halt-and-ask escalation flow.

Findings fixed

  1. MEDIUM — board_escalate.options '|'-delimiter bypass. The MCP handler pipe-joined its options array and escalate.mjs re-split on '|', so an option element containing an embedded '|' reconstituted into extra options downstream — defeating the maxItems: 20 bound and injecting fabricated options into the human-facing GitHub decision comment.
  2. INFO→fix — unescaped Markdown. reason/context/options were interpolated unescaped into the trusted Markdown decision comment.

Approach

  • Structured array end-to-end. The MCP board_escalate handler now passes args.options straight through to runEscalate (no pipe-join). New normalizeOptions splits only the string form, so the CLI --options "a|b|c" contract is preserved while an array element's embedded '|' stays literal and cannot fabricate options. The schema's maxItems: 20 bound now genuinely holds.
  • escapeMd escapes backslash + Markdown/HTML control punctuation on reason/context/options before they render, so caller text cannot forge block structure, a fake "recommended" tag, or a <!-- forge:... --> marker. Recommend-matching stays on the raw option values.
  • Zero new runtime deps. ASCII-only additions. Windows-first.

Acceptance criteria

  • AC300.1 an option element containing '|' cannot fabricate extra downstream options — structured pass-through makes it impossible (MCP-handler test + engine test assert the count holds).
  • AC300.2 the CLI --options "a|b|c" form still parses into exactly [a, b, c] (regression test).
  • AC300.3 caller-supplied reason/context/options are escaped in the decision comment; injected Markdown/HTML-comment structure is neutralized (test).

Verification

  • pnpm verify green locally: 547 passed (50 files).
  • New tests in tests/escalate.test.mjs (AC300.1/.2/.3 + escapeMd unit) and updated tests/mcp-forge/forge-core.test.mjs (structured pass-through + AC300.1 at the MCP surface).

🤖 Generated with Claude Code

…kdown (#300)

board_escalate pipe-joined its options array and escalate.mjs re-split on
'|', so an option element with an embedded '|' reconstituted into extra
options downstream — defeating the maxItems:20 bound and injecting fabricated
options into the human-facing decision comment.

- Pass options as a structured array end-to-end. The MCP handler hands
  args.options straight to runEscalate; normalizeOptions only splits the
  string form, preserving the CLI --options "a|b|c" contract. An array
  element's embedded '|' now stays literal and cannot fabricate options.
- Escape caller-supplied reason/context/options with escapeMd before they
  land in the trusted Markdown decision comment, so they cannot forge block
  structure, a fake "recommended" tag, or a <!-- forge:... --> marker.

AC300.1 embedded-'|' element cannot inflate option count (MCP + engine tests).
AC300.2 CLI --options "a|b|c" still parses to [a,b,c] (regression).
AC300.3 injected Markdown/HTML-comment structure is neutralized in the comment.

Closes #300

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SATRHKa6mDHDuirhP6QuwL
…ollow-up)

escapeMd escaped '-' (blocking a forged Setext H2) but not '=', so an
embedded-newline payload like "text\n===" still forged a Setext H1 heading in
the human-facing decision comment. Add '=' to the escaped class so neither
setext underline survives. From the branch security pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SATRHKa6mDHDuirhP6QuwL
@dngioidev
dngioidev merged commit 8b42266 into main Jul 26, 2026
6 checks passed
@dngioidev dngioidev mentioned this pull request Jul 26, 2026
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.

board_escalate.options '|'-delimiter bypasses maxItems + unescaped markdown in decision comment

1 participant