Every free PDF tool on the internet leads with “your file never leaves your browser”. This one does not, because it cannot — and a page that dodged that would be the wrong page to put on a security site. So the honest version is first, and everything after it is the detail behind it.
Why this runs on a server
A browser-only checker is a real and good design, and it buys exactly one
property: the bytes stay on your machine. What it costs is depth. The
analysis here walks the cross-reference tables, replays page content
streams to recover paint order, decodes covered text through font
/ToUnicode maps, and compares a document against structural
baselines built from genuine issuer specimens. That is a parser with a
hard CPU budget and a kill switch, not something to run inside the tab of
whoever happens to have opened the page.
So the trade is deliberate: the file is sent over TLS, and in exchange the answer is one no in-browser metadata reader can produce. What we owe you for that trade is that the file's stay is as short and as contained as we can make it — which is the rest of this page.
If that trade is wrong for your document, do not make it. A file under seal, a live incident, an unredacted brief before filing — for those, the right tool is one that runs on your own machine, and today that is not this one. We would rather say so here than have you find out afterwards.
What happens to a document you upload
The whole lifetime of an uploaded file, in order:
- It arrives over TLS and is held in memory. There is no temporary file, no upload directory, no object store and no queue behind the endpoint.
-
Its type is decided by sniffing the bytes, never by the
Content-Typeheader or the filename — so a hostile caller cannot choose which parser runs on their input. -
The buffer is handed to a worker thread, one of two,
with a 15-second hard deadline. A file that exhausts it
has its worker killed and replaced, and the caller gets
422 analysis_timeout. When both workers are busy and the queue is full the answer is503 busyrather than an ever-growing latency tail. - The report is serialised and returned. The buffer is discarded. Nothing about the document survives the response — not the bytes, not the extracted text, not the filename.
- One ledger row is written, and it holds no document: a key id, an account id, a byte count, a risk band and a timestamp. That is what meters your quota.
Uploads are capped at 10 MB. Every other route on the origin is capped at 64 KB, so nothing except the one endpoint that needs headroom can be made to buffer megabytes before its handler runs.
Recovered text — the words found underneath a failed redaction — is returned to you, in the response, and stored nowhere. It is the finding, so withholding it would make the report useless; and keeping it would make this the most sensitive database in the product. It is neither logged nor persisted.
The privacy policy is the column-by-column version of what the database holds.
Where it runs
-
One server, in Finland. A Hetzner instance in Helsinki
(
hel1). Compute and the database are in the EU. - No inbound port is published. The origin is not reachable from the internet at all. Traffic arrives through a Cloudflare Tunnel, which makes an outbound connection from the box; there is no listening socket for anyone to scan, and no firewall rule standing between you and a mistake.
-
TLS everywhere. Terminated at Cloudflare's edge, carried
to the origin inside the tunnel. HTTP is redirected, and the canonical
host is
tamperlens.com. - The database is a single SQLite file on that box's disk, holding accounts, key hashes, sessions and the usage ledger. It has never held a document, because no code path writes one.
Application hardening
-
A real Content-Security-Policy.
default-src 'self'with no'unsafe-inline'anywhere on the site. Every page we author loads its script and its stylesheet as external files, and CI fails the build if an inline script or style appears undersrc/web. The single exception is/docs, where Swagger UI injects its own bootstrap and offers no way to configure it away; it is scoped to that one prefix, which renders no user content. -
Cross-origin uploads are refused. A browser may only
call
/inspectfrom our own pages. Requests with noOriginheader — curl, server-side SDKs — are unaffected, because they spend their own address rather than borrowing a visitor's. -
Rate limits keyed on the network, not the header.
Anonymous callers get 10 inspections an hour and 25 a day per IPv6
/64(one customer is one bucket, not 264 of them), under a deployment-wide daily ceiling. Keyed callers are metered by their monthly quota instead. -
Other headers:
Referrer-Policy: no-referrer,Cross-Origin-Opener-Policy: same-origin,X-Content-Type-Options: nosniff, HSTS in production, andframe-ancestors 'self'. - Encrypted PDFs are disclosed, not attacked. A password-protected file is reported as “not content-analysed” rather than scanned as ciphertext and reported as clean. We do not attempt to break protection on anything you send.
Accounts, keys and secrets
- API keys are stored as SHA-256 hashes. The key itself exists in our systems only for as long as it takes to show it to you once. That is also why we cannot resend it — rotation issues a new one and invalidates the old.
- Passwords are scrypt hashes with a per-account random salt. The plaintext is never stored and the hash is not reversible.
-
Sessions are
HttpOnly,SecureandSameSite=Lax, expire after 30 days, and are deleted on logout. A password reset invalidates every other session. - Card data never reaches us. Checkout and the billing portal are Stripe-hosted; we hold an opaque customer id and nothing else.
- One key, one tenant. Private issuer baselines resolve against the owning account only — there is no code path from one account's request to another's profile.
Availability and recovery
-
Deploys are gated and reversible. A push to
mainruns typecheck, build and the full test suite; only on green does a runner on the box rebuild, wait on the container's healthcheck, and roll back automatically if the new build is unhealthy. - Database backups every six hours, 48 snapshots retained (twelve days). They contain no documents, because the database does not.
- Uptime is probed externally every 15 minutes, and errors are reported to Sentry with the request path — never with a document.
Who else touches it
The complete list. None of them receives an uploaded document, because the document does not outlive the request that carried it.
| Provider | What it does | What it sees |
|---|---|---|
| Cloudflare | DNS, TLS, tunnel ingress, bot challenge | Request metadata and IP address, in transit |
| Hetzner | The server and its disk | The physical host; no application-level access |
| Stripe | Checkout, subscriptions, billing portal | Your billing details. We never see card data |
| Resend | Confirmation and password-reset email | Your email address and the message |
| Sentry | Error reporting | Stack traces and request paths |
| GitHub | Source, CI, deploy trigger | Nothing about you; no production data |
What we do not have
A security page that lists only strengths is marketing. These are the questions a procurement review will ask where the answer today is no.
- No SOC 2, ISO 27001 or HIPAA attestation. Tamperlens is a small, independently run product and has not been audited against any of them. If your process requires a certified vendor, this is not one yet.
- No multi-region redundancy. One server, one region. A hardware failure is downtime, not data loss — but it is downtime.
- No customer-managed keys and no on-premise build. There is currently no way to run the engine inside your own network.
- No penetration-test report to share. The hardening above is real and much of it is enforced by tests in CI; none of it has been reviewed by an external assessor.
-
Metadata forensics is evidence, not proof. A cleanly
regenerated PDF launders away byte-level signals, and
exiftool -all=strips every image identity finding. A match is strong evidence; an absence is none — and every finding says so in its own prose.
Reporting a vulnerability
Email [email protected] with enough detail to reproduce it. You will get a human reply. There is no bounty programme and no legal threat: report it in good faith, give us a reasonable window to fix it, and we will credit you if you want the credit.
Please do not run automated scanners against the production host. The rate limits above will simply refuse you, and the noise buries the report you were trying to make.