Files
epub2pdf/tests/test_review_findings_20260903.py
T
sirdave73 51d39fead1
Test / Tests (Python 3.12) (push) Canceled after 0s
C2 SSRF fix: pre-validate HTML resource refs before WeasyPrint render
WeasyPrint's image loader swallows per-fetch errors and renders a broken
image, so a job could report DONE while a remote URL was contacted
(SSRF/data egress against the no-data-egress guarantee). Add
scan_for_remote() which rejects http(s)/ftp/ws/wss/file resource
references up-front (ValueError -> job error) before WeasyPrint ever
runs; local data: URIs still render via the default loader. Also add
tests/test_review_findings_20260903.py (remote -> job error, valid
data: -> job done).
2026-09-04 00:18:17 -04:00

87 lines
3.5 KiB
Python

"""Regression tests for the 2026-09-03 adversarial-review findings.
Source of truth:
.a0proj/_bmad-output/implementation-artifacts/review-findings-epub3-pdf-2026-09-03.md
C2 - Remote resource fetching by WeasyPrint (blind SSRF / server-side data egress).
The server must never fetch a remote http(s)/ftp/ws/wss resource on behalf of a
user upload. The fix lives in converters.common._blocking_url_fetcher
(common.py:130-150), which refuses remote schemes and delegates local `data:` /
same-directory references to WeasyPrint's default loader. The import that makes
that delegation possible is converters.common:18.
Each test maps to C2 and is asserted through the public HTTP API so a regression
in the fetcher policy is caught end-to-end (not just at the unit boundary).
Satisfies: C2
"""
import base64
import io
import app
from tests.helpers import wait_for_job
def _upload(client, content, filename="doc.html"):
"""POST an upload, wait for the terminal job state, return the snapshot."""
files = {"file": (io.BytesIO(content.encode("utf-8")), filename)}
resp = client.post("/api/convert", data=files, content_type="multipart/form-data")
assert resp.status_code == 202, resp.get_json()
return wait_for_job(client, resp.get_json()["job_id"])
# ---------------------------------------------------------------------------
# Finding C2: WeasyPrint SSRF guard - remote resource references are refused
# ---------------------------------------------------------------------------
def test_remote_http_image_reference_rejected(client):
"""A user-supplied .html referencing http:// image must FAIL the job rather
than let the server contact a remote host (SSRF / data egress).
Satisfies: C2
"""
html = (
"<!DOCTYPE html><html><head><title>Remote Img</title></head>"
'<body><img src="http://example.com/pic.png" alt="remote">'
"</body></html>"
)
job = _upload(client, html)
assert job["status"] == "error", job
assert "blocked" in job["error"].lower() or "remote" in job["error"].lower()
def test_remote_https_image_reference_rejected(client):
"""Same guard for https:// - the common metadata / SSRF vector.
Satisfies: C2
"""
html = (
"<!DOCTYPE html><html><body>"
'<img src="https://evil.example.com/x.png">'
"</body></html>"
)
job = _upload(client, html)
assert job["status"] == "error", job
assert "blocked" in job["error"].lower() or "remote" in job["error"].lower()
# ---------------------------------------------------------------------------
# Finding C2 (regression): local data: references remain permitted
# ---------------------------------------------------------------------------
def test_local_data_uri_image_succeeds(client):
"""A local `data:` URI resolves through WeasyPrint's default loader and the
conversion completes. Regression guard: must not regress to returning None
(which raised WeasyPrint's 'URL fetcher must return ...' assertion).
Satisfies: C2
"""
# 1x1 red PNG encoded as a data: URI (PIL-generated, valid base64).
png = base64.b64decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"
)
data_uri = "data:image/png;base64," + base64.b64encode(png).decode("ascii")
html = (
"<!DOCTYPE html><html><head><title>Local Img</title></head>"
'<body><img src="' + data_uri + '" alt="local">'
"</body></html>"
)
job = _upload(client, html)
assert job["status"] == "done", job