Test / Tests (Python 3.12) (push) Canceled after 0s
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).
87 lines
3.5 KiB
Python
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 |