Document security / Who can reach the document
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.
The exposure
Application routes are protected carefully and file downloads frequently are not, because the file is served by different machinery: a storage bucket, a static path, a direct link. The classic failure is an authorisation check that proves the user is logged in without proving the document is theirs, so any authenticated user can fetch any document by changing a number in a URL. It is easy to write, it passes every test that uses one user, and it is one of the most common serious defects in document systems.
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.
- Check ownership, not authentication. Being logged in says who somebody is; it says nothing about whether this document is theirs.
- Do the check on every retrieval, not only on the page that links to the document. The link is not the control.
- Never make the identifier the control. A sequential or guessable identifier with no ownership check is an enumeration away from every document in the system.
- Use unguessable identifiers anyway, as defence in depth, but treat them as a second layer rather than the first.
- Serve through your own handler where the check lives, or use a signed URL minted after the check. A storage path that is reachable without passing through either is outside the system.
- Test the negative case explicitly: user A requesting user B's document should be a 404 rather than a 403, so the response does not confirm the document exists.
In practice
A fragment, with the thing that goes wrong kept in a comment where it is the more instructive half.
// The check that matters is ownership, and it runs on every retrieval.
app.get("/documents/:id", requireAuth, async (req, res) => {
const doc = await db.documents.findOne({
id: req.params.id,
ownerId: req.user.id, // ownership, not just authentication
});
// 404 rather than 403: a 403 confirms the document exists.
if (!doc) return res.sendStatus(404);
const bytes = await storage.read(doc.storageKey);
res.type("application/pdf").send(bytes);
});
/* The failure this prevents, which passes every single-user test:
const doc = await db.documents.findById(req.params.id); // no owner
res.send(await storage.read(doc.storageKey));
Logged in, so authorised, so any user can read any document by
changing the id.
And the other half: if the storage bucket is publicly readable, this
handler is decoration. The object has to be unreachable except
through a path that performs the check, or through a signed URL
minted after it. */What people do instead
Protecting the page that lists documents and leaving the download path open. The listing enumerates only your own documents, which makes the system look correct, and the download path accepts any identifier from anyone.
How this is found out
Almost never by monitoring, because every request is a well-formed authenticated request that returns 200. It is found by a security review, by a curious customer, or by an incident. That asymmetry is the argument for testing the negative case.
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.
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.
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.
Keeping one customer's documents away from another's
The controls that stop a multi-tenant document system delivering one organisation's document to another, which is the worst failure available to it.
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.