尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Inspect suspicious PDFs. Keep your files local.
A browser-only PDF security triage tool: a verified TypeScript reimplementation of Didier Stevens' PDFiD, plus a deep analysis that finds what keyword scanners miss, all running locally in your browser.
Phishing and malware campaigns still use PDFs that run JavaScript when opened, launch programs, carry executable attachments or leak Windows credentials through network-share links. Analysts triage such files with PDFiD, but uploading a suspicious (and often confidential) document to an online scanner is not always acceptable.
PDF Signal does that triage entirely in the browser: no upload, no server, no telemetry. It explains what it found in plain language, separates raw measurements from interpretation, and never claims a file is "safe".
[!IMPORTANT] PDF Signal is a static triage tool, not an antivirus product. A finding is a reason to look closer, not proof of malware, and a clean result is not proof of safety.
| 🔍 PDFiD-compatible scan | All 20 PDFiD keywords plus /Colors > 2^24, hex-escape (obfuscation) counts, header and %%EOF checks, entropy, dates and names. Matches pdfid.py 0.2.10 field by field on 50 synthetic fixtures and 400 real PDFs, in two modes. |
| 🧬 Deep analysis | Decompresses object streams, where modern PDFs and evasive malware keep their dictionaries, then resolves what /OpenAction, /AA events, links and document-level scripts actually do. |
| 🧪 Script inspection | Checks JavaScript (including XFA forms and encoded streams) for heap-spray data, shellcode-style escapes, eval of decoded data and APIs from known Acrobat exploits, without ever running it. |
| 📎 Attachments and links | Flags executable attachments (including invoice.pdf.exe and direction-override tricks), archives, macro-capable Office files, network-share links and javascript: URLs. |
| 🚨 Clear verdicts | Five evidence-based levels, from "No configured suspicious indicators detected" to "High-risk indicators detected", each with a "What to do" next step, and a red warning (orange when not high risk) for anything that can run code. |
| 🎯 Fewer false alarms | Keywords that PDFiD counts inside image, font or text data, but that the document never uses, are shown as context instead of raising the verdict. |
| 🔒 Private by design | Runs in a Web Worker; strict CSP (connect-src 'none', Trusted Types); no storage, analytics or third-party code. |
| 📄 JSON report | Generated locally; contains counts, findings and limitations but never document text, scripts, URLs or file paths. |
| ♿ Accessible | Keyboard operable, screen-reader announcements, light and dark themes, responsive down to phone widths, print-friendly results. |
All screenshots are taken from the production build, scanning the harmless built-in samples.
| Start page | Clean result |
|---|---|
| Keyword counts with danger rows | Resolved actions and script patterns |
Files without a %PDF header are not scanned unless you choose Scan anyway (forced mode),
the equivalent of pdfid.py --force.
flowchart LR
A["PDF file<br/>(chosen or dropped)"] --> B{"Validation<br/>main thread"}
B -- "size limit,<br/>%PDF signature" --> C["Web Worker<br/>(new for each scan)"]
C --> D["PDFiD-compatible<br/>keyword scan"]
C --> E["Deep analysis<br/>object streams · actions<br/>scripts · attachments"]
D --> F["Deterministic<br/>triage rules"]
E --> F
F --> G["Results<br/>(rendered as text)"]
F --> H["JSON report<br/>(local download)"]
Validation checks the 10 MB size limit and looks for the %PDF signature in the first
1024 bytes, treating the file name and MIME type as untrusted.
The worker reads the bytes, computes a SHA-256 hash and runs two independent analyses:
/OpenAction, /AA, link and name-tree actions, and inspects
scripts, attachments and link targets as text.Every message back to the page is validated at runtime, and the worker is terminated afterwards, which also makes cancellation immediate.
The assessment applies documented, deterministic rules to both results and explains each finding, kept separate from the raw counts. It is not a machine learning model and its level is not a probability.
| Level | Examples |
|---|---|
| High risk | JavaScript that runs on open (/OpenAction, page-open events) · automatic or executable /Launch · exploit-style script code · executable attachments · automatic network-share access |
| Priority | Document-level scripts · hex-obfuscated keywords (/J#61vaScript) · network-share or javascript: links · decompression bombs |
| Review | /JS, /JavaScript, /AA, /Launch, /EmbeddedFile, /RichMedia, /XFA, /JBIG2Decode · automatic web or file actions · archives and macro documents · indicators hidden in object streams · structural anomalies (data before the header or after %%EOF, unbalanced objects) |
| Context | Forms, object streams, encryption, open actions that only navigate, incremental updates, keywords that occur only inside stream data |
Every level comes with a "What to do" recommendation. Entries that can run code are highlighted in red when the verdict is high risk and in orange otherwise, so the colours always agree with the verdict.
The deep analysis is hardened against evasion: it analyses every definition of an object (so decoys and incremental updates cannot hide the real one), reads encryption only from the trailer, keeps going past malformed objects, and reports whatever it could not inspect. If it is defeated, the PDFiD keyword heuristics still apply. Keywords are only downgraded to context when the deep analysis completed and inspected everything.
The full rule set, the highlighting rules and the differences from PDFiD's triage plugin are in docs/triage-rules.md.
innerHTML, eval or dynamic code anywhere, and Trusted Types enforce
it in the browser.Details: SECURITY.md (threat model, CSP, GitHub Pages limitations, reporting a vulnerability) and PRIVACY.md.
The keyword engine reproduces pdfid.py 0.2.10's tokenizer exactly, including its quirks (for
example /JS_x counts as /JS, and keywords inside streams, strings and comments count).
scripts/pdfid_reference.py runs the real pdfid.py on every synthetic fixture. Its output is
committed and checked on every test run, and a CI job re-runs it live against the pinned
upstream file (SHA-256 verified):
mkdir -p .pdfid && curl -fsSL -o .pdfid/pdfid.py \
https://raw.githubusercontent.com/DidierStevens/DidierStevensSuite/104bb4fe77e24f0891a954af349449227d06f77d/pdfid.py
PDFID_PY="$PWD/.pdfid/pdfid.py" npm run test:parity
| Check | Result |
|---|---|
| 50 synthetic fixtures × 2 modes | All 100 comparisons agree: 95 match exactly; in 5, pdfid.py itself crashes on malformed input |
| 400 real PDFs × 2 modes (local, once) | All 800 comparisons agree (files not included in the repository) |
The deep analysis never changes these counts. See docs/pdfid-compatibility.md for the source study, the method, the real-world check and every known difference.
Requires Node.js 22.12 or later (CI uses Node.js 24).
git clone <repository-url> pdf-signal
cd pdf-signal
npm ci
npm run dev # http://localhost:5173
| Command | Purpose |
|---|---|
npm run dev | Development server with hot reload |
npm run build | Type-check and build the static site into dist/ |
npm run preview | Serve the production build locally |
npm test | Run all tests once |
npm run test:watch | Run tests in watch mode |
npm run test:coverage | Tests with coverage thresholds |
npm run test:parity | Compare with the real pdfid.py (set PDFID_PY; see above) |
npm run lint / npm run typecheck / npm run format:check | Static checks |
The Content Security Policy is added only to the production build, so check security-related
changes with npm run build && npm run preview.
PDFID_PY is set: unit tests for every engine module, integration tests through the real
worker handler, component and full-flow tests in jsdom, and the recorded pdfid.py parity suite.eval,
embedded viewers, inline styles, remote URLs, a weakened CSP, extra runtime dependencies or
hidden Unicode (Trojan Source) characters.noUncheckedIndexedAccess,
exactOptionalPropertyTypes) and type-aware ESLint with zero warnings.main and on pull requests, plus the live pdfid.py comparison.| Area | Choice |
|---|---|
| Interface | React 19, TypeScript 6 (strict), plain CSS with design tokens, light and dark themes |
| Build | Vite 8, static output with a relative base path |
| Scanning | Web Worker; Web Crypto (SHA-256); DecompressionStream for Flate; no PDF libraries |
| Testing | Vitest 5, React Testing Library, jsdom, V8 coverage, Python 3 for the pdfid.py reference |
| Code quality | ESLint 10 with typescript-eslint, Prettier |
| CI and hosting | GitHub Actions (SHA-pinned actions), GitHub Pages or any static host |
The only runtime dependencies are react and react-dom.
src/
├── app/ App shell and styles
├── components/ layout · upload (drop zone, samples) · scanner · results · icons
├── config/ size and time limits
├── features/
│ ├── pdf-analysis/
│ │ ├── engine/ PDFiD-compatible scan · deep analysis (objects, filters, scripts)
│ │ ├── worker/ Web Worker, message protocol and client
│ │ ├── assessment/ triage rules and explanations
│ │ ├── validation/ file checks and name sanitizing
│ │ ├── reports/ JSON report export
│ │ ├── hooks/ upload → scan → assess workflow
│ │ └── types/ shared result types
│ └── samples/ harmless sample PDFs generated in the browser
├── security/ Trusted Types policy
└── styles/ design tokens, global and accessibility styles
tests/ unit · integration · components · parity · fixtures
scripts/pdfid_reference.py runs pdfid.py for the parity tests
docs/ PDFiD study, triage rules, screenshots
The build is a static site with a relative base path, so dist/ works at
https://<user>.github.io/<repository>/, at a domain root or in any subfolder.
PAGES_ENABLED to true to deploy on every push to main.No secrets are needed. GitHub Pages cannot send custom security headers, so the CSP is delivered
by a <meta> element; see SECURITY.md for the consequences. Any other static
host works too: upload the contents of dist/.
PDF Signal is released under the MIT licence. It is an independent implementation inspired by, and verified against, PDFiD by Didier Stevens, whose source code is in the public domain. No pdfid.py code is included, and PDF Signal is not affiliated with or endorsed by its author.