How a Tampered PDF Reveals Itself: Forensics of Structure, Not Appearance
I came from KYC/AML and tax compliance before I built Tamperlens, and the instinct I carry from that life is a tedious one to repeat to anyone new to looking at documents: opening cleanly is not integrity. A PDF that opens in the viewer, shows the right stamp, prints fine, and returns “200 OK” when you download it tells you nothing about whether it was edited after it was signed. Appearance is the layer the forger controls. Structure is the one he forgets.
This post is about the structure — how a PDF is assembled internally, where an edit gets recorded even when the page looks untouched, and how to reason about it. It is not a pitch. If you leave knowing how to open a PDF in a byte editor and understand what you’re seeing, I’ve done what I came to do.
The four parts of a PDF
A PDF, at the bone, has four sections, in this order in the file:
- Header — one line,
%PDF-1.7or similar. - Body — the list of objects: pages, fonts, images, the catalog, content streams. Each object is numbered (
12 0 obj … endobj). - Cross-reference table (xref) — an index that says, for each object, which byte of the file it starts at. It’s what lets a reader jump straight to page 40 without reading the 39 before it.
- Trailer — points to the document root and, crucially, says where the xref table begins, with the keyword
startxreffollowed by an offset.
The reader opens the file, goes to the end, reads startxref, jumps to the xref table, and from there finds everything else. Hold onto this: a PDF is read back to front. It’s that 1993 design decision that makes tampering visible.
The original sin: the incremental update
Here’s the detail almost nobody outside the format knows. When you edit a PDF and save, the standard does not rewrite the file. It appends. The new bytes go to the end, followed by a new xref table that records only what changed, followed by a new trailer whose startxref points at that new table.
And that new trailer carries a /Prev entry: the offset of the previous xref table. So each revision points back at the one before it, forming a chain linked backward all the way to the original. The whole file becomes a stack of sediment layers, and each layer is one time somebody opened it, changed something, and saved.
Why does the standard do this? Two legitimate reasons: it allows fast saves (no need to rewrite 200 pages to change one), and it allows signing without invalidating earlier signatures — the next signer appends, doesn’t rewrite. That’s why the incremental update exists, and it’s why it is not, on its own, proof of fraud. It’s proof that the file was written more than once. What was written the second time is the next question.
Tamperlens has a signal family called incremental-updates that does exactly this reading: it counts the revisions and, for each one after the first, lists which objects were written and which were overwritten. Overwriting an object of type page, content, or image in a later revision is the structural signature of “someone changed what was visible on the page after the document was finalized.” Object 12 existed; revision 3 wrote a new object 12 that the new xref now points to, and the old one is still there, buried, byte for byte, still readable for anyone who knows where to look.
The first way to be wrong: counting too many revisions
The honest part of this story is that the naive revision count lies. Two perfectly normal mechanisms write two cross-reference sections in a single save:
- Linearization (the “fast web view” that lets a PDF start rendering before it finishes downloading) writes a second xref table by design.
- Hybrid-reference layout — a classic table for old PDF 1.4 readers, plus a second one pointing at a
/XRefStm— also writes two.
If you count xref sections, a PDF freshly exported from Word and saved exactly once shows up as “two revisions” and you cry fraud over a pristine file. Tamperlens’s incremental-updates treats each of those pairs as one save — the parser recognizes the linearization pair and the hybrid pair and collapses them — which is why the family stays silent on an immaculate “fast web view” file and on an ordinary unsigned Word, Excel, or PowerPoint. The revision count it reports is deliberately lower than the number of startxref values you’d count in the raw file, and the finding itself explains why, because an analyst comparing the two numbers needs to be told the reason.
This is what separates forensics from theatre: a signal that fires on the most common legitimate document in the market is not a signal, it’s a false-positive generator. I’ll come back to this point — it’s the subject of an entire other post.
The benign causes have names
A genuine extra revision has innocent explanations, and the right move is to name them, not hide them: signing, form filling, annotation. Each appends bytes legitimately. The forensic question is never “is there more than one revision?” — it’s “what did the later revision touch?” A signature that appends a signature object is one thing. A “signature” that, in the same append, overwrites the content object of page 2 is something else entirely, and the structure reports both with equal clarity.
/ByteRange: why “signed” doesn’t close the conversation
Now the part most people get wrong, compliance people included. A digital signature in a PDF covers a declared byte range, the /ByteRange. It does not magically cover “the document.” It covers the bytes that existed when it was applied.
And what stops someone from signing, and then appending an incremental update with new bytes, beyond the end of the signed range? Nothing, in the format’s structure. The signature stays cryptographically valid — it covers what it covered — but there are bytes in the file it does not cover. Tamperlens’s signature-coverage compares each signature’s /ByteRange against the file size and reports exactly that gap: how many bytes fell beyond coverage, and where coverage ends.
Notice what this family does not do: it does not verify the signature’s cryptography. It reports coverage. That distinction matters, because a lot of “signature verification” out there answers the wrong question — it confirms the signature math checks out and declares the document valid, without ever asking whether the signature covers the whole file or just the first two-thirds of it.
The case that proves the risk band is not an oracle
Let me give you the most uncomfortable example we have, because it’s the best lesson in “opening cleanly is not integrity” — and in humility about measurement.
On 11 August 2026 we ran the engine over 1,317 public contracts from PNCP (Brazil’s government procurement portal). Result: 77% of the signed contracts score high — the top risk band. And the trigger is not tampering. It’s a second signature.
The mechanism is beautiful in its simplicity. A contract has two parties who sign. The first signer signs; their /ByteRange covers the file up to that point. The second signer (the countersignature) appends their bytes — which fall, necessarily, beyond the first signer’s /ByteRange. From the structure’s point of view, this is literally identical to someone having altered the document after the first signature: signature-coverage fires (there are bytes beyond coverage), incremental-updates fires (there’s a later revision), id-inconsistency fires (the trailer identifier changed). Three signals light up. And at the same time, the family that verifies the integrity of the signatures reports both intact.
Every statement is true of the bytes. And yet, on a two-party contract — the most common legitimate document in that market — the high band is noisy and uninformative. That’s why I don’t treat the score as a verdict. The score is the sum of the signals; what each signal means requires reading the finding, not the number. On a two-signature contract, the signals to read are the signature integrity and permissions families, not the band.
Publishing that finding about your own tool — “look at the false positive it produces on the most common document in the market” — is the opposite of marketing. It’s the only defensible posture for someone selling trust.
How the score is assembled (and why it’s conservative)
Since I brought up the band, it’s worth opening the box. Tamperlens aggregates the signals into a 0–100 score with an explicit contract:
- Any
high-severity signal forces the score to at least 70 — thehighband starts at 70. - And conversely, the
highband is reserved forhighsignals: without one, the score is clamped to 69.band === highis equivalent to “there is at least one signal that, on its own, establishes a change.” - Multiple
mediumsignals accumulate, but with diminishing returns past the first two of the same severity — a pile of weak signals cannot alone reach 100. - Bands: below 30 is
low, 30–69 iselevated, 70 or more ishigh.
The design point here is the deliberate asymmetry: weakness does not become strength by accumulation. You cannot stack ten weak suspicions into a certainty. Certainty — the high band — is reserved for the kind of finding that, in isolation, is already a change. That’s a risk choice, not an engineering one: I’d rather the high score mean something firm than be easy to reach.
How to reason about a PDF, in practice
If you’re going to look at a suspect PDF by hand, the script is this:
- Go to the end. Find the last
startxref. Count how manystartxrefvalues the file has — discounting the linearization and hybrid pairs, each of which is worth one real save. - Walk the
/Prevchain backward. Each trailer points at the previous xref. Each hop is one time the file was written. - On each revision after the first, ask what was overwritten. A
page,content, orimageobject rewritten is the question that matters. - If there’s a signature, compare the
/ByteRangewith the file size. Bytes left beyond coverage? Find out what they are. A legitimate countersignature and tampering cast the same shadow here — the difference is in what the appended bytes do, not in their mere existence. - Treat the score as a summary, never a verdict. Read the signals.
Two sanity defenses the format demands, because the file is written by the adversary: the /Prev chain is bounded (Tamperlens caps it at 64 hops — real documents accumulate a few revisions, not thousands), and the xref table is capped at 250,000 entries total. A 50 KB file can declare a /Prev chain that consumes gigabytes of a naive parser’s RAM; that’s the first thing a forensic parser has to refuse, because the hostile document will try.
The one-sentence summary
A forger controls what the page shows. He does not control — and almost never cleans — the trail the PDF’s structure keeps of every save: the revision chain, the objects he overwrote, the bytes he appended beyond the signature. “Opens fine” and “200 OK” are claims about the surface. Integrity is a claim about the structure. They’re different questions, and only the second matters when the document is money, a contract, or an identity.
If you want to see this reasoning turn into JSON on a real file, that’s what tamperlens.com is for. But the reasoning is yours to keep — take it even if you never touch the tool.
Need a custom technical project?
Architecture, TypeScript, APIs and automation, from prototype to production. The person answering your email is the one writing the code, and the deadline I promise is the one I can meet.
Send me a message →