PDFPipe

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.

js
// 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.

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.