Skip to main content

pdf-defang

Strip JavaScript, OpenAction, Launch actions and other active content from PDFs. Lightweight Python library on top of pikepdf. MIT licensed.

PyPI Python Downloads CI Docs License mypy Ruff

📚 Full documentation | 📦 PyPI | 🛠️ Built by kovetz.co.il


Why?

PDFs can carry executable content: JavaScript that runs when the file opens, auto-actions that fire on every page navigation, "Launch" actions that try to open other programs, embedded files that drop malware. If you process user-uploaded PDFs in your app, you should strip this content before serving them back.

The Python ecosystem has parsers (pikepdf, pypdf, PyMuPDF) and a heavy container-based tool (Dangerzone), but no clean drop-in library that says "give me this PDF without active content." This is that library.

What this protects against — and what it does not

Be precise about the threat model before you rely on this.

In scope. Content the PDF asks a viewer to execute: document and annotation JavaScript, /OpenAction and /AA auto-actions, /Launch and the other dangerous action types, embedded files, XFA, and URI actions pointing at javascript:, file:, data: and friends. This is a real and actively exploited class — removing it is worth doing, and it costs you a few milliseconds per file.

Out of scope. Memory-corruption bugs in the viewer's parser — the heap overflows and use-after-frees that turn up in PDFium and Acrobat several times a year. Those fire on malformed structure, with no active content involved at all. Stripping actions does nothing to them, and neither does any other structural sanitizer.

If that is your threat model — hostile documents from untrusted senders, journalists, incident response — you want render-to-pixels reconstruction inside a disposable sandbox, i.e. Dangerzone. Not this. A library installed into your own process inevitably parses the hostile file in your process; the isolation is the protection, and a library cannot ship isolation.

This library is a cheap, high-value layer for a normal web app that accepts PDF uploads and serves them back. It is one layer, not a guarantee. Run it behind an antivirus pass on the original upload, and keep your viewers patched.

Install

pip install pdf-defang

Requires Python 3.9+ and pikepdf 8+.

Quick start

Python API

from pdf_defang import sanitize, scan

# Clean a file in place
sanitize("uploaded.pdf")

# Get a detailed report of what was removed
report = sanitize("uploaded.pdf", return_report=True)
print(report.javascript_in_names)        # 2
print(report.open_action_removed)        # True
print(report.annotation_action_types)    # ['Launch']
print(report.dangerous_uris_removed)     # 1
print(report.as_dict())                  # JSON-serialisable

# Inspect a file WITHOUT modifying it
report = scan("suspicious.pdf")
print(report.risk_level)                 # 'high' / 'medium' / 'low' / 'none'
print(report.has_javascript)             # True

Async API (FastAPI / aiohttp / asyncio)

from pdf_defang import sanitize_async, scan_async

async def handle_upload(path):
    report = await sanitize_async(path, return_report=True)
    return report.as_dict()

In-memory API (S3, Lambda, no disk)

from pdf_defang import SanitizeError, sanitize_bytes

raw_pdf: bytes = ...   # from S3, HTTP, anywhere
try:
    cleaned: bytes = sanitize_bytes(raw_pdf)
except SanitizeError as exc:
    # Unparseable or encrypted - nothing was stripped, so there is no
    # clean file to serve. Reject it.
    ...
# No disk involved

Encrypted PDFs (encryption preserved on output)

sanitize("encrypted.pdf", password="hunter2")
# Still encrypted with the same password, JavaScript removed.

Two levels: strict (default) vs balanced

# Public uploads: kill everything active (safest)
sanitize("untrusted.pdf")                            # level="strict"

# Trusted internal forms that need Submit / Calculate buttons:
sanitize("expense_form.pdf", level="balanced")

Both levels strip pure attack vectors (/Launch, /GoToR, document JavaScript, dangerous URI schemes, etc.). balanced additionally preserves /SubmitForm / /ResetForm / form JS actions, annotation /AA and /JS triggers, the AcroForm /CO calculation order, and embedded files (used by PDF portfolios). Default is strict.

Command line

# Clean a single file (strict by default)
pdf-defang clean uploaded.pdf

# Clean many at once
pdf-defang clean *.pdf

# Keep form interactivity working
pdf-defang clean --level balanced internal_form.pdf

# Inspect without changes
pdf-defang scan suspicious.pdf

# Get JSON output for piping into your logging stack
pdf-defang scan suspicious.pdf --json | jq .risk_level
pdf-defang clean *.pdf --json > sanitization-log.json

Exit codes follow shell conventions:

Code clean scan
0 All files were already clean No active content found
1 At least one file had something stripped Active content detected
2 At least one file could not be opened File could not be scanned

Use cases

Web app that accepts PDF uploads

from pdf_defang import sanitize

def handle_upload(uploaded_file_path: str) -> str:
    report = sanitize(uploaded_file_path, return_report=True)
    if report.error:
        raise ValueError(f"Could not process PDF: {report.error}")
    # Log what was removed for your audit trail
    logger.info("Sanitized %s: %s", uploaded_file_path, report.as_dict())
    return uploaded_file_path  # safe to serve back to other users now

Suspicious file investigation

from pdf_defang import scan

report = scan("phishing_attachment.pdf")
if report.risk_level == "high":
    quarantine(report)
elif report.risk_level == "medium":
    notify_security_team(report)

Compliance pipeline (PDF/A clean output)

find /var/incoming -name '*.pdf' | xargs pdf-defang clean --json >> audit.jsonl

What gets removed

Item Where What it does
/JavaScript in /Names Document root Document-level JavaScript that runs on open
/EmbeddedFiles Document root Files hidden inside the PDF (potential malware)
/OpenAction Document root Action automatically executed when PDF opens
/AA Document root "Additional Actions" - auto-execute on navigation
/XFA /AcroForm Legacy XML forms - well-known attack surface
/CO /AcroForm Form field Calculation Order
/AA Each page Page-level auto-execute actions
Dangerous /A Each annotation JavaScript, Launch, ImportData, SubmitForm, ResetForm, Rendition, GoToR, GoToE, Movie, Sound actions
/AA Each annotation Per-annotation auto-actions
/JS Each annotation JavaScript attached directly to an annotation
Unsafe /URI Each annotation URI actions with dangerous schemes (javascript:, file:, data:, vbscript:, UNC paths). Standard hyperlinks (http, https, mailto, tel, ftp, etc.) are preserved.

What is preserved

Sanitization is non-destructive to visible content:

  • All text, images and layout
  • Standard form fields (filled values stay intact)
  • Bookmarks, table of contents, page labels
  • Document metadata (Author, Title, Subject, Keywords)
  • Standard link annotations to mailto: / http(s): URLs
  • Document structure, page count, page order

Why not Dangerzone / iText / commercial SDKs?

Tool Why this might not fit you
Dangerzone Excellent for sensitive analyst workflows, but runs a full Docker container per file. Minutes per PDF, not milliseconds.
iText / Apryse Powerful, but commercial licenses start at thousands of USD/year.
pikepdf directly Brilliant library, but it's a parser, not a sanitizer. You'd write the same _strip_document_level() code we wrote here. That's exactly what we extracted.

pdf-defang is for the case where you want a small, free, drop-in function to ship in your existing Python app. No subprocesses, no Docker, no per-seat license.

Performance

Measured on a Windows 11 laptop, Python 3.13, on the fixture PDFs:

Operation Median time
scan_bytes() on a clean PDF (in memory) ~0.3 ms
sanitize_bytes() on a malicious PDF (in memory) ~0.6 ms
sanitize() on a clean PDF (with disk I/O) ~8 ms
sanitize() kitchen-sink PDF (with disk I/O) ~8 ms

These are 50-100 times faster than container-based tools like Dangerzone (which take seconds-to-minutes per file).

To benchmark on your hardware:

python -m pytest tests/test_performance.py -v -s

Caveats

  • Sanitization modifies the input file in place. If you need the original preserved for audit, copy it first.
  • Encrypted PDFs require the password= argument. Wrong-password attempts return an error report (not an exception).
  • Malformed PDFs may not open at all - we surface the underlying pikepdf error in the report. The original file is not touched on failure.
  • This is defense in depth, not a replacement for layered controls. Don't rely on a sanitizer alone for high-risk attachment workflows: also validate uploaders, sandbox processing, and scan with AV.

Origin story

This library was originally written for kovetz.co.il (Hebrew PDF tools, www.kovetz.co.il) in May 2026, during an APT scanning campaign by an Iranian-attributed threat actor sweeping endpoints for upload vectors. We needed to make sure that any PDF leaving our service was free of executable payloads, even if an attacker successfully uploaded a poisoned file.

We initially wrote 67 lines of pikepdf code, tested it on the kovetz.co.il fleet (thousands of files/day), then realised there's no clean equivalent in the OSS Python ecosystem. So we extracted it here for everyone else who needs the same thing.

Contributing

Issues and PRs welcome at github.com/kovetz-PDF/pdf-defang.

If you've found a PDF in the wild that contains active content we don't strip, please open an issue with the file (or a minimal reproducer) attached.

Development setup

git clone https://github.com/kovetz-PDF/pdf-defang.git
cd pdf-defang
python -m pip install -e ".[test]"
python -m pytest

The tests/conftest.py will auto-generate the test fixture PDFs on first run.

License

MIT - free for any use, including commercial.


Built and maintained by kovetz.co.il. Contact: contact@kovetz.co.il

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pdf_defang-0.2.0.tar.gz (33.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pdf_defang-0.2.0-py3-none-any.whl (22.4 kB view details)

Uploaded Python 3

File details

Details for the file pdf_defang-0.2.0.tar.gz.

File metadata

  • Download URL: pdf_defang-0.2.0.tar.gz
  • Upload date:
  • Size: 33.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for pdf_defang-0.2.0.tar.gz
Algorithm Hash digest
SHA256 6985e0ca701b4500b3bfe784230b20b390efea20b5a28d13564d178c90df4e5f
MD5 14e1f829b5182e9e132474e15c0d8028
BLAKE2b-256 9b28835b8bd437a33e7edcbb6e7cec862842b77eb1f5ff2f393a1d56790c29cb

See more details on using hashes here.

Provenance

The following attestation bundles were made for pdf_defang-0.2.0.tar.gz:

Publisher: publish.yml on kovetz-PDF/pdf-defang

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pdf_defang-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: pdf_defang-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 22.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for pdf_defang-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4877c4fda55e217b7b2d43d04183287a8b0e55d01b23258f7ac448cd54ac9861
MD5 d27fe27bde0d430b5128841d83e75114
BLAKE2b-256 891159d1d5fb4e4e05b8f25893752d62358c94f808ec49aceaba3e10d2f73930

See more details on using hashes here.

Provenance

The following attestation bundles were made for pdf_defang-0.2.0-py3-none-any.whl:

Publisher: publish.yml on kovetz-PDF/pdf-defang

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page