DEV Community

Cover image for [REDACTED] doesn't mean deleted
alphanumericentity
alphanumericentity

Posted on Edited on Originally published at pdfairlock.com

[REDACTED] doesn't mean deleted

Here's a PDF. Open it, and one line reads Name and SSN: [REDACTED].

Copy the text out of it, and you get [REDACTED]. Search the whole file for the SSN, and you find nothing. Open it in qpdf, a PDF debugging tool that unpacks the document and prints everything in it, and you still find nothing.

Now run one command:

head -c 823 released.pdf > recovered.pdf
Enter fullscreen mode Exit fullscreen mode

That keeps the first 823 bytes of a 1,158-byte file and throws the rest away. Open recovered.pdf:

Name and SSN: Marigold-Vance-447-26-8891
Enter fullscreen mode Exit fullscreen mode

It isn't a corrupted file or a trick. It's a perfectly valid PDF, and it's the document exactly as it was before the edit.

(The SSN is made up. I built every file in this post, so nobody's real data is involved.)

The part that stung: I ran this file through the redaction checker on my own site, with the SSN typed into its search box, and it said "Nothing recoverable found." I've fixed that, and the fix turned out to be the most useful thing in this post.

PDFs can keep their own history

PDF has a feature most people never hear about. When an editor saves, it doesn't have to rewrite the file. It can add just the changed parts onto the end, plus a note saying "these replace the old ones."

If you're a developer, think git. Every save like that is a new commit on top. Your PDF viewer only ever checks out the latest commit, so you see [REDACTED]. But the old commit is still sitting in the history, and in a PDF the history is simply the front of the file. Cut the file where the first save ended, and you've checked out the original.

That's why every check missed it. Copying the text, searching, and the debugging tool all ask what the document says now, and they all answer correctly: it says [REDACTED]. The old version isn't part of the document anymore. It's still part of the file.

The plain search failed for a more boring reason. PDF page content is usually compressed, with the same algorithm zip uses, so the SSN is in there, just not as readable text.

Nothing warns you when an editor saves this way. The file looks, opens and prints like any other.

Four ways to "remove" an SSN

I built four PDFs that each claim the SSN is gone, and ran three checks on each. ✅ means the check caught the problem. ❌ means it missed it.

How the SSN was "removed" Copy the text Search the file Inspect with qpdf Still recoverable?
Black box drawn over it ✅ ✅ ✅ yes
Deleted, but left in the metadata ❌ ✅ ✅ yes
Edited and saved by appending ❌ ✅ ❌ yes
Same, with normal compression ❌ ❌ ❌ yes

The first two are the classic mistakes, and I've written about them before. A black box is just a shape drawn on top of text that's still there. Metadata is a properties panel that apps fill in on their own and people rarely check.

The last row is what this post is about. Every check misses it, and the secret is still one command away.

Catching it without knowing what to look for

Searching only works if you know what you're searching for. Often you don't, because you're checking a file somebody else redacted.

The good news is that the file gives itself away. Every save ends with a short footer:

startxref
608
%%EOF
Enter fullscreen mode Exit fullscreen mode

A PDF saved once has one footer. A PDF that was edited and saved by appending has one per save. So the check is:

  1. Find every footer.
  2. Cut the file at each one except the last. Each cut is an older version.
  3. Pull the text out of each older version.
  4. Report any text that's in an older version but missing from the current one.

No search term needed. On the file above, that turns up the full SSN line on its own.

The bug I almost shipped

My first version was simpler: more than one %%EOF means edit history, so warn.

Then I tested it on a "linearized" PDF, the kind saved for fast loading on the web, which I made from the clean, saved-once file. It had two footers and no history at all. That detector would have flagged perfectly normal PDFs as edited.

The difference is that in the linearized file, the extra footer points at position 0. A real save can never point there, because position 0 is where the file's %PDF header lives. So the checker ignores footers that point at 0. It also ignores the first footer of any file that says it's linearized, in case some tool writes a different number there.

And it compares text rather than counting footers. So even if it misreads a footer, it can only report a leak when an older version really does hold text you can no longer see.

Try it on your own files

grep -c %%EOF yourfile.pdf
Enter fullscreen mode Exit fullscreen mode

1 means the file was written in one go. More than 1 means it has edit history, or it's a fast-loading web PDF. To find out which, and to see exactly what any older version contains, drop it into the redaction checker. It runs in your browser, so the file never leaves your machine.

The checker now does everything above. It finds older versions, pulls the text from each, lists anything the current pages don't show, and searches whatever you type in every version, not just the latest. If older versions exist but their text matches, it still tells you, because a removed photo or signature wouldn't show up when comparing text.

PDFAirlock's own redaction tool never had this problem, because it doesn't edit your file. It builds a brand-new one, with the redacted pages rebuilt as images and the metadata stripped, so there's no history for anything to hide in. It's all open source (AGPL), and the detector is in the repo if you want to borrow it.

What I haven't tested

I built these files myself, so this shows how the leak works, not how often it happens. I haven't scanned anyone's real documents, and I'd be wary of anyone quoting a percentage without saying where it came from.

Other leaks I know about but didn't build here:

  • Scanned PDFs with a hidden, searchable text layer behind the image
  • Thumbnail previews saved from before the redaction
  • Accessibility tags that keep a second copy of the text for screen readers
  • Embedded fonts that still list which letters were used
  • Leftover compressed pieces of a file that no version points to. Every check above misses those, my checker included.

So here's my question

Text, I can now catch. Pictures, I can't.

If someone deletes a face or a signature and the editor saves by appending, the checker can tell you an older version exists. It can't tell you that version holds a face someone meant to destroy. Comparing the pixels of each version will show that something changed, but to a pixel diff, a moved logo and a deleted face look exactly the same.

How would you tell, automatically, that an image in an older version was meant to be removed? I don't have an answer I trust, and I'd really like one.

Top comments (2)

Collapse
 
jes_p_d4f61f7aa2ec405a418 profile image
Dev_PJS •

The scariest property here is that every ordinary check passes. Copy the text: clean. Search the file: clean. Even unpack it properly: clean — and the data is still there, one truncation away. That's the general shape of false deletion: the verification everyone runs tests the index, not the bytes. I do removal work in agent memory systems and the same trap waits there — delete the note and every summary, quote, and derived claim built from it keeps circulating untouched, each one individually 'not the deleted file.' A redaction claim is really a claim about every copy and every layer, and it's only as good as the check that targets the layer where the data actually lives. Search is a reading tool. It was never a deletion proof.

Collapse
 
alphanumericentity profile image
alphanumericentity •

One thing I'd sharpen: the byte search does look at the bytes. On the uncompressed file it finds the SSN immediately. It misses the compressed one because the old page is still in there, just not as those 26 bytes. And the check that finally worked doesn't search bytes at all. It re-opens each old version and reads it as a document. So I'd put it as: every check reads one layer, and the data sat in a layer nobody re-read.

Your point about derived copies maps onto the half I didn't test. A thumbnail preview or an OCR text layer is a summary of the page stored inside the same file, and comparing text between versions can't see a thumbnail. If anyone has a reliable way to check what a thumbnail still shows, I'd like to hear it.