PDFPipe

Document security / Who can reach the document

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.

The exposure

A signed URL is a bearer token in a URL bar. That makes it convenient, because it works in an email, in a browser with no session and in a third-party viewer, and it makes it dangerous for the same reason: anyone holding the URL has the document. URLs leak in ways tokens in headers do not, through referrer headers, browser history, chat logs, shared screens and support tickets, so the expiry is not a formality. It is the only thing limiting the damage of a leak.

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.

  • Perform the authorisation when minting the link, not when redeeming it, and mint it only for a user who was entitled at that moment.
  • Set the shortest expiry the workflow tolerates. A link in an email that a person clicks within the hour does not need to work for a week.
  • Prefer minting on demand behind an authenticated action over embedding a long-lived link in a message that persists.
  • Know what the signature covers. The retrieval token here is an HMAC over the key, the document and the expiry, which means it cannot be altered to point at another document or to extend its life.
  • Remember an expiry is not a revocation. If a document must become unreachable before its links expire, that needs a check at redemption time or the document removed.
  • Log the minting rather than the redemption if you can only log one, because who was given access is the more useful record.

In practice

A fragment, with the thing that goes wrong kept in a comment where it is the more instructive half.

js
// Mint after the authorisation check, with the shortest workable ttl.
app.post("/documents/:id/share", requireAuth, async (req, res) => {
  const doc = await db.documents.findOne({
    id: req.params.id,
    ownerId: req.user.id,               // the check happens here
  });
  if (!doc) return res.sendStatus(404);

  const link = await pdf.post(`/v1/documents/${doc.documentId}/link`, {
    ttl: 900,                           // seconds; 15 minutes, not a week
  });

  // Who was given access is the more useful record.
  await audit.record({
    action: "document.link_minted",
    documentId: doc.documentId,
    by: req.user.id,
    expiresAt: link.expires_at,
  });

  res.json({ url: link.url, expiresAt: link.expires_at });
});

/* What the signature covers, which is what makes it safe to hand out:
   an HMAC over the key, the document and the expiry. A recipient cannot
   edit the URL to reach a different document or to extend its life,
   because either change invalidates the signature.

   What it does not do: revoke. An expiry is a maximum lifetime, not a
   switch. If a document must become unreachable now, remove the
   document or add a check at redemption; shortening future links does
   nothing to the ones already issued.                                 */

What people do instead

Embedding a long-lived signed link directly in an email so the recipient can click through without logging in. The email is stored on servers, forwarded, and searchable for years, and the link inside it is a working credential for as long as its expiry allows.

How this is found out

A leaked link is usually discovered when a document appears somewhere it should not, or during an audit that finds a URL in a chat log. Neither is fast, which is why the expiry rather than the detection is the control.

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.