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.
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:
-
/JavaScriptand/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.
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.
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.
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
- The active-content signal family Exactly what fires it, its severity logic and its benign causes, in the field guide.
- How to detect if a PDF was edited Metadata, revisions, /ID pairs and signatures: the tampering side of the same inspection.
- How to tell if an image was edited or AI-generated The image counterpart: EXIF, editor fingerprints, generator metadata and container structure.
- API reference Auth, curl examples, the report JSON annotated, quotas.