惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
GbyAI
GbyAI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 三生石上(FineUI控件)
美团技术团队
Last Week in AI
Last Week in AI
WordPress大学
WordPress大学
L
LangChain Blog
雷峰网
雷峰网
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 叶小钗
Engineering at Meta
Engineering at Meta
腾讯CDC
Recent Announcements
Recent Announcements
The Register - Security
The Register - Security
有赞技术团队
有赞技术团队
Blog — PlanetScale
Blog — PlanetScale
博客园 - Franky
博客园 - 司徒正美
The Cloudflare Blog
Google DeepMind News
Google DeepMind News
T
Tailwind CSS Blog
C
Check Point Blog
小众软件
小众软件
V
Visual Studio Blog
V
V2EX
F
Full Disclosure
J
Java Code Geeks
MongoDB | Blog
MongoDB | Blog
罗磊的独立博客
人人都是产品经理
人人都是产品经理
量子位
Apple Machine Learning Research
Apple Machine Learning Research
F
Fortinet All Blogs
Microsoft Security Blog
Microsoft Security Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
博客园_首页
Y
Y Combinator Blog
N
Netflix TechBlog - Medium
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
Recorded Future
Recorded Future
G
Google Developers Blog
Vercel News
Vercel News
大猫的无限游戏
大猫的无限游戏
Microsoft Azure Blog
Microsoft Azure Blog
U
Unit 42
爱范儿
爱范儿
Jina AI
Jina AI

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
PDF xref Table Forensics: Detect Edits From File Structure
Iurii Rogulia · 2026-06-19 · via DEV Community

Originally published at htpbe.tech. The version on htpbe.tech stays in sync with the latest detection algorithm — refer to it for the canonical text.

Most PDF fraud detection focuses on what you can read: timestamps, producer strings, creator fields. That metadata is useful, but it is also the easiest thing to forge. A fraudster can overwrite every metadata field in under a minute.

The xref table is different. It is not a metadata field you can find and overwrite with a text editor. It is the PDF’s internal index — the structure the reader uses to locate every object in the file. Because the PDF specification requires editors to append a new xref rather than rewrite the existing one, every save operation leaves a structural mark that cannot be cleanly removed without rebuilding the file from scratch.

This article covers what the xref table is at the byte level, why it is the primary forensic signal for incremental modifications, what the edit trail looks like in practice, and how HTPBE reads the xref chain to produce intact, modified, and inconclusive verdicts. If you want a broader overview of how PDF tamper detection works across all five analysis layers, that is covered separately.

What the PDF cross-reference table actually is

A PDF file is not a sequential document. It is a collection of numbered objects — pages, fonts, images, the info dictionary, annotation arrays — written at arbitrary byte positions in the file. The cross-reference table (xref) is the index that maps each object number to its byte offset.

A minimal xref section looks like this:

xref
0 6
0000000000 65535 f 
0000000009 00000 n 
0000000058 00000 n 
0000000115 00000 n 
0000000266 00000 n 
0000000397 00000 n 

Each 20-byte entry records three values: the byte offset (0000000058), the generation number (00000), and a flag (n for in-use, f for free). When a PDF reader opens the file, it goes directly to the xref, reads the index, and uses the offsets to locate objects without scanning the entire file from the beginning.

After the xref section comes the trailer:

trailer
<<
  /Size 6
  /Root 1 0 R
  /Info 2 0 R
>>
startxref
431
%%EOF

The startxref offset tells the reader where in the file the xref begins. The /Root and /Info entries in the trailer point to the document catalog and the metadata dictionary.

Modern PDF 1.5+ files often use compressed xref streams (object streams) instead of the traditional plaintext xref table. The format differs — object offsets are packed into a binary stream inside a numbered PDF object — but the forensic logic is identical: each save session appends a new xref stream with its own /Prev pointer to the previous one.

A freshly exported PDF has exactly one xref section (or xref stream) and one %%EOF. That is the baseline.

Why incremental updates leave an unavoidable structural trail

The PDF specification defines a mechanism called the incremental update. When a PDF is edited and saved, a conforming implementation does not rewrite the existing file body. It appends changes to the end:

  1. New or modified objects are written after the existing %%EOF
  2. A new xref section is written, containing entries only for the changed objects
  3. A new trailer is written with a /Prev pointer to the previous xref’s byte offset
  4. A new %%EOF marker closes the update

The resulting file looks like this:

[original body — objects 1–5]
%%EOF
[new object 2 — modified Info dict]
[new object 6 — added annotation]
xref
0 1
0000000000 65535 f 
2 1
0000000512 00000 n 
6 1
0000000688 00000 n 
trailer
<<
  /Size 7
  /Root 1 0 R
  /Info 2 0 R
  /Prev 431          ← points to first xref
>>
startxref
730
%%EOF

The reader starts from the last %%EOF, follows startxref backward, reads the newest xref, and follows /Prev to reconstruct the full object table. Later revisions override earlier ones for the same object number — that’s how edits work.

For forensic purposes, the /Prev chain is a directed linked list of every save operation the file has ever undergone. Each node in the chain is an xref section. Counting the nodes gives you the number of save sessions.

A file with one xref was saved exactly once — almost always at export time from the originating application. A file with three xref sections was saved three times: once when created, and twice more afterward. That is a PDF revision history embedded in the structure. Whether it is malicious depends on context, but it is structurally irrefutable.

What the cross-reference table tampering trail looks like at the byte level

Here are the key differences between an untouched PDF and a modified one, visible at the binary level.

A clean, single-session PDF:

%PDF-1.7
[objects]
xref
0 12
[12 entries]
trailer
<< /Size 12 /Root 1 0 R /Info 2 0 R >>
startxref
4821
%%EOF

xref_count: 1. No /Prev. One %%EOF. This is what an unmodified export looks like.

A PDF modified with a standard editor:

%PDF-1.7
[original objects 1–11]
xref
0 12
[12 entries — original]
trailer
<< /Size 12 /Root 1 0 R /Info 2 0 R >>
startxref
4821
%%EOF

[modified Info dict at new offset]
[modified page content object at new offset]
xref
2 1
0000009104 00000 n 
5 1
0000009388 00000 n 
trailer
<< /Size 12 /Root 1 0 R /Info 2 0 R /Prev 4821 >>
startxref
9512
%%EOF

xref_count: 2. One /Prev pointer. Two %%EOF markers. The /Info object (object 2) was rewritten — metadata was changed in the edit session. The page content object (object 5) was also rewritten — text was altered.

A PDF modified twice:

Three xref sections, two /Prev pointers, three %%EOF markers. xref_count: 3. Each session is a separate link in the chain. If object 2 (the Info dict) changed in the second session but not the third, you know when the metadata modification happened relative to the content modification.

The xref chain is self-timestamping in the sense that the order of modifications is preserved in the structure. You cannot reorder the sessions without rebuilding the file. Duplicate object IDs across sessions confirm which objects were overwritten during each edit pass.

How HTPBE reads the xref chain to detect modified PDFs

HTPBE walks the xref chain from the last %%EOF backward through every /Prev pointer, counting sections and identifying which objects changed in each session. This structural pass is one part of the full forensic detection pipeline.

The raw structural data is returned in the API response alongside the verdict:

{
  "id": "ck_7b3e1a09-...",
  "status": "modified",
  "modification_confidence": "high",
  "modification_markers": ["HTPBE_MULTIPLE_REVISION_LAYERS", "HTPBE_DATES_DISAGREE"],
  "xref_count": 3,
  "has_incremental_updates": true,
  "update_chain_length": 2,
  "has_digital_signature": false,
  "creator": "Microsoft Word",
  "producer": "iLovePDF",
  "creation_date": 1704067200,
  "modification_date": 1709251200
}

xref_count is the total number of xref sections. update_chain_length is xref_count - 1 — the number of post-creation edit sessions. has_incremental_updates is true when update_chain_length > 0.

The verdict is not produced by xref analysis alone. HTPBE aggregates signals across 35 forensic checks — metadata timestamps, xref structure, digital signature integrity, producer/creator consistency, and object-level anomalies — into a single verdict. HTPBE_MULTIPLE_REVISION_LAYERS appearing in modification_markers means the xref chain was the triggering signal for that verdict.

The three verdicts in structural terms

intact: One xref section. No /Prev pointer. Timestamps consistent. Producer matches the document’s claimed origin tool. No anomalous object rewrites in a single session.

modified: Multiple xref sections, or a single xref section with timestamp anomalies, or a signature covering less than the full file body, or a producer string that names a known editing tool in a context where it should not appear. The modification_markers field names exactly which signals fired. See how the five detection layers combine to produce a verdict for the full scoring logic.

inconclusive: The document was created with consumer software — Microsoft Word, Google Docs, LibreOffice, a print-to-PDF driver. These tools do not produce the structural signatures of institutional document generators, so xref analysis is not meaningful: a fraudster can produce a fake bank statement in Word with a single clean xref, the same as a legitimate one. inconclusive is not a failure; it is a precise signal that the document’s origin bypasses structural forensics. The what inconclusive means guide explains this verdict class in detail.

Querying xref data from the PDF forensics API

Submit a PDF URL for analysis and retrieve the structural fields:

# Step 1 — submit for analysis
curl -X POST https://api.htpbe.tech/v1/analyze \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-storage.example.com/documents/statement.pdf"}'

# Response
# {"id": "ck_7b3e1a09-3d7c-4a8e-b1f2-9e0d3c5a7b8f"}

# Step 2 — retrieve result
curl https://api.htpbe.tech/v1/result/ck_7b3e1a09-3d7c-4a8e-b1f2-9e0d3c5a7b8f \
  -H "Authorization: Bearer YOUR_API_KEY"

In TypeScript, reading the xref fields directly:

interface HTPBEResult {
  id: string;
  status: 'intact' | 'modified' | 'inconclusive';
  modification_confidence: 'certain' | 'high' | 'none' | null;
  modification_markers: string[];
  xref_count: number;
  has_incremental_updates: boolean;
  update_chain_length: number;
  creator: string | null;
  producer: string | null;
  creation_date: number | null;
  modification_date: number | null;
}

async function analyzeXrefChain(pdfUrl: string): Promise<void> {
  const submitRes = await fetch('https://api.htpbe.tech/v1/analyze', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.HTPBE_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ url: pdfUrl }),
  });

  const { id } = await submitRes.json() as { id: string };

  const resultRes = await fetch(`https://api.htpbe.tech/v1/result/${id}`, {
    headers: { Authorization: `Bearer ${process.env.HTPBE_API_KEY}` },
  });

  const result = await resultRes.json() as HTPBEResult;

  console.log(`Verdict: ${result.status}`);
  console.log(`xref sections: ${result.xref_count}`);
  console.log(`Edit sessions after creation: ${result.update_chain_length}`);
  console.log(`Incremental updates: ${result.has_incremental_updates}`);

  if (result.has_incremental_updates) {
    console.log(`Modification markers: ${result.modification_markers.join(', ')}`);
  }
}

For a Python integration example see the PDF tamper detection Python tutorial. The xref fields are useful beyond routing the status verdict. A document review system might log xref_count and update_chain_length to an audit trail even for intact documents, for later investigation if the document is disputed.

The limits of xref forensics

xref chain analysis does not catch every modification. Two scenarios defeat it entirely.

Full file reconstruction. Tools like Ghostscript, qpdf, and print to PDF workflows flatten the entire file into a fresh single-revision PDF. The output has one xref, no /Prev pointers, and a clean structure — because it is a genuinely new file. If someone edits a PDF then runs it through Ghostscript or prints-to-PDF before submitting it, the xref chain is gone. The modification happened; the structural evidence did not survive.

Editing the source, not the PDF. If a fraudster has the original Word document and changes a salary figure there before exporting to PDF, no PDF editing ever occurred. The resulting PDF has a single xref, consistent timestamps, and a Producer that legitimately says “Microsoft Word.” Structural analysis has nothing to work with. This is precisely why inconclusive exists as a verdict class — and why consumer-software-origin documents are treated differently from institutional ones in high-stakes workflows.

These are real gaps. HTPBE documents them honestly because workflows built on a false sense of completeness fail in production. The xref chain catches the majority of real-world document fraud, which uses standard PDF editors rather than sophisticated reconstruction pipelines. Against a skilled adversary who understands PDF internals, additional signals — content analysis, issuer fraud detection, document provenance controls — are necessary. The PDF forensics without the original file article covers which signals remain available when there is no reference copy to compare against.

What xref forensics catches in practice

The gap between “what it misses in theory” and “what it misses in practice” is significant.

Real document fraud in lending, HR, insurance, and legal workflows is overwhelmingly performed with off-the-shelf tools: iLovePDF, Adobe Acrobat, PDF-XChange Editor, Foxit. These tools all produce incremental updates by default. A fraudster who edits a bank statement balance in iLovePDF and submits it is not going to run the result through Ghostscript afterward — they do not know what Ghostscript is.

In these cases, the xref chain is unambiguous: xref_count: 2, update_chain_length: 1, has_incremental_updates: true, modification_markers: ["HTPBE_MULTIPLE_REVISION_LAYERS"]. The file was opened, edited, and saved with a consumer PDF editor. That is the fact the structure records.

For developers building document intake pipelines, the xref chain is the most reliable structural signal to expose in an audit log. It does not require interpretation. xref_count: 1 means the file has lived one life. xref_count: 3 means it has had three.

Integrating PDF xref forensics into a document intake pipeline

If you are building a document intake workflow — loan applications, payslip checks, insurance claims, contract review — xref forensics belongs in the pipeline at the point of ingestion. Not as the final word on a document’s authenticity, but as a fast, structural first pass that catches the common cases and surfaces anomalies for human review.

The HTPBE PDF forensics API exposes xref_count, has_incremental_updates, update_chain_length, and the full modification_markers array in every analysis response. Pricing starts at $15/month for 30 checks. You can start testing with a free test API key against synthetic documents before touching production data — no sales call, no approval queue.