Document security / Who can reach the document
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.
The exposure
In a multi-tenant system every document belongs to a tenant, and every query that omits the tenant is a potential cross-tenant disclosure. The risk is concentrated in document systems because the artefacts are complete and quotable: a leaked API response is a fragment, and a leaked document is a customer's invoice with their name, their addresses, their prices and their volumes on it. The failure mode is almost always a query missing a filter rather than an authorisation system being wrong.
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.
- Put the tenant in every query as a matter of structure rather than discipline, so omitting it is impossible rather than merely discouraged.
- Scope the storage path by tenant, so a mistake in application code cannot reach across the boundary in the storage layer as well.
- Use a separate API key per tenant where the volume justifies it, which also makes usage attributable and revocation surgical.
- Test cross-tenant access explicitly and continuously. One test per read path, asserting that tenant A gets a 404 for tenant B's document.
- Include the tenant in the audit record, so a disclosure can be scoped afterwards rather than guessed at.
- Be careful with batch operations, which are where a loop over the wrong collection sends everybody the same thing or sends each thing to the wrong recipient.
In practice
A fragment, with the thing that goes wrong kept in a comment where it is the more instructive half.
// Structural, not disciplinary: the tenant cannot be omitted.
class TenantScopedDocuments {
constructor(private tenantId: string) {}
find(id: string) {
return db.documents.findOne({ id, tenantId: this.tenantId });
}
list(filter = {}) {
return db.documents.find({ ...filter, tenantId: this.tenantId });
}
storageKey(documentId: string) {
return `tenants/${this.tenantId}/documents/${documentId}.pdf`;
}
}
// One per read path, and they run continuously.
test("tenant A cannot read tenant B's document", async () => {
const b = await asTenant("B").createInvoice();
const res = await request(app)
.get(`/documents/${b.documentId}`)
.set("Authorization", tokenFor("A"));
expect(res.status).toBe(404); // not 403: do not confirm existence
});
/* The place this actually goes wrong is a batch: a loop that iterates
one tenant's recipients and one tenant's documents from different
queries, one of which lost its filter. Everybody receives a document
and some of them receive somebody else's. */What people do instead
Relying on every developer remembering the tenant filter. It works until one query is written in a hurry, and the resulting defect is a cross-customer disclosure rather than a bug. Make the scoping structural so the filter cannot be left out.
How this is found out
By a customer reporting that they received somebody else's document, which is the worst possible discovery channel because it is also a disclosure notification. Continuous negative tests are the only realistic way to find it first.
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.
Responding when a document goes to the wrong recipient
The response to the most common document incident, where the technical options are limited and the decisions are mostly about scope and disclosure.
Recording who generated, accessed and shared each document
The record of what happened to a document and who did it, which is asked for after an incident and cannot be reconstructed then.
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.
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.
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.