diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 27b137dc4e..a56c3cadc0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -40,10 +40,13 @@ jobs:
run: pip install --require-hashes -r requirements-dev.lock.txt
- name: Run ruff check
- run: ruff check backend/
+ run: ruff check backend/ installers/windows/build.py installers/windows/tests/
- name: Run ruff format check
- run: ruff format --check backend/
+ run: ruff format --check backend/ installers/windows/build.py installers/windows/tests/
+
+ - name: Test Windows PDF runtime packaging
+ run: python -m pytest installers/windows/tests/ --no-cov -q
- name: Run source size budget
run: python tools/check_source_size_budget.py
@@ -309,7 +312,7 @@ jobs:
working-directory: frontend
# V8 coverage instrumentation makes the largest jsdom integration
# tests slower on shared runners; keep the normal 10s limit unchanged.
- run: npm run test:coverage -- --testTimeout=20000 && npm run check:i18n
+ run: npm run test:coverage -- --testTimeout=20000 && npm run check:i18n && npm run check:mocker-security
frontend-build:
name: Frontend Build
diff --git a/backend/app/resources/pdf/runtime-manifest.json b/backend/app/resources/pdf/runtime-manifest.json
index 12ea16710a..8ad47c1d9a 100644
--- a/backend/app/resources/pdf/runtime-manifest.json
+++ b/backend/app/resources/pdf/runtime-manifest.json
@@ -1,7 +1,7 @@
{
"schema_version": 1,
"renderer": {
- "weasyprint": "69.0",
+ "weasyprint": "70.0",
"pikepdf": "10.10.0",
"fonttools": "4.63.0",
"jinja2": "3.1.6"
diff --git a/backend/app/services/document_layout_catalog.py b/backend/app/services/document_layout_catalog.py
index d8fdb201b9..1ac2b45aef 100644
--- a/backend/app/services/document_layout_catalog.py
+++ b/backend/app/services/document_layout_catalog.py
@@ -6,7 +6,7 @@
from backend.app.services.document_catalog import DocumentType
-RENDERER_VERSION = "weasyprint-69.0+pikepdf-10.10.0"
+RENDERER_VERSION = "weasyprint-70.0+pikepdf-10.10.0"
VALIDATOR_VERSION = "verapdf-1.30.2"
SUPPORTED_LANGUAGES = ("de", "en")
SUPPORTED_DOCUMENT_TYPES = tuple(document_type.value for document_type in DocumentType)
diff --git a/backend/app/services/pdfa.py b/backend/app/services/pdfa.py
index 4d1897e724..fe280ff232 100644
--- a/backend/app/services/pdfa.py
+++ b/backend/app/services/pdfa.py
@@ -20,7 +20,7 @@
_ICC_PACKAGE = "backend.app.resources.pdf"
_ICC_FILENAME = "sRGB.icc"
-_PRODUCER = "PrintOps document renderer / WeasyPrint 69.0 / pikepdf 10.10.0"
+_PRODUCER = "PrintOps document renderer / WeasyPrint 70.0 / pikepdf 10.10.0"
_BOX_TOLERANCE_PT = 0.1
_RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
_PDFA_EXTENSION_NS = "http://www.aiim.org/pdfa/ns/extension/"
diff --git a/backend/tests/fixtures/document_layouts/effective-classic-a4.json b/backend/tests/fixtures/document_layouts/effective-classic-a4.json
index 114a22c0a3..9c43201867 100644
--- a/backend/tests/fixtures/document_layouts/effective-classic-a4.json
+++ b/backend/tests/fixtures/document_layouts/effective-classic-a4.json
@@ -1,7 +1,7 @@
{
"schema_version": 1,
"template_version": "1.0.0",
- "renderer_version": "weasyprint-69.0+pikepdf-10.10.0",
+ "renderer_version": "weasyprint-70.0+pikepdf-10.10.0",
"validator_version": "verapdf-1.30.2",
"page": {
"template_key": "classic",
diff --git a/backend/tests/fixtures/document_layouts/effective-modern-letter.json b/backend/tests/fixtures/document_layouts/effective-modern-letter.json
index 7bd7fc2529..73a962a5f3 100644
--- a/backend/tests/fixtures/document_layouts/effective-modern-letter.json
+++ b/backend/tests/fixtures/document_layouts/effective-modern-letter.json
@@ -1,7 +1,7 @@
{
"schema_version": 1,
"template_version": "1.0.0",
- "renderer_version": "weasyprint-69.0+pikepdf-10.10.0",
+ "renderer_version": "weasyprint-70.0+pikepdf-10.10.0",
"validator_version": "verapdf-1.30.2",
"page": {
"template_key": "modern",
diff --git a/backend/tests/integration/test_document_pdf_conformance.py b/backend/tests/integration/test_document_pdf_conformance.py
index dffa1a05f3..33d8d153d4 100644
--- a/backend/tests/integration/test_document_pdf_conformance.py
+++ b/backend/tests/integration/test_document_pdf_conformance.py
@@ -209,7 +209,9 @@ def test_ten_page_document_renders_within_release_budget(tmp_path):
if not WEASYPRINT.exists():
pytest.skip("pinned WeasyPrint runtime is not staged")
sample = load_sample("invoice-de-standard")
- expanded = sample.model_copy(update={"lines": sample.lines * 14})
+ # Keep a >=10-page workload with v70's denser pagination; do not relax
+ # either the minimum page count or the ten-second release budget.
+ expanded = sample.model_copy(update={"lines": sample.lines * 16})
renderer = DocumentRenderer(
engine_cli=WEASYPRINT,
cache_dir=tmp_path / "cache",
diff --git a/backend/tests/integration/test_document_render_determinism.py b/backend/tests/integration/test_document_render_determinism.py
index 3130baaf90..77844c963a 100644
--- a/backend/tests/integration/test_document_render_determinism.py
+++ b/backend/tests/integration/test_document_render_determinism.py
@@ -86,7 +86,9 @@ def test_ten_page_document_and_long_position_render_within_hard_limit(tmp_path):
"description": f"Position {number}: {base.description}",
}
)
- for number in range(1, 81)
+ # v70's Pango/layout stack fits 80 positions on nine pages. Ninety
+ # positions retain this test's ten-page workload (not a golden layout).
+ for number in range(1, 91)
)
request = _request("compact")
request = RenderInput(
diff --git a/backend/tests/integration/test_weasyprint_fetcher_security.py b/backend/tests/integration/test_weasyprint_fetcher_security.py
new file mode 100644
index 0000000000..02342eddfc
--- /dev/null
+++ b/backend/tests/integration/test_weasyprint_fetcher_security.py
@@ -0,0 +1,69 @@
+"""CVE-2026-55073: auxiliary resources must retain the original fetch policy.
+
+Only test-owned temporary files are used; no external service is contacted.
+Explicit trusted filename/file-object inputs are not URL policy inputs.
+"""
+
+from __future__ import annotations
+
+import pytest
+
+
+@pytest.fixture
+def resources(tmp_path):
+ from weasyprint import HTML
+ from weasyprint.urls import FatalURLFetchingError, URLFetcher, URLFetcherResponse
+
+ blocked = tmp_path / "blocked.css"
+ blocked.write_text("@page { size: 1234px 5678px }", encoding="utf-8")
+ outer = tmp_path / "outer.css"
+ outer.write_text(f'@import url("{blocked.as_uri()}");', encoding="utf-8")
+ metadata = tmp_path / "metadata.xmp"
+ marker = b"printops-owned-metadata-canary"
+ metadata.write_bytes(
+ b'
Control document
", url_fetcher=policy) + with pytest.raises(Denied, match="original fetcher"): + if channel == "xmp": + html.write_pdf(xmp_metadata=[metadata.as_uri()], pdf_variant="pdf/a-3b") + elif channel in {"stylesheet", "import"}: + html.render(stylesheets=[(outer if channel == "import" else blocked).as_uri()]) + else: + html.render() + assert (metadata if channel == "xmp" else blocked).as_uri() in policy.calls + + +def test_original_fetcher_still_allows_approved_stylesheets_and_metadata(resources): + HTML, Policy, _, blocked, _, metadata, marker = resources + policy = Policy([blocked.as_uri(), metadata.as_uri()]) + html = HTML(string="Allowed document
", url_fetcher=policy) + document = html.render(stylesheets=[blocked.as_uri()]) + assert (document.pages[0].width, document.pages[0].height) == (1234, 5678) + pdf = document.write_pdf(xmp_metadata=[metadata.as_uri()], pdf_variant="pdf/a-3b", uncompressed_pdf=True) + assert marker in pdf + assert policy.calls == [blocked.as_uri(), metadata.as_uri()] diff --git a/backend/tests/unit/services/test_document_layout_catalog.py b/backend/tests/unit/services/test_document_layout_catalog.py index 6e24feeb96..06fd6d75d4 100644 --- a/backend/tests/unit/services/test_document_layout_catalog.py +++ b/backend/tests/unit/services/test_document_layout_catalog.py @@ -39,7 +39,7 @@ def test_catalog_is_closed_complete_and_versioned(): assert set(PAGE_FORMATS_MM) == {"A4", "Letter"} assert SUPPORTED_LANGUAGES == ("de", "en") assert set(SUPPORTED_DOCUMENT_TYPES) == {document_type.value for document_type in DocumentType} - assert RENDERER_VERSION == "weasyprint-69.0+pikepdf-10.10.0" + assert RENDERER_VERSION == "weasyprint-70.0+pikepdf-10.10.0" assert VALIDATOR_VERSION == "verapdf-1.30.2" assert LAYOUT_SECTION_KEYS == ( "page", diff --git a/backend/tests/unit/services/test_pdf_runtime.py b/backend/tests/unit/services/test_pdf_runtime.py index 79fcaa79be..8850f84ac6 100644 --- a/backend/tests/unit/services/test_pdf_runtime.py +++ b/backend/tests/unit/services/test_pdf_runtime.py @@ -12,7 +12,7 @@ def test_python_pdf_packages_are_exactly_pinned() -> None: - assert version("weasyprint") == "69.0" + assert version("weasyprint") == "70.0" assert version("pikepdf") == "10.10.0" assert version("fonttools") == "4.63.0" @@ -36,3 +36,14 @@ def test_srgb_output_intent_matches_manifest_receipt() -> None: assert len(content) >= 3_000 assert hashlib.sha256(content).hexdigest() == manifest["srgb"]["sha256"] assert manifest["srgb"]["color_space"] == "RGB" + + +def test_renderer_manifest_and_receipt_track_installed_runtime() -> None: + from backend.app.services.document_layout_catalog import RENDERER_VERSION + + manifest = json.loads((RESOURCE_DIR / "runtime-manifest.json").read_text(encoding="utf-8")) + for package, pinned in manifest["renderer"].items(): + assert version(package) == pinned + assert f"weasyprint-{version('weasyprint')}+pikepdf-{version('pikepdf')}" == RENDERER_VERSION + key = RESOURCE_DIR / manifest["verapdf"]["signing_key"] + assert hashlib.sha256(key.read_bytes()).hexdigest() == manifest["verapdf"]["signing_key_sha256"] diff --git a/docs/security-alert-triage-2026-09-11.md b/docs/security-alert-triage-2026-09-11.md new file mode 100644 index 0000000000..f80dfd3e2b --- /dev/null +++ b/docs/security-alert-triage-2026-09-11.md @@ -0,0 +1,73 @@ +# Dependabot remediation — 2026-09-11 + +Tracking: [issue #177](https://github.com/ichwars/PrintOps/issues/177). +All five alerts open at the start of this change are treated as actionable +dependency findings, not dismissed on the basis of limited application exposure. + +| Alerts | Dependency / manifests | Vulnerable version | Fixed version | +| --- | --- | --- | --- | +| 21, 22 | `@vitest/mocker`, `vitest`; `frontend/package-lock.json` | 4.1.8 | 4.1.11 | +| 23, 24, 25 | `weasyprint`; `requirements.txt` and both Python lockfiles | 69.0 | 70.0 | + +## Boundary and fix + +[CVE-2026-84373](https://github.com/advisories/GHSA-82fw-gwwq-j7x9) +allows redirect mocks to bypass Vite's filesystem allow/deny policy. The complete +Vitest package family, including coverage, moves together to the patched v4 +release. PrintOps uses jsdom tests, not the optional browser mocker server; this +limits exposure but is not a reason to retain the vulnerable dependency. + +[CVE-2026-55073](https://github.com/advisories/GHSA-jf6q-chmf-3h3v) +allows auxiliary stylesheet and XMP URLs to bypass WeasyPrint's original fetcher. +[WeasyPrint 70.0](https://github.com/Kozea/WeasyPrint/releases/tag/v70.0) +threads that fetcher through both entry points, including nested CSS imports. +PrintOps continues using fixed CLI arguments, registered hash-verified assets, +file-only loading, and disabled HTTP redirects. Untrusted options, filenames, +file objects, or independently constructed CSS objects must not be forwarded to +WeasyPrint; explicit trusted file inputs are not URL-fetcher policy inputs. + +## Runtime compatibility + +- Python requirements and both hash-locked dependency graphs pin 70.0. +- Windows packaging uses the official `weasyprint-windows-onedir.zip`, verified + against SHA-256 `ab1151f210b4e6bb7aa7a79e91a67e8ddb760094c107bfda55241b6aaefe7d53`. + The complete runtime, including `_internal` native libraries, is staged at + the existing `runtime/weasyprint/dist/weasyprint.exe` path. + Portable packaging tests live under `installers/windows/tests/` and run in + backend-lint CI; they do not require installer sources in the Docker image. +- New render receipts, cache fingerprints, and PDF producer metadata identify + WeasyPrint 70.0. Previously issued artifacts and their immutable 69.0 receipts + are not rewritten. The append-only schema tests intentionally retain the + historical version in their fixtures. +- veraPDF, its signatures, ICC profiles, pikepdf, fonts, and validation policy + remain unchanged. A test now verifies the public signing key's canonical hash; + the existing `.gitattributes` LF rule remains its owner. +- The v70 Windows layout stack places the 80-position stress sample on nine + pages. Text extraction confirmed every position remains present. Ninety + positions produce ten pages with every position present, preserving the + existing ten-page workload test. The timed sample likewise grows from 14 to + 16 repetitions without relaxing its page minimum or ten-second limit. + This is not a byte-for-byte cross-version + rendering promise; deterministic output is tested within the pinned runtime. + +## Regression evidence + +`npm run check:mocker-security` exercises the installed interceptor registration +and load hooks against Vite's real filesystem policy. Both a denied in-root +`.env` fixture and an out-of-root opaque-URL traversal were readable under 4.1.8 +and are rejected under 4.1.11; an allowed redirect remains readable. The check +also runs in the required frontend test CI job. + +`test_weasyprint_fetcher_security.py` exercises real rendering with owned local +canaries only: HTML-link control, stylesheet, nested import, XMP metadata, and +approved stylesheet/metadata controls. Under 69.0 the three auxiliary rejection +tests fail and the allow-control shows the configured fetcher was never called. +All five pass under 70.0. No internal service, credential, or unrelated user file +is accessed by these tests. + +Release verification additionally covers PDF/A-3u with the pinned veraPDF CLI, +document types/languages/page formats/templates, hybrid e-invoices, deterministic +output, font/letterhead handling, Windows packaging, frontend tests/build, and +dependency audits. CI and the linked PR carry the final check results. Closure +requires GitHub to mark alerts 21–25 fixed after merging into the default branch; +no vulnerability dismissal is part of this change. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ba87bfe06e..b319abd791 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -59,7 +59,7 @@ "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.2.0", - "@vitest/coverage-v8": "^4.1.8", + "@vitest/coverage-v8": "^4.1.11", "acorn": "^8.15.0", "autoprefixer": "^10.4.22", "baseline-browser-mapping": "^2.9.19", @@ -74,7 +74,7 @@ "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^8.0.16", - "vitest": "^4.1.8" + "vitest": "^4.1.11" } }, "node_modules/@adobe/css-tools": { @@ -2629,6 +2629,7 @@ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, + "license": "MIT", "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" @@ -2709,7 +2710,8 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", @@ -3101,13 +3103,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", - "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, + "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.8", + "@vitest/utils": "4.1.11", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -3121,8 +3124,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.8", - "vitest": "4.1.8" + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -3131,15 +3134,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", - "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, + "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -3148,12 +3152,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", - "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.8", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3174,10 +3179,11 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", - "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, + "license": "MIT", "dependencies": { "tinyrainbow": "^3.1.0" }, @@ -3186,12 +3192,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", - "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.8", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -3199,13 +3206,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", - "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.8", - "@vitest/utils": "4.1.8", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3214,21 +3222,23 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", - "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, + "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", - "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.8", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -3339,6 +3349,7 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } @@ -3541,6 +3552,7 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -4121,10 +4133,11 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -4397,10 +4410,11 @@ "license": "MIT" }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } @@ -6617,14 +6631,18 @@ "license": "MIT" }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" - ] + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/optionator": { "version": "0.9.4", @@ -6775,7 +6793,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/pdfjs-dist": { "version": "6.2.108", @@ -7520,7 +7539,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/signal-exit": { "version": "4.1.0", @@ -7558,7 +7578,8 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/statuses": { "version": "2.0.2", @@ -7571,10 +7592,11 @@ } }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", - "dev": true + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" }, "node_modules/strict-event-emitter": { "version": "0.5.1", @@ -7717,13 +7739,15 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -7745,10 +7769,11 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -8431,18 +8456,19 @@ } }, "node_modules/vitest": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", - "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", - "dev": true, - "dependencies": { - "@vitest/expect": "4.1.8", - "@vitest/mocker": "4.1.8", - "@vitest/pretty-format": "4.1.8", - "@vitest/runner": "4.1.8", - "@vitest/snapshot": "4.1.8", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -8470,12 +8496,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.8", - "@vitest/browser-preview": "4.1.8", - "@vitest/browser-webdriverio": "4.1.8", - "@vitest/coverage-istanbul": "4.1.8", - "@vitest/coverage-v8": "4.1.8", - "@vitest/ui": "4.1.8", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -8616,6 +8642,7 @@ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, + "license": "MIT", "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" diff --git a/frontend/package.json b/frontend/package.json index ce0f3fa9ac..d5196ac051 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,10 +9,11 @@ "lint": "eslint .", "preview": "vite preview", "test": "vitest", - "test:run": "vitest run && npm run check:i18n", + "test:run": "vitest run && npm run check:i18n && npm run check:mocker-security", "test:coverage": "vitest run --coverage", "test:ui": "vitest --ui", "check:i18n": "node scripts/check-i18n-parity.mjs", + "check:mocker-security": "node scripts/check-mocker-security.mjs", "check:browser-runtime": "node scripts/check-browser-runtime.mjs", "check:order-master-data-generated": "node scripts/verify-order-master-data-unicode.mjs --generated-only", "verify:order-master-data-unicode": "node scripts/verify-order-master-data-unicode.mjs" @@ -81,7 +82,7 @@ "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.2.0", - "@vitest/coverage-v8": "^4.1.8", + "@vitest/coverage-v8": "^4.1.11", "acorn": "^8.15.0", "autoprefixer": "^10.4.22", "baseline-browser-mapping": "^2.9.19", @@ -96,6 +97,6 @@ "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^8.0.16", - "vitest": "^4.1.8" + "vitest": "^4.1.11" } } diff --git a/frontend/scripts/check-mocker-security.mjs b/frontend/scripts/check-mocker-security.mjs new file mode 100644 index 0000000000..be0d7776d7 --- /dev/null +++ b/frontend/scripts/check-mocker-security.mjs @@ -0,0 +1,48 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { test } from 'node:test'; +import { createServer } from 'vite'; +import { interceptorPlugin } from '@vitest/mocker/node'; + +// Exercise the installed plugin's actual WebSocket registration handler and +// load hook, with Vite's real fs policy. All canaries belong to this test. +for (const target of ['allowed.js', '.env', '../outside.js']) { + test(`redirect mocks respect server.fs: ${target}`, async () => { + const directory = await mkdtemp(join(tmpdir(), 'printops-mocker-')); + const root = join(directory, 'root'); + let server; + try { + await mkdir(root); + const canary = 'export const canary = "owned-security-fixture";'; + for (const file of ['allowed.js', '.env', '../outside.js']) { + await writeFile(join(root, file), canary); + } + server = await createServer({ + configFile: false, root, logLevel: 'silent', + server: { middlewareMode: true, ws: false, fs: { strict: true, allow: [root], deny: ['.env'] } }, + }); + const handlers = new Map(); + const plugin = interceptorPlugin(); + plugin.configureServer({ + config: server.config, + ws: { on: (event, handler) => handlers.set(event, handler), send() {} }, + }); + const register = handlers.get('vitest:interceptor:register'); + assert.equal(typeof register, 'function'); + // An opaque URL retains dot segments, unlike normalized http/file URLs. + const event = { type: 'redirect', raw: './mock.js', id: '/mock.js', url: '/mock.js', redirect: `fixture:${target}` }; + if (target === 'allowed.js') { + await register(event); + assert.equal(await plugin.load.handler('/mock.js'), canary); + } else { + await register(event); + assert.equal(await plugin.load.handler('/mock.js'), undefined, 'blocked redirect must never be registered or read'); + } + } finally { + await server?.close(); + await rm(directory, { recursive: true, force: true }); + } + }); +} diff --git a/installers/windows/build.py b/installers/windows/build.py index ee6da775a7..1b1ad1e2ec 100644 --- a/installers/windows/build.py +++ b/installers/windows/build.py @@ -78,8 +78,8 @@ # Official, immutable Windows runtimes used by the commercial-document # pipeline. The WeasyPrint standalone binary contains its matching native # Pango/GTK stack; Temurin supplies the Java runtime required by veraPDF. -WEASYPRINT_RUNTIME_URL = "https://github.com/Kozea/WeasyPrint/releases/download/v69.0/weasyprint-windows.zip" -WEASYPRINT_RUNTIME_SHA256 = "330101ff3ea50ebde4abf805283b6d703d5f3d71c77c983db94357ec4524a3ef" +WEASYPRINT_RUNTIME_URL = "https://github.com/Kozea/WeasyPrint/releases/download/v70.0/weasyprint-windows-onedir.zip" +WEASYPRINT_RUNTIME_SHA256 = "ab1151f210b4e6bb7aa7a79e91a67e8ddb760094c107bfda55241b6aaefe7d53" TEMURIN_JRE_URL = ( "https://github.com/adoptium/temurin21-binaries/releases/download/" "jdk-21.0.10%2B7/OpenJDK21U-jre_x64_windows_hotspot_21.0.10_7.zip" @@ -347,7 +347,7 @@ def stage_document_runtimes(python_dir: Path, *, verify_only: bool = False) -> N weasy_zip = download_verified( WEASYPRINT_RUNTIME_URL, - DOWNLOADS / "weasyprint-69.0-windows.zip", + DOWNLOADS / "weasyprint-70.0-windows-onedir.zip", WEASYPRINT_RUNTIME_SHA256, ) jre_zip = download_verified( @@ -363,6 +363,10 @@ def stage_document_runtimes(python_dir: Path, *, verify_only: bool = False) -> N if weasy_target.exists(): shutil.rmtree(weasy_target) unzip(weasy_zip, weasy_target) + # v70's onedir distribution includes the native DLLs in _internal. + # Preserve our installed CLI path; move the complete runtime, not just EXE. + (weasy_target / "onedir" / "weasyprint").rename(weasy_target / "dist") + (weasy_target / "onedir").rmdir() weasy_exe = weasy_target / "dist" / "weasyprint.exe" if not weasy_exe.exists(): raise RuntimeError(f"official WeasyPrint executable missing at {weasy_exe}") diff --git a/installers/windows/tests/test_pdf_runtime_packaging.py b/installers/windows/tests/test_pdf_runtime_packaging.py new file mode 100644 index 0000000000..041931d81e --- /dev/null +++ b/installers/windows/tests/test_pdf_runtime_packaging.py @@ -0,0 +1,43 @@ +"""Portable installer tests for PrintOps' stable Windows PDF runtime path.""" + +from __future__ import annotations + +import zipfile + +import pytest + +from installers.windows import build + + +@pytest.mark.parametrize("include_executable", [True, False]) +def test_windows_onedir_runtime_is_staged_with_native_libraries(tmp_path, monkeypatch, include_executable): + weasy = tmp_path / "weasy.zip" + with zipfile.ZipFile(weasy, "w") as archive: + archive.writestr("onedir/weasyprint/_internal/libpango-1.0-0.dll", b"native fixture") + if include_executable: + archive.writestr("onedir/weasyprint/weasyprint.exe", b"executable fixture") + jre = tmp_path / "jre.zip" + with zipfile.ZipFile(jre, "w") as archive: + archive.writestr("temurin/bin/java.exe", b"java fixture") + monkeypatch.setattr(build, "STAGING", tmp_path / "staging") + monkeypatch.setattr(build, "DOWNLOADS", tmp_path / "downloads") + monkeypatch.setattr( + build, "download_verified", lambda url, *_: weasy if url == build.WEASYPRINT_RUNTIME_URL else jre + ) + commands = [] + monkeypatch.setattr(build.subprocess, "run", lambda command, **_: commands.append(command)) + + if not include_executable: + with pytest.raises(RuntimeError, match="official WeasyPrint executable missing"): + build.stage_document_runtimes(tmp_path / "python") + assert commands == [] + return + + build.stage_document_runtimes(tmp_path / "python") + runtime = build.STAGING / "runtime" + assert (runtime / "weasyprint/dist/weasyprint.exe").read_bytes() == b"executable fixture" + assert (runtime / "weasyprint/dist/_internal/libpango-1.0-0.dll").read_bytes() == b"native fixture" + assert not (runtime / "weasyprint/onedir").exists() + assert (runtime / "java/bin/java.exe").read_bytes() == b"java fixture" + assert len(commands) == 1 + assert commands[0][1] == str(build.REPO_ROOT / "scripts/vendor_pdf_runtime.py") diff --git a/requirements-dev.lock.txt b/requirements-dev.lock.txt index 3d6222b6a5..4e07383aff 100644 --- a/requirements-dev.lock.txt +++ b/requirements-dev.lock.txt @@ -3168,9 +3168,9 @@ watchfiles==1.2.0 \ --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn -weasyprint==69.0 \ - --hash=sha256:475951cfd917014de6d4d005caff48c6aa867e7e42b80cd5b16a0484a1609ee6 \ - --hash=sha256:a7a32f39ca16bd82ef11de99c92ea4b5f14951c9033af035e451ce4f4ee0a88c +weasyprint==70.0 \ + --hash=sha256:5043e55e38d2a2af2b2b871e869697b1f65dad5f8b4a3677961d04ceacf9c5fe \ + --hash=sha256:c263abf0e86c747b12af678b67f85f4abbfb97d18a20503031e7ba94e4b8cf8c # via -r requirements.txt webencodings==0.5.1 \ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ @@ -3418,8 +3418,8 @@ zopfli==0.4.3 \ # The following packages are considered to be unsafe in a requirements file: pip==26.2 \ - --hash=sha256:931c303696af6fa3417112103b1cad26890e5a07eccb5b99783700e33f2b8aad \ - --hash=sha256:2d8542afcc84cdd8e846c2b36b2861fad1da376dd98f8e7113e9108a3c331690 + --hash=sha256:2d8542afcc84cdd8e846c2b36b2861fad1da376dd98f8e7113e9108a3c331690 \ + --hash=sha256:931c303696af6fa3417112103b1cad26890e5a07eccb5b99783700e33f2b8aad # via # pip-api # pip-tools diff --git a/requirements.lock.txt b/requirements.lock.txt index 2c0f60ab10..967b02a42a 100644 --- a/requirements.lock.txt +++ b/requirements.lock.txt @@ -2729,9 +2729,9 @@ watchfiles==1.2.0 \ --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn -weasyprint==69.0 \ - --hash=sha256:475951cfd917014de6d4d005caff48c6aa867e7e42b80cd5b16a0484a1609ee6 \ - --hash=sha256:a7a32f39ca16bd82ef11de99c92ea4b5f14951c9033af035e451ce4f4ee0a88c +weasyprint==70.0 \ + --hash=sha256:5043e55e38d2a2af2b2b871e869697b1f65dad5f8b4a3677961d04ceacf9c5fe \ + --hash=sha256:c263abf0e86c747b12af678b67f85f4abbfb97d18a20503031e7ba94e4b8cf8c # via -r requirements.txt webencodings==0.5.1 \ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ diff --git a/requirements.txt b/requirements.txt index 0208e4d6b9..7da6c76ff3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -83,7 +83,7 @@ reportlab>=4.0.0 # Commercial document rendering. These exact versions form part of the # immutable render receipt stored with issued PDFs; do not loosen them without # a renderer-version migration and fresh PDF/A conformance fixtures. -weasyprint==69.0 +weasyprint==70.0 pikepdf==10.10.0 fonttools==4.63.0 jinja2==3.1.6