Your PDF still contains the paragraph you deleted
Last month I sat with a small team cleaning up a supplier agreement before sending it back. One clause had commercial terms they didn't want to share. They struck it through, drew a white box over it, saved the file, and
Last month I sat with a small team cleaning up a supplier agreement before sending it back. One clause had commercial terms they didn't want to share. They struck it through, drew a white box over it, saved the file, and sent it to me to check.
Looked clean on screen. Printed clean. Opened fine in three different viewers.
Then I ran one command:
pdftotext -layout supplier-draft.pdf - | grep -i "exclusivity"
And the clause came back, in full, exactly as written before anyone touched it. Twenty minutes of careful editing, undone by the fact that none of those edits were deletions.
Nothing was broken. The tool did what it was built to do. What nobody asked it to do was remove anything.
A PDF is a log, not a document
This is the mental model that fixes the whole problem. A PDF isn't a canvas you repaint. It's a set of numbered objects plus an index (the cross-reference table) that says where each object lives in the file, plus a trailer pointing at that index.
When you open a file and change one word, a well-behaved editor has two options:
- Full rewrite. Rebuild every object, write a new file from scratch. Slow, but nothing old survives.
-
Incremental update. Append only the changed objects, append a new cross-reference section, append a new trailer, append
%%EOF. The original bytes are never touched.
Option 2 is what almost every editor does by default, and the reason is boring: it's fast, and it's crash-safe. If your laptop dies mid-save, the previous complete revision is still intact at the front of the file. For a word processor that's a feature. For a document you're about to send to a counterparty, it's a liability you didn't sign up for.
Count the saves
You can see how many revisions are stacked in a file in about a second:
grep -c "%%EOF" supplier-draft.pdf
Every incremental save appends its own %%EOF. One hit means the file was written once. Four hits means there are four generations of content in that file, and the earlier ones are still fully readable.
If you want to see the object offsets rather than just count revisions:
qpdf --show-xref supplier-draft.pdf | head -20
Each line is an object number, generation, and byte offset. Objects from older revisions still have offsets. They're still in there. Whether a viewer displays them depends on which cross-reference section it follows, and that's a much weaker guarantee than "deleted."
Drawing a box is not a redaction
Here's the specific failure I keep running into. Someone opens a contract in a general-purpose editor, selects the sensitive paragraph, and does one of three things:
- draws a filled rectangle over it
- sets the text colour to white
- puts a black highlight on it
All three produce a content stream that draws something on top of the text. The text operator is still there, one layer down, in the same content stream. pdftotext reads text operators, not pixels, so it hands you the paragraph instantly.
This is why "we redacted it" and "the text is gone" are two different claims, and only the second one can be verified.
Genuine removal means the object containing that text is no longer reachable from the document's cross-reference table. Most tools that offer a real redaction feature do this by rewriting affected content streams. A drawing tool does not.
The fix is a rewrite, and it has a cost
If you need the old bytes physically gone, you have to rewrite the file. Three options, in increasing order of thoroughness:
qpdf β fast, preserves structure and most annotations:
qpdf --object-streams=disable supplier-draft.pdf clean.pdf
mutool β garbage-collects unreachable objects:
mutool clean -gggg supplier-draft.pdf clean.pdf
Ghostscript β rebuilds the content streams by re-rendering them:
gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite \
-sOutputFile=clean.pdf supplier-draft.pdf
Ghostscript is the most aggressive and the one I reach for when the stakes are high, because it doesn't copy old objects at all β it reconstructs the page. The cost is real: interactive form fields flatten, some annotation types disappear, tagged-PDF accessibility structure is usually lost, and fonts get re-encoded. For a document you're sending to a counterparty who only needs to read it, that's an acceptable trade. For a document that has to stay machine-readable or accessible, it isn't, and you need a redaction tool that rewrites objects in place instead.
Whatever you pick, work on a copy. I've watched someone gs a signed agreement and quietly strip the signature layer. The signature was still visible as an image. It was no longer verifiable.
Verify, don't assume
The only thing that counts is reading the output file the same way an attacker would.
pdftotext -layout clean.pdf - | grep -ci "exclusivity" # expect 0
strings clean.pdf | grep -ci "exclusivity" # expect 0
grep -c "%%EOF" clean.pdf # expect 1
The strings pass matters because pdftotext only decodes content streams it understands. If a stream is compressed with a filter it can't handle, or the text is split across font subsets, pdftotext may return nothing while the bytes are still sitting there. strings catches the raw case. Neither command catches text baked into an image β for that you'd need OCR, and at that point the honest answer is that your redaction wasn't a redaction, it was a screenshot.
Two hits instead of zero means you found the problem before someone else did.
The part nobody wants to hear
A file that still contains deleted content is a retention problem, not just a cosmetic one. If the removed paragraph contained personal data, you've now transferred it while representing that you didn't. Under most privacy regimes the representation is what gets examined, and "the box was black" is not a defence.
The fix is small and unglamorous: check %%EOF count before sending, verify with a text extraction after editing, and rewrite rather than draw when something genuinely needs to disappear. Five minutes per file.
How are people handling this on inbound documents? I check files we receive, but I've never worked anywhere that checked them systematically β curious whether that's normal or just my sample.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.