WeasyPrint 69.0 carries CVE-2026-55073, and the dependency audit refuses it #163

Closed
opened 2026-09-10 08:52:37 +00:00 by tiagoagueda · 1 comment
Owner

Observation

The security job fails:

Found 1 known vulnerability in 1 package
Name       Version ID             Fix Versions
---------- ------- -------------- ------------
weasyprint 69.0    CVE-2026-55073 70.0

Why now — and not because of a commit

The job audits the lock file against the advisory databases on every push and every Monday.
Nothing recent touched WeasyPrint: the advisory was published on 9 September 2026
(CVSS 6.2, medium) and the first run after it failed. Every earlier commit fails the same way
today, including both released versions — v0.2.0 and v0.2.1 lock WeasyPrint 69.0.

What the advisory is

From the GitLab advisory database:
two write_pdf() parameters ignore the document's url_fetcher and build a fresh default one.

  • xmp_metadata=[url] — the URL is fetched and embedded in the PDF: an arbitrary local file
    read when the path is attacker-influenced.
  • stylesheets=[url_or_path] — SSRF / arbitrary local-or-internal resource loading,
    transitive through @import and url().

An application is affected when it runs WeasyPrint server-side, relies on a restrictive
url_fetcher, and forwards an attacker-influenced URL into either parameter. The
70.0 release also stops rendering EPS images,
and recommends upgrading for anybody who embeds untrusted images.

Whether Postulo is exposed

Not through either channel described. documents/pdf.py calls
HTML(string=html).write_pdf() — neither stylesheets nor xmp_metadata — and nothing
WeasyPrint renders here (the CV, portfolio and letter themes, report_print.html) contains an
<img> or a url().

So this is not an emergency, but the gate is right to fail. --strict exists so that a
known-vulnerable dependency is not shipped, and 0.3.0 should not be released on a lock the
audit refuses.

What fixing it is

The constraint is weasyprint>=69, so nothing holds it back.
uv lock --upgrade-package weasyprint --dry-run resolves exactly one change:

Update weasyprint v69.0 -> v70.0

The upgrade is not covered by the tests, and the documents are worth looking at. WeasyPrint
bumps its major version every release, and the suite renders every PDF through a fake backend —
it tests which backend is chosen, never what WeasyPrint draws. It also cannot run WeasyPrint on
Windows, where Pango is missing. So the check is: render a CV, a portfolio, a letter and a report
on Linux (in the image, where the libraries are) and read them.

Worth being careful about

  • The image scan will say the same thing. scripts/scan-image.sh (#156) gates on fixable
    findings, and this one has just become fixable, so the image workflow fails on it too until
    the lock moves. Same fix.
  • A released version is affected, and the fix is a lock change. Whether 0.2.x gets a patch
    release is a release decision rather than this issue's.
  • Optional hardening while you are there. Postulo sets no url_fetcher at all. That is safe
    today because every document comes from escaped templates and inlines its own CSS: the comment
    on the Chromium backend promises that "the renderer never reaches the network or the disk", and
    it is true by construction. A fetcher that refuses everything would make it true by
    enforcement
    , which starts to matter once plugin themes (#132) put markup Postulo did not write
    in front of the renderer.

Classification

Security. Blocks CI, and so the 0.3.0 release.

## Observation The `security` job fails: ``` Found 1 known vulnerability in 1 package Name Version ID Fix Versions ---------- ------- -------------- ------------ weasyprint 69.0 CVE-2026-55073 70.0 ``` ## Why now — and not because of a commit The job audits the lock file against the advisory databases on every push and every Monday. Nothing recent touched WeasyPrint: the advisory was **published on 9 September 2026** (CVSS 6.2, medium) and the first run after it failed. Every earlier commit fails the same way today, **including both released versions** — v0.2.0 and v0.2.1 lock WeasyPrint 69.0. ## What the advisory is From the [GitLab advisory database](https://advisories.gitlab.com/pypi/weasyprint/CVE-2026-55073/): two `write_pdf()` parameters ignore the document's `url_fetcher` and build a fresh default one. - `xmp_metadata=[url]` — the URL is fetched and embedded in the PDF: an arbitrary local file read when the path is attacker-influenced. - `stylesheets=[url_or_path]` — SSRF / arbitrary local-or-internal resource loading, transitive through `@import` and `url()`. An application is affected when it runs WeasyPrint server-side, relies on a restrictive `url_fetcher`, and forwards an attacker-influenced URL into either parameter. The [70.0 release](https://github.com/Kozea/WeasyPrint/releases) also stops rendering EPS images, and recommends upgrading for anybody who embeds untrusted images. ## Whether Postulo is exposed **Not through either channel described.** `documents/pdf.py` calls `HTML(string=html).write_pdf()` — neither `stylesheets` nor `xmp_metadata` — and nothing WeasyPrint renders here (the CV, portfolio and letter themes, `report_print.html`) contains an `<img>` or a `url()`. So this is not an emergency, but the gate is right to fail. `--strict` exists so that a known-vulnerable dependency is not shipped, and 0.3.0 should not be released on a lock the audit refuses. ## What fixing it is The constraint is `weasyprint>=69`, so nothing holds it back. `uv lock --upgrade-package weasyprint --dry-run` resolves exactly one change: ``` Update weasyprint v69.0 -> v70.0 ``` **The upgrade is not covered by the tests, and the documents are worth looking at.** WeasyPrint bumps its major version every release, and the suite renders every PDF through a fake backend — it tests which backend is chosen, never what WeasyPrint draws. It also cannot run WeasyPrint on Windows, where Pango is missing. So the check is: render a CV, a portfolio, a letter and a report on Linux (in the image, where the libraries are) and read them. ## Worth being careful about - **The image scan will say the same thing.** `scripts/scan-image.sh` (#156) gates on *fixable* findings, and this one has just become fixable, so the `image` workflow fails on it too until the lock moves. Same fix. - **A released version is affected, and the fix is a lock change.** Whether 0.2.x gets a patch release is a release decision rather than this issue's. - **Optional hardening while you are there.** Postulo sets no `url_fetcher` at all. That is safe today because every document comes from escaped templates and inlines its own CSS: the comment on the Chromium backend promises that "the renderer never reaches the network or the disk", and it is true *by construction*. A fetcher that refuses everything would make it true *by enforcement*, which starts to matter once plugin themes (#132) put markup Postulo did not write in front of the renderer. ## Classification Security. Blocks CI, and so the 0.3.0 release.
tiagoagueda added this to the 0.3.0 milestone 2026-09-10 08:52:37 +00:00
Author
Owner

Done in dbd55b2c. The security job has passed since (runs 274 and 276), and the live test instance now runs WeasyPrint 70.0.

The upgrade

The lock is at 70.0, the only change the resolver makes. The requirement is now weasyprint>=70, so an installation from the wheel cannot resolve to the old one. As you established, Postulo passes neither stylesheets nor xmp_metadata, and a comment on render() now says why that matters.

The optional hardening: taken

  • WeasyPrint gets URLFetcher(allowed_protocols={"data"}, allow_redirects=False): data: and nothing else. Both 69 and 70 have that argument, so no fetcher of our own was needed.
  • Chromium aborts every request the page makes (page.route("**/*", abort)). A data: address is not a request, so an embedded image still draws.

tests/security/test_pdf_fetching.py holds both, and each test was run against the unsafe version and failed. With WeasyPrint's default fetcher, the fetcher test read the test's secret file, and a stylesheet on the disk turned the page into a 100 mm square. Without the route, Chromium fetched both /sheet.css and /pixel.png from a local listener. The wiki's CV page tells theme authors the rule: a theme draws only what it carries.

"The upgrade is not covered by the tests": now it is

tests/test_pdf_render.py draws every kind of document in every shipped theme, a document set right to left, and the report, with the real renderer. In CI's test job (run 276): tests/test_pdf_render.py ......... and tests/security/test_pdf_fetching.py ...ss. The two skipped are the Chromium tests, since that job has no browser.

Running them on Linux first found that they would have skipped on CI in silence. WeasyPrint 70 warns at import when HarfBuzz-Subset is missing, and says a later version will require it. The suite treats warnings as errors, so the import failed and the availability check said no. The tests now skip on Pango itself, so a warning fails them instead of hiding them. libharfbuzz-subset0 is in the image, in both CI jobs that install Pango, and in the README and wiki install commands, and the changelog tells operators who install without the image.

Reading real documents, on the test instance

I seeded a demo account in a throwaway container on SQLite in /tmp, so nothing touched the live data, and drew the same HTML with 69.0 and 70.0 through Postulo's own backend. There were nine documents: the CV and the portfolio in both themes, four letters (cover, motivation, follow-up, in both themes) and the report.

  • Page counts identical, and not one pixel different at 200 dpi, with no threshold at all. Every document had between 16,000 and 128,000 dark pixels, so they were real pages, not blank ones.
  • The bytes differ only in metadata and font-subset bookkeeping, 4 to 1,605 bytes per file.
  • 70.0 was about 11 % faster in total (5.13 s against 5.79 s on the Pi).

Worth knowing

  • The image scan (#156) reads the same lock, so an image built from it carries 70.0. I have not run the scan.
  • The released versions are affected all the same. v0.2.0 and v0.2.1 lock 69.0. SECURITY.md's process says a patch release (0.2.2) for them. That is a release decision, and I have not taken it.
  • Deploying found #166. The image had not built since #157, so nobody could have deployed any of this. It is fixed in b6cfb8f7, and the instance was backed up first (compose, .env, and a verified database backup).
Done in `dbd55b2c`. The `security` job has passed since (runs 274 and 276), and **the live test instance now runs WeasyPrint 70.0**. ## The upgrade The lock is at 70.0, the only change the resolver makes. The requirement is now `weasyprint>=70`, so an installation from the wheel cannot resolve to the old one. As you established, Postulo passes neither `stylesheets` nor `xmp_metadata`, and a comment on `render()` now says why that matters. ## The optional hardening: taken - **WeasyPrint** gets `URLFetcher(allowed_protocols={"data"}, allow_redirects=False)`: `data:` and nothing else. Both 69 and 70 have that argument, so no fetcher of our own was needed. - **Chromium** aborts every request the page makes (`page.route("**/*", abort)`). A `data:` address is not a request, so an embedded image still draws. `tests/security/test_pdf_fetching.py` holds both, and **each test was run against the unsafe version and failed**. With WeasyPrint's default fetcher, the fetcher test read the test's secret file, and a stylesheet on the disk turned the page into a 100 mm square. Without the route, Chromium fetched both `/sheet.css` and `/pixel.png` from a local listener. The wiki's CV page tells theme authors the rule: a theme draws only what it carries. ## "The upgrade is not covered by the tests": now it is `tests/test_pdf_render.py` draws every kind of document in every shipped theme, a document set right to left, and the report, with the real renderer. In CI's test job (run 276): `tests/test_pdf_render.py .........` and `tests/security/test_pdf_fetching.py ...ss`. The two skipped are the Chromium tests, since that job has no browser. **Running them on Linux first found that they would have skipped on CI in silence.** WeasyPrint 70 warns at import when HarfBuzz-Subset is missing, and says a later version will require it. The suite treats warnings as errors, so the import failed and the availability check said no. The tests now skip on **Pango itself**, so a warning fails them instead of hiding them. `libharfbuzz-subset0` is in the image, in both CI jobs that install Pango, and in the README and wiki install commands, and the changelog tells operators who install without the image. ## Reading real documents, on the test instance I seeded a demo account in a throwaway container on SQLite in `/tmp`, so nothing touched the live data, and drew the same HTML with **69.0 and 70.0** through Postulo's own backend. There were nine documents: the CV and the portfolio in both themes, four letters (cover, motivation, follow-up, in both themes) and the report. - **Page counts identical, and not one pixel different at 200 dpi, with no threshold at all.** Every document had between 16,000 and 128,000 dark pixels, so they were real pages, not blank ones. - The bytes differ only in metadata and font-subset bookkeeping, 4 to 1,605 bytes per file. - **70.0 was about 11 % faster** in total (5.13 s against 5.79 s on the Pi). ## Worth knowing - **The image scan** (#156) reads the same lock, so an image built from it carries 70.0. I have not run the scan. - **The released versions are affected all the same.** v0.2.0 and v0.2.1 lock 69.0. SECURITY.md's process says a patch release (0.2.2) for them. That is a release decision, and I have not taken it. - **Deploying found #166.** The image had not built since #157, so nobody could have deployed any of this. It is fixed in `b6cfb8f7`, and the instance was backed up first (compose, .env, and a verified database backup).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#163
No description provided.