Document security / Who can reach the document
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.
The exposure
How a browser treats a served file depends on headers rather than on the bytes, and a wrong or missing header produces two distinct problems. The mild one is behavioural: the document opens inline when the user wanted a download, or downloads when they wanted to read it. The serious one is that a browser allowed to guess a content type can decide a file is HTML and render it, which turns a file-serving endpoint into a way of executing content in your origin. That matters most where any part of the file is influenced by a user.
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.
- Always set an explicit content type. A missing type invites the browser to guess, and guessing is the failure.
- Always send nosniff, which tells the browser to trust the type you declared rather than inspecting the bytes.
- Choose the disposition deliberately. Inline for something a person reads in place, attachment for something they keep. The API returns inline with a generic filename by default, so serving onward is where the decision is made.
- Quote the filename in the header and strip anything that could break out of it, because a filename with a newline in it is a header injection.
- Serve user-influenced files from a separate origin where the content type cannot be trusted at all, so a mistake is contained.
- Set a cache policy that matches the sensitivity. A document served with a public cache header can end up in a shared cache.
In practice
A fragment, with the thing that goes wrong kept in a comment where it is the more instructive half.
res.setHeader("Content-Type", "application/pdf");
// Trust the type I declared; do not inspect the bytes and decide.
res.setHeader("X-Content-Type-Options", "nosniff");
// Deliberate, not default. The render returns inline with a generic
// name; this is where the real decision is made.
res.setHeader(
"Content-Disposition",
`attachment; filename="${safeFilename(name)}"`,
);
// Sensitivity, not convenience.
res.setHeader("Cache-Control", "private, no-store");
function safeFilename(name) {
// A newline here is a header injection, not a cosmetic problem.
return name.replace(/[\r\n"\\]/g, "").slice(0, 200);
}
/* Why nosniff matters specifically: without it, a browser may inspect
the bytes, decide a file is HTML and render it in your origin. For a
document whose content is influenced by a user, that is script
execution rather than a display quirk.
Where any part of the file is user-influenced, serve it from a
separate origin so a mistake cannot reach your session cookies. */What people do instead
Relying on the file extension. Browsers key off headers, not the URL, so a path ending in .pdf served with no content type and no nosniff is exactly the case where sniffing decides what happens.
How this is found out
The behavioural half is reported by users immediately. The security half is found by a review or an assessment, because nothing about a correctly rendered document looks wrong in a log.
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.
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.
Rendering HTML or a URL that a user supplied, safely
What a renderer can be made to do when the thing it renders is chosen by somebody else, which is a server-side request forgery problem before it is a rendering problem.
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.
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.
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.