Document security / Who can reach the document
Access control on documents you stored rather than streamed
The authorisation model for documents that persist somewhere after generation, which is a longer-lived and larger surface than the render call itself.
The exposure
Storing a document converts a momentary artefact into a durable one with its own access story. The render call was authorised once, by a key, at a moment. The stored document is reachable for as long as it is retained, potentially by anyone who can reach the storage, and the authorisation that produced it does not automatically govern retrieval. Systems routinely get the render authorisation right and then leave the storage reachable by anything that can guess or enumerate an identifier.
The decisions
Reasons rather than a description of the code. Each one has a default that is fine for a page and wrong for a file somebody keeps.
- Bind a stored document to an owner in your own records at the moment it is stored, so retrieval has something to check against.
- Check that binding on every retrieval, the same as any other document, rather than treating storage as an internal detail that has already been authorised.
- Keep the storage identifier separate from anything user-facing and unguessable, so a leaked reference in one place is not a key to the store.
- Know the retention: the render response carries document_expires when a document is stored, and a system that does not record it cannot tell a user when access ends.
- Treat the ability to mint a retrieval link as a privileged action, because it converts an internally reachable document into an externally reachable one.
- Review who and what can read the store directly. A backup process, an analytics job or a support tool with blanket read access is part of this surface.
In practice
A fragment, with the thing that goes wrong kept in a comment where it is the more instructive half.
// Bind at store time, so retrieval has something to check.
const res = await pdf.post("/v1/pdf", {
html,
filename: filenameFor(invoice),
store: true,
});
await db.documents.create({
documentId: res.document_id,
expiresAt: res.document_expires, // record it, or nobody can tell a user
ownerId: invoice.customerId, // the binding retrieval checks
kind: "invoice",
subjectId: invoice.id,
});
// Every retrieval checks the binding. Storage is not "already authorised".
async function retrieve(documentId, user) {
const doc = await db.documents.findOne({ documentId, ownerId: user.id });
if (!doc) throw new NotFound();
if (doc.expiresAt && new Date(doc.expiresAt) < new Date()) throw new Gone();
return pdf.get(`/v1/documents/${documentId}`);
}
/* Minting a retrieval link is a privileged action: it turns an
internally reachable document into an externally reachable one.
Authorise it like a share, not like a read.
And review the direct readers of the store: a backup job, an
analytics pipeline or a support tool with blanket access is part of
this surface and is usually not in the threat model anybody drew. */What people do instead
Treating stored documents as internal because they were produced by an authorised call. The call was authorised; the object is now a durable thing with its own lifetime, and the render's authorisation has no bearing on who reads it tomorrow.
How this is found out
By enumeration, usually by somebody curious rather than malicious, or by an audit that asks who can read the bucket. Rarely by monitoring, because reads look like reads.
Frequently asked
Does this page tell me what the law requires?
No, and deliberately not. Retention periods, erasure obligations and residency rules vary by jurisdiction, by industry and by the kind of document, and they change. What these pages describe is the shape of the problem and the mechanisms a system needs in order to implement whatever answer your own advisers give you. Where a genuine tension exists, such as an immutable record against a right to erasure, it is named as a tension rather than resolved.
Why is so much of this about the contents rather than access control?
Because access control decides who can obtain a copy and has no opinion at all about what happens to the copy. Once a document is on somebody's laptop, forwarded to a colleague or printed, every control listed here has already stopped applying to it. What is inside the file is therefore the part that keeps mattering, which is the opposite of the balance you would strike for a page.
How much of this applies at a small volume?
Most of it, because these are decisions rather than infrastructure. Redacting by omitting rather than covering costs nothing. Deciding what goes on a template costs one review. Classification is one object in code. Legal hold and an audit trail are the two that take real work, and both are far cheaper to build before they are requested than under the deadline that comes with the request.
Related security topics
The decisions that depend on each other, then the rest of the same group.
Serving a generated document behind proper access control
Making sure the person downloading a document is entitled to it, which fails most often because the document lives somewhere the check does not.
Signed URLs for documents, and choosing an expiry that fits
Giving out a URL that carries its own authorisation for a limited time, which moves the access check to the moment the link is minted rather than the moment it is used.
Deciding how long to keep a document, and proving it was deleted
The period a document remains available and the mechanism that removes it afterwards, where the second half is the one that is usually missing.
What belongs in a document filename, and what must not
The name the recipient sees, which travels further than the document's contents and is visible in places the contents are not.
Serving a document so the browser treats it as a document
The headers that decide whether a file downloads, opens in a viewer, or is interpreted as something else entirely by a browser guessing at its type.
Every document security topic
The full list, grouped by access, contents and what happens afterwards.
What this API actually does
The options and endpoints these decisions are built on, one page each.
Most of these are decisions rather than features, and the cheapest time to make them is before the first document is delivered rather than after one reaches the wrong person.