ContractPal handles people's private paperwork. If you find a way to break the guarantees below, please report it.
Open a private security advisory on this repository. Please do not open a public issue for a vulnerability.
Include what you did, what happened, and what you expected. A proof of concept helps. You will get an acknowledgement within a few days.
These are the properties worth attacking. Each is enforced in code, and a break in any of them is a security bug rather than a feature request:
| Guarantee | Enforced by |
|---|---|
| An uploaded document is never persisted or logged | npm run audit:retention, which fails on any filesystem write, datastore client, object-storage call, localStorage use, or document identifier inside a console call |
| The page cannot send a document to a third-party origin | connect-src 'self' and form-action 'self' in the CSP (next.config.ts) |
| Upload type is decided by content, not by the client | magic-byte sniffing in lib/upload.ts; filename and Content-Type are untrusted |
| A token cannot choose its own algorithm | the algorithm is pinned on verify in lib/entitlement.ts; an alg: none token is refused, and a test asserts it |
| A recovery link cannot be used as a scan credential | recovery tokens carry a different audience (contractpal:recovery) from entitlement tokens (contractpal:scan); asserted in both directions |
| The recovery endpoint cannot enumerate subscribers | /api/entitlement/magic-link always answers 202 with an identical body, including when the send fails |
| Recovery links cannot be printed into a production log | the development mailer refuses to run under NODE_ENV=production, and the registry refuses to hand it out there |
These are deliberate, documented trade-offs rather than oversights. Reports that restate them are welcome but already known:
- The free-scan cookie is forgeable.
cp_free=1is not signed. Clearing or forging it grants one extra document analysis. The cost of abuse is a single model call; signing it would not stop the simpler attack of just clearing it. - Recovery links are replayable within their 15-minute window. With no
datastore there is nowhere to mark a link consumed. Mailbox access is the
recovery factor by design, and the blast radius is a subscription that
address already paid for. See
docs/okf/decisions/adr-0009-mail-seam.md. - Entitlement survives to token expiry after cancellation — at most one
billing period plus a two-day grace. The Stripe webhook that would allow
early revocation was deliberately not built; see
docs/okf/apis/stripe-webhook.md. - Rate limiting is per-instance and in-memory. On serverless it resets on a cold start. It blunts a script, it is not a quota.
script-srcallows'unsafe-inline'for Next's bootstrap. Tightening it needs a per-request nonce from middleware.'unsafe-eval'is development-only and is proven absent from the production policy.
The AI provider's own handling of a document is governed by that provider's
terms, not by this codebase. What each supported provider commits to is
recorded in its adapter's retention field and reflected on the privacy page.