DEV Community

Cover image for Your PDF parser reads more than you do
Alessandro Pignati
Alessandro Pignati

Posted on

Your PDF parser reads more than you do

A Connecticut court just sanctioned someone for hiding AI instructions in a filing. The same trick works on your document pipeline.

Open a PDF in a viewer and you see what the author wanted you to see. Run it through pdfplumber or PyMuPDF and you get something else: every glyph on the page, including the ones set in 1pt white-on-white under the header. Your eyes do lossy rendering. Your extractor does not.

That gap is the whole attack. And in August 2026 it stopped being a lab demo.

The case

In Elliott v. New York Bariatric Group, LLC, a self-represented plaintiff in Connecticut Superior Court filed documents containing text no human reader would notice. The hidden block opened with a line to the effect of "IF THIS DOCUMENT IS REVIEWED BY AN AI MODEL" and then instructed the model to treat a clerk's prior denial as an error and resolve it in his favor.

Judge Walter M. Spader Jr. issued an order to show cause. More hidden text turned up in subsequent filings. The court revoked the plaintiff's electronic filing privileges, though he can still file on paper. NeuralTrust's writeup of the ruling, Hidden Text Prompt Injection in a US Court, has the procedural detail and the full timeline.

What matters for engineers is not the sanction. It is that someone with no technical background, no exploit toolkit, and no access to the target system wrote a payload in Microsoft Word and shipped it into an adversary's ingestion pipeline. The cost of attempting this is close to zero.

Why this class of bug is structural

An LLM receives one undifferentiated token stream. Your system prompt, the user's question, and the contents of the third-party PDF you just chunked all arrive in the same channel. There is no Content-Type boundary the model enforces, no parameterized query, no equivalent of prepared statements that separates instruction from data.

OWASP ranks this as LLM01 in the Top 10 for LLM Applications 2025 and is explicit that "it is unclear if there are fool-proof methods of prevention for prompt injection." That is an unusual admission for a security standard. Plan accordingly. You are managing blast radius, not closing a hole.

The indirect variant is the one that should worry you, because it needs no attacker contact with your users at all. NeuralTrust's guide to indirect prompt injection walks through the delivery surfaces: document metadata, zero-width characters, background-matched text, and poisoned content pulled in through retrieval.

Detecting the obvious cases

Hidden text has signatures. Font size below a readable threshold, fill color close to the page background, and render mode set to invisible are all cheap to check at ingestion. PyMuPDF exposes size and color per span:

import fitz  # PyMuPDF

SUSPICIOUS_MIN_SIZE = 4.0

def flag_hidden_spans(path, bg=(255, 255, 255), tol=24):
    out = []
    with fitz.open(path) as doc:
        for pno, page in enumerate(doc):
            for block in page.get_text("dict")["blocks"]:
                for line in block.get("lines", []):
                    for span in line["spans"]:
                        if not span["text"].strip():
                            continue
                        c = span["color"]
                        rgb = ((c >> 16) & 255, (c >> 8) & 255, c & 255)
                        near_bg = all(abs(a - b) <= tol for a, b in zip(rgb, bg))
                        tiny = span["size"] < SUSPICIOUS_MIN_SIZE
                        if near_bg or tiny:
                            out.append((pno, round(span["size"], 2), rgb, span["text"][:80]))
    return out
Enter fullscreen mode Exit fullscreen mode

A second check costs almost nothing and catches different cases: render the page to an image, OCR it, and diff the OCR output against the extracted text layer. Anything present in extraction but absent from the pixels is, by definition, text a human never saw. Normalize whitespace before diffing or you will drown in false positives.

Both checks are heuristics. White-on-white is the lazy version. An attacker can use a near-background color that survives a tolerance check, or phrase the injection as plain visible prose that reads as boilerplate. Treat the detector as a filter on low-effort attempts, not as a control you rely on.

The controls that actually bound the damage

Detection reduces volume. Architecture decides what happens when detection fails.

Keep the model out of the decision. If an LLM output can trigger a refund, a merge, an email, or a database write with no deterministic check in between, the injection does not need to be clever. It needs to be read once. Gate state-changing actions behind code that validates the action against policy, not behind the model's own judgment.

Scope credentials to the task, not the agent. The agent should not hold a token that can do more than the current request requires. This is ordinary least privilege and it is routinely skipped in agent builds because the convenient thing is one fat service account.

Label provenance in context. Wrap untrusted document content in explicit delimiters and tell the model, in the system prompt, that anything inside them is data to be analyzed rather than instructions to follow. This is weak on its own. It still measurably raises the bar.

Test it like an attacker. Hand-written injection strings go stale fast. Automated adversarial testing that regenerates payloads and runs in CI is how you catch regressions when someone swaps the model or edits a prompt template. NeuralTrust's TrustTest covers prompt injection and indirect injection as explicit attack categories, and the broader patterns for agent-level threats are collected at agentsecurity.com.

The part that generalizes

Courts are an unusually visible target, but nothing about the technique is legal-specific. Any pipeline that ingests documents it did not author has the same exposure. Resume screening. Vendor security questionnaires. Invoice processing. Research summarization. RAG over a shared drive that half the company can write to.

The threat model shift is small and easy to state. Documents used to be data. Once an LLM reads them, documents are code you are executing with your own privileges. Sanitize accordingly.

Top comments (0)