whatsinmypdf/whatsinmypdf

GitHub: whatsinmypdf/whatsinmypdf

一款完全在浏览器端本地运行的 PDF 隐藏内容扫描工具,检测不可见文本、隐藏层、嵌入文件及提示注入,文件永不上传。

Stars: 0 | Forks: 0

# WhatsInMyPDF 完全在你的浏览器中查找 PDF 中的隐藏内容 —— 白底白字文本、不可见的渲染模式、微小字体、隐藏层、嵌入的文件/JavaScript,以及针对 AI 审阅者的提示注入措辞。 在线访问:[whatsinmypdf.com](https://whatsinmypdf.com)。推送到 `main` 分支的代码在测试套件通过后,会通过 GitHub Actions 自动部署。 ## 隐私模型 扫描 100% 在本地运行。PDF 由编译为 WebAssembly 的 [mupdf](https://www.npmjs.com/package/mupdf) 解析和分析,在访问者自己的浏览器中的 Web Worker 内运行。文件永远不会上传到任何服务器,扫描结果也不会发送到任何地方。 这不仅仅是文案中的声明:`tests/e2e/scan.spec.ts` 包含一个测试(“没有任何网络请求会携带 PDF,且 WASM 仅在请求扫描后加载”),该测试对扫描期间页面的实际网络流量进行断言。如果未来的更改意外引入了上传功能,该测试将会失败。 该站点不附带任何类型的数据分析或跟踪脚本,也不允许任何第三方运行此类脚本:`public/_headers` 中的 CSP 仅允许来自此源点的脚本。Cloudflare Web Analytics 曾通过区域级设置在边缘节点被短暂注入 —— 该设置现已关闭,并且 `tests/smoke/prod.spec.ts` 断言已部署站点上的实时扫描除了其自身源点外不联系任何主机。 ## 许可证 AGPL-3.0-or-later。这是强制要求的,因为扫描器捆绑了 mupdf 的 WASM 构建版本,而 mupdf 本身基于 AGPL 许可证 —— 在其之上构建的任何产品(包括本站点)都必须在兼容的 copyleft 许可证下分发。请参阅 `LICENSE`。 ## 与参考扫描器的关系 `src/lib/scanner/`(`detect.ts`、`patterns.ts`、`mupdfAdapter.ts`)中的检测逻辑是 Python 参考实现的 TypeScript/WASM 移植版:即本项目衍生自的本地基于 PyMuPDF 的扫描脚本(`scan_pdf.py`)。十个发现类别、阈值和注入模式列表有意与该脚本保持一致,以便两个实现对相同的输入文件得出相同的结果。`tests/fixtures/EXPECTED.md` 记录了少数无法实现完全一致的地方(例如 `offpage.pdf`)及其原因,这些是根据参考扫描器的实际输出而非原始计划的假设进行验证的。 其中一处差异是刻意为之。这里的 `near_white_text` 还会查看文本*背后*绘制的内容:对于包含近乎白色文本片段的页面,适配器会渲染该页面并测量每个片段下方像素的暗度,如果片段位于明显较暗的背景上,则不会被报告。参考脚本仅比较文本颜色,这意味着它会将每个深底白字的表单标题、表头行和图表标签都标记为隐藏文本。在包含 48 份真实论文和政府表单的语料库中,这占了全部 170 个近乎白色文本的发现;通过背景检查,仍有 2 份文档会报告,其中一份包含真正不可见的文本(IRS 出版物中 0.01pt 的白色片段)。 渲染是有界限的:仅渲染包含近乎白色文本的页面,每个文档最多 40 页,并且任何失败都会导致片段未测量并按原样报告。有关测量工具,请参阅 `tests/sweep/`。 第二处差异涉及页面外文本。PDF 引擎将文本提取限制在 CropBox 内,因此停放在可见页面之外的文本永远不会到达检测器 —— 包括注入模式,这就是为什么参考扫描器对 `tests/fixtures/offpage.pdf` 报告没有任何发现,即使该文件包含注入短语。适配器在提取之前将 CropBox 扩大到 MediaBox,并通过页面变换(因此可以处理旋转和非零裁剪原点的情况)映射原始裁剪区域,以决定哪些片段位于页面之外。超出 MediaBox 的文本仍然无法触及:那是在纸张之外,而不仅仅是被裁剪掉。 ## 开发命令 ``` pnpm install pnpm dev # local dev server pnpm build # static build to dist/ pnpm preview # serve the built dist/ locally pnpm test # vitest unit tests (all passing) pnpm e2e # playwright e2e tests against a local build (all passing) pnpm smoke # playwright tests against the live site, run after every deploy pnpm sweep # false-positive measurement; needs CORPUS_DIR (see below) ``` ## 重新生成测试夹具 `tests/fixtures/` 下的 PDF 夹具是生成的,而不是手工编写的: ``` uv run --with pymupdf python scripts/make_fixtures.py ``` 每个夹具的预期发现计数都通过对相同生成的文件运行参考 Python 扫描器并比较输出来进行交叉验证;有关完整的交叉验证表以及单元测试断言所依据的真实计数,请参阅 `tests/fixtures/EXPECTED.md`。 夹具在计数上是稳定的(重新生成会产生具有相同发现和相同分类计数的 PDF),但在字节上不可复现 —— PyMuPDF 在每个保存的 PDF 中嵌入 `CreationDate` 时间戳,因此即使内容和检测到的发现没有改变,不同运行之间的文件字节(和哈希值)也会不同。 ## 针对真实文档测量误报情况 每个夹具都是为了触发检测器而构建的,这证明检测器能够启动,但对于它们在没有人刻意设计被捕获的文档上触发的频率却毫无说明。这第二个数字决定了访问者的首次扫描体验到的是一款有用的工具还是一个误报的烟雾报警器,因此它是被测量出来的,而不是假设出来的: ``` uv run python scripts/fetch_corpus.py /tmp/corpus # ~48 real PDFs, a few minutes CORPUS_DIR=/tmp/corpus pnpm sweep ``` 语料库未提交到仓库(包含他人的文档,体积达数十兆字节);脚本从公开来源重建等效的语料库 —— 跨越七个学科的最新 arXiv 预印本、四份 IRS 表单和一份 RFC。arXiv 被查询以获取最新提交的内容,因此重新运行复现的是该方法,而不是完全相同的文件。 `pnpm sweep` 打印出分类命中率和所有发现,并且仅断言一件事:没有任何真实文档导致扫描器崩溃报错。它是一种测量手段,而不是一道关卡,并且它不是 `pnpm test` 的一部分 —— 因为它需要不在仓库中的语料库。 在 48 份文档的语料库 (2026-07-28) 上:48 份中有 31 份是干净的,16 份文件出现 `tiny_font`(比例缩小的图表,这就是为什么该类别被标记为高误报风险并在报告中折叠的原因),2 份出现 `near_white_text`(其中一份是真正不可见的文本 —— IRS 出版物中 0.01pt 的白色片段),2 份出现 `embedded_files`(一份携带自身 XML 源码的 RFC,一份携带 Distiller 设置的 IRS 出版物),而其他八个类别完全没有发现。 `public/demo/` 下的两个“尝试示例”演示文件是以相同方式生成的 —— `uv run --with pymupdf python scripts/make_demo_pdfs.py` —— 并且该脚本同样是幂等的,会跳过任何已存在的文件。
标签:AI工具, PDF分析工具, WebAssembly, 云安全监控, 前端, 提示词注入检测, 本地隐私计算, 特征检测, 自动化攻击, 静态分析