PDFPipe

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.

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

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.