security: sanitize rendered research-report HTML#364
Conversation
lalalune
left a comment
There was a problem hiding this comment.
✅ Tested & LGTM
Re-verified on a clean checkout of pull/364/head with real dependencies (SQLAlchemy / FastAPI / Pydantic, Python 3.12): pytest tests/test_security_regressions.py → 34 passed, 1 warning in 0.66s.
What it does: Research-report markdown is built from LLM output over crawled pages (untrusted) and report pages run under script-src 'self' 'unsafe-inline' (confirmed core/middleware.py:69); python-markdown passes raw HTML through verbatim -> genuine stored-XSS path. Fix runs nh3.clean() with an allowlist built from nh3 safe defaults plus exactly the report's own tags/attrs (details/summary, heading ids, codehilite classes, table align, link rel/target, img). Also html.escape()s the request-supplied category that lands in (separate attribute-injection XSS). nh3 API usage verified correct. nh3 ships prebuilt wheels so it doesn't compromise install reliability.
On-mission (security & privacy).
lalalune
left a comment
There was a problem hiding this comment.
✅ Tested & LGTM
Re-verified on a clean checkout of pull/364/head with real dependencies (SQLAlchemy / FastAPI / Pydantic, Python 3.12): pytest tests/test_security_regressions.py → 34 passed, 1 warning in 0.66s.
What it does: Research-report markdown is built from LLM output over crawled pages (untrusted) and report pages run under script-src 'self' 'unsafe-inline' (confirmed core/middleware.py:69); python-markdown passes raw HTML through verbatim -> genuine stored-XSS path. Fix runs nh3.clean() with an allowlist built from nh3 safe defaults plus exactly the report's own tags/attrs (details/summary, heading ids, codehilite classes, table align, link rel/target, img). Also html.escape()s the request-supplied category that lands in (separate attribute-injection XSS). nh3 API usage verified correct. nh3 ships prebuilt wheels so it doesn't compromise install reliability.
On-mission (security & privacy).
|
Thanks for the PR. This currently needs a rebase / conflict resolution before I can review it properly. Could you please rebase onto the current Sorry for the extra delay — we are still adjusting to the large influx of contributions and trying to keep reviews careful and fair. |
The visual research report is assembled from LLM output over crawled web
pages (untrusted content) and served under a relaxed `script-src
'unsafe-inline'` CSP. Two values reached that HTML without sanitization:
- `_md_to_html` rendered the report markdown via python-markdown, which
passes raw HTML through verbatim, so `<script>` / `<img onerror>` /
`<svg onload>` / `javascript:` links carried in crawled content ran in
the app origin.
- `category` (from the /api/research/start request body, no enum check) was
interpolated raw into `<body class="category-{category}">`.
Allowlist-sanitize the rendered markdown with nh3, keeping the formatting
the report emits (tables, code, details/summary, toc anchors, codehilite
classes, external-link target/rel) while dropping active content, and
html.escape the category. Adds regression tests.
2a77c75 to
144715d
Compare
|
rebased onto current main and resolved the conflicts. heads up, the report XSS is still live on main: ready when you are. |
alteixeira20
left a comment
There was a problem hiding this comment.
LGFM. I checked the PR patch and focused security validation.
The PR adds nh3 to requirements.txt; my local venv did not have that new dependency yet, so the first test run failed with ModuleNotFoundError: No module named 'nh3'. After installing the dependency locally, the focused validation passed:
python3 -m pytest tests/test_security_regressions.py -q
# 81 passed, 1 warning
python3 -m py_compile src/visual_report.py tests/test_security_regressions.py
git diff --check upstream/main...HEADI also ran a direct malicious-input probe against the actual report rendering helpers. It confirmed that:
<script>is stripped- inline event handlers such as
onerror/onloadare stripped javascript:links are stripped- normal report formatting is preserved, including headings, tables, fenced code blocks, details/summary, and external links
- request
categoryis escaped before reaching the body class
Scope checked:
requirements.txt
src/visual_report.py
tests/test_security_regressions.py
This is a focused fix for untrusted research-report HTML rendering and category attribute injection.
|
appreciate the merge. you mentioned the influx of contributions, so i figured i'd offer. i do security and bug-bounty work for a living, so if it helps while you're buried, happy to spot-check incoming security PRs and flag the real ones (and the ones quietly making things worse). no strings, just an extra set of eyes that reads diffs all day. either way, glad this one landed. |
|
@StressTestor Really appreciate that, thank you. That kind of security review help would be highly valuable, especially right now with the current influx of PRs. I’m finishing up some CI-related fixes first so the test signal is cleaner and PR audits are easier to trust, but once that is stable, an extra experienced set of eyes on security-sensitive diffs would help a lot. |
|
sounds good, no rush. ping me whenever the CI's settled and i'll start sanity-checking the security-sensitive ones. cleaner test signal first is the right call. |
The visual research report is assembled from LLM output over crawled web
pages (untrusted content) and served under a relaxed `script-src
'unsafe-inline'` CSP. Two values reached that HTML without sanitization:
- `_md_to_html` rendered the report markdown via python-markdown, which
passes raw HTML through verbatim, so `<script>` / `<img onerror>` /
`<svg onload>` / `javascript:` links carried in crawled content ran in
the app origin.
- `category` (from the /api/research/start request body, no enum check) was
interpolated raw into `<body class="category-{category}">`.
Allowlist-sanitize the rendered markdown with nh3, keeping the formatting
the report emits (tables, code, details/summary, toc anchors, codehilite
classes, external-link target/rel) while dropping active content, and
html.escape the category. Adds regression tests.
The visual research report is assembled from LLM output over crawled web
pages (untrusted content) and served under a relaxed `script-src
'unsafe-inline'` CSP. Two values reached that HTML without sanitization:
- `_md_to_html` rendered the report markdown via python-markdown, which
passes raw HTML through verbatim, so `<script>` / `<img onerror>` /
`<svg onload>` / `javascript:` links carried in crawled content ran in
the app origin.
- `category` (from the /api/research/start request body, no enum check) was
interpolated raw into `<body class="category-{category}">`.
Allowlist-sanitize the rendered markdown with nh3, keeping the formatting
the report emits (tables, code, details/summary, toc anchors, codehilite
classes, external-link target/rel) while dropping active content, and
html.escape the category. Adds regression tests.
The visual research report is assembled from LLM output over crawled web
pages (untrusted content) and served under a relaxed `script-src
'unsafe-inline'` CSP. Two values reached that HTML without sanitization:
- `_md_to_html` rendered the report markdown via python-markdown, which
passes raw HTML through verbatim, so `<script>` / `<img onerror>` /
`<svg onload>` / `javascript:` links carried in crawled content ran in
the app origin.
- `category` (from the /api/research/start request body, no enum check) was
interpolated raw into `<body class="category-{category}">`.
Allowlist-sanitize the rendered markdown with nh3, keeping the formatting
the report emits (tables, code, details/summary, toc anchors, codehilite
classes, external-link target/rel) while dropping active content, and
html.escape the category. Adds regression tests.
The visual research report is assembled from LLM output over crawled web
pages (untrusted content) and served under a relaxed `script-src
'unsafe-inline'` CSP. Two values reached that HTML without sanitization:
- `_md_to_html` rendered the report markdown via python-markdown, which
passes raw HTML through verbatim, so `<script>` / `<img onerror>` /
`<svg onload>` / `javascript:` links carried in crawled content ran in
the app origin.
- `category` (from the /api/research/start request body, no enum check) was
interpolated raw into `<body class="category-{category}">`.
Allowlist-sanitize the rendered markdown with nh3, keeping the formatting
the report emits (tables, code, details/summary, toc anchors, codehilite
classes, external-link target/rel) while dropping active content, and
html.escape the category. Adds regression tests.
Summary
The visual research report (
GET /api/research/report/{id}) is served under a deliberately relaxed CSP (script-src 'self' 'unsafe-inline',core/middleware.py) because the report template uses inline scripts. Its content is built from LLM output over crawled web pages (untrusted), and two values reached that HTML unsanitized:src/visual_report.py::_md_to_htmlrenders the report markdown withmarkdown.markdown(...), which passes raw HTML through verbatim, so<script>,<img onerror=…>,<svg onload=…>, andjavascript:links carried in crawled content execute in the app origin when a user opens their report.categoryarrives from the/api/research/startrequest body with no enum check and is interpolated raw into<body class="category-{category}">, an attribute-injection sink independent of the markdown body.This allowlist-sanitizes the rendered markdown with
nh3(the maintained ammonia binding), keeping everything the report emits (tables, fenced code,<details>/<summary>, heading/toc anchors, codehilite classes, external-linktarget/rel) while dropping active content, andhtml.escapes the category. I re-confirmed both are still live on currentmainbefore rebasing.Linked Issue
Part of #169
Type of Change
Checklist
maindocker compose uporuvicorn app:app) and verified the change works end-to-end. (Verified via the security andvisual_reporttest suites below, not a full app run. Happy to do one if you want it.)How to Test
The XSS is still live on
main. Before the fix:After:
The new cases assert
<script>/onerror/onload/javascript:are stripped, that normal report formatting (heading + toc anchor, tables, fenced code,<details>, external links withrel) survives, and that a malicious or non-stringcategorycannot break out of the class attribute. They fail onmainand pass with this change; the three existingtests/test_visual_report*.pysuites still pass. (The other ~29 failures intest_security_regressions.pyare missing-dependencyModuleNotFoundErrors, identical on a cleanmaincheckout in my environment.)Visual / UI changes
None. This is security-only and preserves all legitimate report formatting (locked by
test_md_to_html_preserves_normal_report_formatting). No rendering or style change to screenshot.Rebased onto current
main, which had drifted ~689 commits since the branch was opened.