fix(reports): safe-fence markdown PoC export and share fence helpers

This commit is contained in:
Ahmed Allam
2026-07-25 13:09:57 -07:00
committed by Ahmed Allam
parent 97ed7e79a1
commit 21243486e2
10 changed files with 55 additions and 36 deletions
+7 -5
View File
@@ -39,13 +39,14 @@ from strix.interface.tui.live_view import TuiLiveView
from strix.interface.tui.messages import send_user_message_to_agent
from strix.interface.tui.renderers import render_tool_widget
from strix.interface.tui.renderers.agent_message_renderer import AgentMessageRenderer
from strix.interface.tui.renderers.fenced import (
from strix.interface.tui.renderers.user_message_renderer import UserMessageRenderer
from strix.interface.utils import build_tui_stats_text
from strix.report.fenced import (
guess_language_name,
parse_fenced_code,
resolve_lexer,
safe_fence,
)
from strix.interface.tui.renderers.user_message_renderer import UserMessageRenderer
from strix.interface.utils import build_tui_stats_text
from strix.report.state import ReportState, set_global_report_state
from strix.runtime import session_manager
@@ -608,9 +609,10 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
if vuln.get("poc_script_code"):
poc_language, poc_code = parse_fenced_code(vuln["poc_script_code"])
fence_lang = poc_language or guess_language_name(poc_code)
lines.append(f"```{fence_lang}")
fence = safe_fence(poc_code)
lines.append(f"{fence}{fence_lang}")
lines.append(poc_code)
lines.append("```")
lines.append(fence)
if vuln.get("code_locations"):
lines.extend(["", "## Code Analysis", ""])
@@ -5,8 +5,9 @@ from pygments.styles import get_style_by_name
from rich.text import Text
from textual.widgets import Static
from strix.report.fenced import parse_fenced_code, resolve_lexer
from .base_renderer import BaseToolRenderer
from .fenced import parse_fenced_code, resolve_lexer
from .registry import register_tool_renderer
@@ -6,7 +6,21 @@ from pygments.lexers.special import TextLexer
from pygments.util import ClassNotFound
_FENCE_RE = re.compile(r"^```([^\n`]*)\n(.*?)\n?```$", re.DOTALL)
_FENCE_RE = re.compile(r"^```([^\n`]*)\r?\n(.*?)\r?\n?```$", re.DOTALL)
_BACKTICK_RUN = re.compile(r"`+")
def safe_fence(content: str) -> str:
"""Return a backtick fence that ``content`` cannot break out of.
Per CommonMark a fenced code block is closed only by a run of backticks at
least as long as the opening fence. LLM-authored, attacker-influenced values
(PoC scripts, code snippets) may contain their own ``` runs, so we open with
a fence one backtick longer than the longest run inside ``content`` (never
fewer than three). Everything in ``content`` then renders verbatim.
"""
longest = max((len(m.group()) for m in _BACKTICK_RUN.finditer(content)), default=0)
return "`" * max(3, longest + 1)
def parse_fenced_code(raw: str) -> tuple[str | None, str]:
+6 -20
View File
@@ -6,34 +6,19 @@ import csv
import io
import json
import logging
import re
import tempfile
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from strix.core.paths import run_record_path
from strix.report.fenced import guess_language_name, parse_fenced_code, safe_fence
logger = logging.getLogger(__name__)
_SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
_BACKTICK_RUN = re.compile(r"`+")
def _safe_fence(content: str) -> str:
"""Return a backtick fence that ``content`` cannot break out of.
Per CommonMark a fenced code block is closed only by a run of backticks at
least as long as the opening fence. LLM-authored, attacker-influenced values
(PoC scripts, code snippets) may contain their own ``` runs, so we open with
a fence one backtick longer than the longest run inside ``content`` (never
fewer than three). Everything in ``content`` then renders verbatim.
"""
longest = max((len(m.group()) for m in _BACKTICK_RUN.finditer(content)), default=0)
return "`" * max(3, longest + 1)
def read_run_record(run_dir: Path) -> dict[str, Any]:
path = run_record_path(run_dir)
@@ -187,9 +172,10 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
lines.append(str(report["poc_description"]))
lines.append("")
if report.get("poc_script_code"):
code = str(report["poc_script_code"])
fence = _safe_fence(code)
lines.append(fence)
language, code = parse_fenced_code(str(report["poc_script_code"]))
fence_lang = language or guess_language_name(code)
fence = safe_fence(code)
lines.append(f"{fence}{fence_lang}")
lines.append(code)
lines.append(fence)
lines.append("")
@@ -209,7 +195,7 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
lines.append(f" {loc['label']}")
if loc.get("snippet"):
snippet = str(loc["snippet"])
fence = _safe_fence(snippet)
fence = safe_fence(snippet)
lines.append(f" {fence}")
lines.extend(f" {ln}" for ln in snippet.splitlines())
lines.append(f" {fence}")
@@ -17,7 +17,7 @@ export function MdCodeBlock({
}) {
const [copied, setCopied] = useState(false);
const raw = String(children).replace(/\n$/, "");
const match = /language-(\w+)/.exec(className || "");
const match = /language-(\S+)/.exec(className || "");
const isBlock = raw.includes("\n") || match;
if (!isBlock) {
+1 -1
View File
@@ -3,7 +3,7 @@ export interface ParsedFencedCode {
code: string;
}
const FENCE_RE = /^```([^\n`]*)\n([\s\S]*?)\n?```$/;
const FENCE_RE = /^```([^\n`]*)\r?\n([\s\S]*?)\r?\n?```$/;
/**
* Agent-generated `poc_script_code` is stored wrapped in a markdown code fence
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<title>Strix Results</title>
<script type="module" crossorigin src="./assets/index-B1jJGKIO.js"></script>
<script type="module" crossorigin src="./assets/index-Dd1cyttN.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-vV8wxCG6.css">
</head>
<body>
+15 -1
View File
@@ -4,10 +4,11 @@ from __future__ import annotations
from pygments.lexers import BashLexer, PythonLexer
from strix.interface.tui.renderers.fenced import (
from strix.report.fenced import (
guess_language_name,
parse_fenced_code,
resolve_lexer,
safe_fence,
)
from strix.viewer.report_pdf import _strip_code_fence
@@ -62,3 +63,16 @@ def test_resolve_lexer_falls_back_to_python_when_unresolvable() -> None:
def test_guess_language_name_defaults_to_python_when_inconclusive() -> None:
assert guess_language_name("") == "python"
def test_parse_fenced_code_handles_crlf() -> None:
language, code = parse_fenced_code("```python\r\nx = 1\r\n```")
assert language == "python"
assert code == "x = 1"
def test_safe_fence_widens_past_embedded_backticks() -> None:
# A PoC body containing a ``` run must be wrapped in a longer fence so it
# can't terminate the block early.
assert safe_fence("plain code") == "```"
assert safe_fence("has ```\nfence inside") == "````"
+5 -3
View File
@@ -119,9 +119,11 @@ def test_render_vulnerability_md_poc_code_cannot_break_out_of_fence() -> None:
injected = "curl x\n```\n\n## Injected Heading\n![x](https://evil.example/beacon.png)"
md = render_vulnerability_md(_sample_report(poc_script_code=injected))
lines = md.split("\n")
fence = next(ln for ln in lines[lines.index("## Proof of Concept") + 1 :] if ln.strip())
assert set(fence) == {"`"}
assert len(fence) >= 4 # wider than the payload's 3-backtick run
opening = next(ln for ln in lines[lines.index("## Proof of Concept") + 1 :] if ln.strip())
ticks = opening[: len(opening) - len(opening.lstrip("`"))]
assert len(ticks) >= 4 # wider than the payload's 3-backtick run
assert "`" not in opening.removeprefix(ticks) # backtick run + language tag only
assert f"\n{ticks}\n" in md # pure-backtick closing fence of the same width
assert injected in md # the payload survives verbatim, inside the fence