mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 09:26:39 +02:00
fix(report): prevent code-fence breakout in vulnerability markdown (#817)
* fix(report): prevent code-fence breakout in vulnerability markdown render_vulnerability_md wrapped LLM-authored poc_script_code and code snippet values in a fixed three-backtick fence, so a triple-backtick inside the value closed the fence early and the rest rendered as live markdown (headings, tracking-beacon images) in the shareable report deliverable. Open each such block with a fence one backtick longer than the longest backtick run in the payload (CommonMark: a block closes only on a fence at least as long as the opener), so the content always renders verbatim. The adjacent ```diff block is already safe (its lines are '- '/'+ ' prefixed and so can never be a bare-backtick closing fence) and is left unchanged. Fixes #815 * fix(report): indent multiline snippets --------- Co-authored-by: thejesh23 <thejesh23@users.noreply.github.com> Co-authored-by: Alex Schapiro <bearsyankees@gmail.com>
This commit is contained in:
co-authored by
thejesh23
Alex Schapiro
parent
f9890a672d
commit
f967e6017b
+26
-4
@@ -6,6 +6,7 @@ import csv
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import tempfile
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
@@ -18,6 +19,21 @@ 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)
|
||||
@@ -171,9 +187,11 @@ 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"):
|
||||
lines.append("```")
|
||||
lines.append(str(report["poc_script_code"]))
|
||||
lines.append("```")
|
||||
code = str(report["poc_script_code"])
|
||||
fence = _safe_fence(code)
|
||||
lines.append(fence)
|
||||
lines.append(code)
|
||||
lines.append(fence)
|
||||
lines.append("")
|
||||
|
||||
if report.get("code_locations"):
|
||||
@@ -190,7 +208,11 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
|
||||
if loc.get("label"):
|
||||
lines.append(f" {loc['label']}")
|
||||
if loc.get("snippet"):
|
||||
lines.append(f" ```\n {loc['snippet']}\n ```")
|
||||
snippet = str(loc["snippet"])
|
||||
fence = _safe_fence(snippet)
|
||||
lines.append(f" {fence}")
|
||||
lines.extend(f" {ln}" for ln in snippet.splitlines())
|
||||
lines.append(f" {fence}")
|
||||
if loc.get("fix_before") or loc.get("fix_after"):
|
||||
lines.append("\n **Suggested Fix:**")
|
||||
lines.append("```diff")
|
||||
|
||||
@@ -113,6 +113,28 @@ def test_render_vulnerability_md_includes_dependency_fields() -> None:
|
||||
assert "## Assumptions" in md
|
||||
|
||||
|
||||
def test_render_vulnerability_md_poc_code_cannot_break_out_of_fence() -> None:
|
||||
# LLM/target-authored PoC content containing its own ``` must not close the
|
||||
# fence early and turn the injected markdown into live headings/images.
|
||||
injected = "curl x\n```\n\n## Injected Heading\n"
|
||||
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
|
||||
assert injected in md # the payload survives verbatim, inside the fence
|
||||
|
||||
|
||||
def test_render_vulnerability_md_snippet_cannot_break_out_of_fence() -> None:
|
||||
snippet = "row = q()\n```\n## Injected"
|
||||
md = render_vulnerability_md(
|
||||
_sample_report(code_locations=[{"file": "app.py", "snippet": snippet}]),
|
||||
)
|
||||
assert (
|
||||
" ````\n row = q()\n ```\n ## Injected\n ````"
|
||||
) in md # indented fence widened past the payload's ``` run
|
||||
|
||||
|
||||
def test_write_vulnerabilities_creates_markdown_csv_and_json(tmp_path: Path) -> None:
|
||||
reports = [
|
||||
_sample_report(id="vuln-0001", severity="medium", timestamp="2026-07-02 11:00:00 UTC"),
|
||||
|
||||
Reference in New Issue
Block a user