Skip to content

Harden report output against stored-XSS via markdown#195

Open
seonghobae wants to merge 1 commit into
developfrom
report-output-hardening
Open

Harden report output against stored-XSS via markdown#195
seonghobae wants to merge 1 commit into
developfrom
report-output-hardening

Conversation

@seonghobae

Copy link
Copy Markdown
Contributor

Summary

A security tool shouldn't emit an injectable artifact. Report renderers interpolate finding fields into markdown; findings from external engines (Trivy/Bandit/Semgrep) can carry attacker-influenced text — a scanned file's <script> landing in a message. When that markdown report is later rendered as HTML (GitHub, a viewer, a PDF pipeline), it's stored XSS.

  • _sanitize_for_markdown() returns a render-safe copy: prose fields (message/remediation/verification) HTML-escaped, and snippet neutralized so it can't break out of its ``` code fence. Applied at every report normalization site, so all report types (buyer-diligence, founder-friendly, agency, fix-pack) are covered.
  • Constrained identifiers (rule_id/category/context/severity) — which also drive gate logic — are intentionally left untouched.

Found by the repo's own strix scan flagging unsanitized interpolation in report rendering — fixing it at the shared boundary rather than per call site.

Test plan

  • pytest — 208 passed, incl. tests/test_report_sanitization.py (no raw <script>/<iframe>/<b> in any report type, entity escaping, fence-breakout neutralized, benign messages unchanged — no over-escaping)

Follow-up: the compliance report (PR #192) will adopt _sanitize_for_markdown once both land.

🤖 Generated with Claude Code

A security tool should not emit an injectable report. Findings from external
engines can carry attacker-influenced text (a scanned file's `<script>` in a
message); when a generated markdown report is rendered as HTML that becomes
stored XSS.

- appguardrail_core/reports.py: _sanitize_for_markdown() HTML-escapes the prose
  fields (message/remediation/verification) and neutralizes ``` code-fence
  breakout in snippets, applied at every report normalization site (covers all
  report types). Constrained identifiers (rule_id/category/context/severity),
  which also drive gate logic, are left untouched.
- Tests: tests/test_report_sanitization.py (no raw <script>/<iframe>/<b> in any
  report type, entity escaping, fence-breakout neutralized, benign text
  unchanged / no over-escaping).
- Verified: full suite 208 passed (benign existing messages unaffected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EywwS2Du8pimW7xqRP3An3
@seonghobae seonghobae force-pushed the report-output-hardening branch from 9358b6b to e993d83 Compare July 5, 2026 21:26
@seonghobae seonghobae enabled auto-merge (squash) July 5, 2026 22:26
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.

1 participant