Document security / Who can reach the document
Keeping credentials out of a document template
The ways a credential ends up inside markup that is sent to a renderer, and why anything in that markup should be assumed to reach the document.
The exposure
Markup sent to a renderer is data in transit to a third system, and everything in it is fetched, rendered or embedded. A token in an asset URL is sent to whatever host that URL names. A key in a comment is in the payload and may survive into the output. An internal endpoint in a src attribute discloses your topology. None of this is exotic; it happens because a template is treated as code, where a secret is merely bad practice, rather than as payload, where it is disclosure.
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.
- Assume everything in the markup reaches the document or the network. Comments included: they are in the payload whether or not they render.
- Fetch protected assets yourself and inline them as data URIs, rather than giving the renderer a URL with a credential in it.
- Serve document assets from a public location with no authentication, which is usually correct: a logo and a stylesheet are not secrets and making them fetchable removes the temptation.
- Keep internal hostnames out of markup, since a document is a portable record of them and it goes to outsiders.
- Scan the assembled markup for credential patterns before sending it, as a guard rather than a design.
- Remember the same applies to the callback and webhook URLs: a secret in a query string is a secret in a log somewhere.
In practice
A fragment, with the thing that goes wrong kept in a comment where it is the more instructive half.
/* Everything in the markup is either fetched, rendered or embedded.
Comments are in the payload too. */
// Wrong: the token is sent to whatever host that URL names, and the
// URL may survive into the document.
// <img src="https://assets.internal/logo.png?token=abc123">
// <!-- build 4471, deploy key dpl_live_... -->
// <link rel="stylesheet" href="https://10.0.3.4/print.css">
// Right: fetch it yourself, inline the bytes.
const logo = await fetchWithAuth("https://assets.internal/logo.png");
const html = template({
logoDataUri: `data:image/png;base64,${logo.toString("base64")}`,
});
// Or better: serve document assets publicly. A logo is not a secret,
// and making it fetchable removes the reason anyone reached for a token.
// <img src="https://assets.example.com/logo.svg">
// A guard, not a design.
const CREDENTIAL_PATTERNS = [
/\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{16,}/,
/\bBearer\s+[A-Za-z0-9._-]{20,}/,
/\b(?:api[_-]?key|secret|password)\s*[=:]\s*\S{8,}/i,
];
function assertNoSecrets(html) {
for (const p of CREDENTIAL_PATTERNS) {
if (p.test(html)) throw new Error("credential pattern in document markup");
}
}What people do instead
Putting a token in an asset URL because the asset host requires authentication and inlining seemed like more work. The credential is then in the payload, in any logs of that payload, and in the fetch to the asset host, for the sake of avoiding a base64 encode.
How this is found out
By a secret scanner if the markup is ever written to a log or a fixture, and otherwise not at all. That is the argument for the guard: it turns an invisible exposure into a build failure.
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.
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.
What metadata a generated PDF carries, and how to check
The fields a document format carries alongside its visible content, which are invisible on the page and present in the file.
Rotating the API credential a document pipeline depends on
Replacing the key your pipeline authenticates with, without an outage, which is easy to plan and routinely never practised.
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.
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.