Can a PDF contain malware? How to check before you open it

The four markers that make a document active instead of static, how to read them by hand, and where a byte-level check ends and an antivirus begins.

Yes. A PDF can embed JavaScript, run an action the moment it opens, ask the reader to launch a program, and carry whole files inside itself. Every one of those announces itself in the file's dictionaries, readable before anything executes.

A PDF is not a passive picture of a page, which means you can read what a document intends to do before any reader does it. This page shows the manual check, what each marker means, where a byte-level read ends and an antivirus begins, and the automated version.

Scope, stated plainly. A structural check reads what the document asks its reader to do. It does not decode payloads, emulate JavaScript or match virus signatures. That is an antivirus's job, and the two checks answer different questions. "This bank statement carries JavaScript and an auto-run action" is a fact worth acting on regardless of whether any scanner recognises the payload.

Four keys are the whole of it, and every one of them is a statement of intent the file makes about itself, not a finding about the file.
WHAT A READER IS ASKED TO DO, AND WHERE THE ASK IS WRITTEN /Root: the catalog /OpenAction fires the instant the file opens, before anyone clicks anything usually just “page 1, fit width” /Names /JavaScript a name tree of scripts the reader may run Acrobat does, browsers heavily restrict it /Launch on a page annotation: run an external program or open another file modern readers warn or refuse /EmbeddedFile a complete file of any type carried inside, and extractable by the reader the point of e-invoice PDFs THE LIMIT THAT MATTERS Reading these keys tells you what the document asks for. It does not tell you whether the ask is hostile. Nothing here is decoded, emulated or matched against a signature. “This file carries JavaScript” is the finding; “this file is malware” is not.

Positions are the ones a structural parser resolves: the catalog's own entries, the name trees hanging off /Names, and the action dictionary on a page annotation. Historic exploits also lived in malformed fonts, images and streams, which is why none of the checks on this page render the file.

1. How a PDF becomes active

Four dictionary keys turn a static document into one that does things:

  • /JavaScript and /JS: embedded script, run by readers that support it (Acrobat does; most browser viewers heavily restrict it).
  • /OpenAction: an action executed automatically the moment the document opens, before the user does anything. The classic delivery mechanism is an OpenAction pointing at embedded JavaScript.
  • /Launch: a request to run an external program or open another file. Modern readers warn or refuse, but the intent is recorded in the file either way.
  • /EmbeddedFile: a complete file carried inside the PDF, of any type, extractable by the reader.

Historically, PDF exploits have also lived in malformed structures, fonts, images and streams crafted to crash a specific parser. That is exactly why nothing in this page involves opening the file: every check below reads bytes without rendering them.

2. Check by hand with pdfid

The standard first-look tool is Didier Stevens' pdfid, a small Python script that counts the interesting names in a file without parsing: deliberately, so a malformed document cannot exploit the tool that inspects it:

A file that does things when opened

$ pdfid.py statement.pdf
 /JS                    1
 /JavaScript            1
 /OpenAction            1
 /Launch                0
 /EmbeddedFile          0

Non-zero counts for /JS, /JavaScript, /OpenAction or /Launch are your cue to look closer (Stevens' companion pdf-parser.py extracts the actual objects), or simply to not open the file outside a sandbox. One caveat comes with the counting approach: pdfid matches name strings anywhere in the raw bytes, so a PDF about JavaScript (this page printed to PDF, say) can trigger counts from its own prose. Position matters: a marker only means something in a real dictionary-key position, which is what a structural parser checks.

The same five characters mean “run this” in one position and mean nothing at all in the other. Counting bytes cannot tell them apart.
A REAL DICTIONARY KEY 7 0 obj << /S /JavaScript /JS (app.launchURL…) >> A key in an action dictionary, naming a script a reader will run. THE SAME BYTES, PRINTED ON A PAGE BT /F1 11 Tf 72 700 Td (a PDF may carry /JS) Tj ET A string inside a content stream. Nothing executes. Nothing is asked for. WHAT EACH TOOL REPORTS pdfid: raw byte scan structural parse of resolved objects 1 1 1 0 key position page text

Not a criticism of pdfid: it refuses to parse on purpose, so a malformed document cannot exploit the tool inspecting it. The cost of that choice is this false-positive class, and it is the reason the two checks are worth running in that order.

3. What each marker means in a document workflow

Context decides severity. In a form distributed by a tax authority, JavaScript is furniture. In a bank statement, a payslip or a proof of address. Documents that are generated, printed-equivalent artifacts, there is no legitimate reason for script or a launch action to exist. One marker needs its own caveat: a bare /OpenAction is usually not active content at all. In the overwhelming majority of files it holds a destination ("open at page 1, fit width"), which ordinary generators write as a matter of course; when it instead carries a script, the JavaScript marker shows up with it. The question "is this PDF dangerous?" and the question "was this PDF tampered with?" overlap here: active content in a generated document is evidence something other than the generator wrote to the file.

Severity tracks what the file can do, not how alarming the key sounds: which is why the most-quoted marker sits at the bottom.
WHAT IS BEING ASKED FOR REPORTED AS /JavaScript · /JS · /Launch Code, or a request to start a program. The document wants the reader to execute something. medium /EmbeddedFile A payload that travels with the document but only moves if somebody extracts it. low /OpenAction: a view destination only “Open at page 1, fit width.” Ordinary generators write it as a matter of course; no code is named. info

Context outranks the ladder. In a tax authority's interactive form, JavaScript is furniture; in a bank statement or a payslip, documents that are print-equivalent artefacts, there is no legitimate reason for any of the top row to be present.

4. Benign causes: most active PDFs are legitimate

Interactive forms validate fields with JavaScript. Government and insurance PDFs auto-fill dates on open. E-invoice standards (ZUGFeRD, Factur-X) attach machine-readable XML as embedded files, an /EmbeddedFile is the point of the format. Portfolios carry whole documents inside a cover PDF. Which is why a counter of markers is a screening tool, not a verdict: the finding is "this document asks its reader to run things", and whether that is normal depends entirely on what the document claims to be.

5. What an antivirus adds, and when to use one

A structural check tells you a payload exists; a scanner tries to tell you whether the payload is known to be hostile: signature matching, heuristics, sometimes detonation in a sandbox. For a file you personally received and intend to open, VirusTotal runs seventy-odd engines and is the right second step (with the obvious caveat that uploading confidential documents to a public multiscanner shares them). For a pipeline receiving customer documents, the two layers answer different business questions: the scanner protects your infrastructure; the structural check protects your decision, it flags the statement that carries script as not being the artifact a bank generates, whether or not the script matches any signature.

The two checks do not overlap and neither replaces the other: one protects your infrastructure, the other protects your decision.
STRUCTURAL READ “What does this document ask its reader to do?” Bytes only. Nothing is decoded, emulated or executed, and nothing is extracted. Answers even for a payload nobody has seen. ANTIVIRUS / MULTISCANNER “Is this payload known to be hostile?” Signatures, heuristics, sometimes detonation in a sandbox. Answers about payloads it recognises. WHAT NEITHER ONE ANSWERS Whether an unrecognised payload in a document that legitimately carries scripts is safe to open. A clean scan means “nothing matched”; an empty marker count means “nothing was asked for”. Neither means “safe”.

One practical caveat on the right-hand box: a public multiscanner shares whatever you upload to it, which is a real cost when the file is a customer's bank statement.

6. Screening uploads automatically

Tamperlens reports active content as one of its nineteen signal families: active-content fires on JavaScript, OpenAction, Launch actions and embedded files, with the object positions as evidence. Weighted by what the file can do: script and launch actions score medium, attachments low, and a bare navigation OpenAction is reported as info only. Detection is scoped to genuine dictionary-key positions in parsed objects: a /JS that merely appears inside a page's text does not fire, which removes the false-positive class that raw byte counting carries.

The engine never executes or opens anything it inspects: documents are parsed in memory as bytes, no JavaScript runs, no embedded file is extracted, nothing is written to disk. The same POST returns the other eighteen families (revision history, metadata mismatches, signature coverage), so an upload pipeline gets the security screen and the tampering screen in one report.

Run the marker check on two files you already understand

Drop an interactive form from a tax authority into the checker, then a statement from your own accounting system. The first should report active content and the second should not, and the gap between those two reports is what tells you whether this signal is worth a rule in your intake. The file is sent over HTTPS, parsed in memory and never written to disk; no account, no quota. The three precomputed samples at the top of the home page cover the tampering half of the same report; none of them carries active content, so this one you have to bring yourself.

When you want it in your pipeline, an API key with 50 documents a month is free: see the API quickstart or create an account.

Tamperlens reports risk signals, not authenticity verdicts, and it is not an antivirus. Signals can have benign causes; combine them with your own decision logic.

Related reading