Skip to content

Senior Engineering Interview Handbook / Chapter 71

Security and Privacy

A boundary-first guide to threat modeling, authentication, authorization, input safety, SSRF, deserialization, secrets, encryption, least privilege, dependency risk, privacy, and auditability.

The forty-third invoice

An account administrator asks a billing service to export the quarter’s 42 invoices. The browser preview shows 42. The finished CSV contains 43.

The extra row belongs to another customer. The API checked the administrator’s session and counted the right records, but the background worker later queried by date without repeating the tenant constraint. Its database credential could read every tenant. The download link works for anyone who receives it. The audit record says only export complete. One invoice includes a billing contact whose deletion request was processed last month, but the export worker read a stale analytics copy.

No exotic cryptography failed. Ordinary components each did what their local code permitted. The harm appeared in the space between them: user and API, API and queue, worker and database, private bucket and public link, source record and derived copy.

Security is the work of controlling those crossings even when someone tries to abuse them. Privacy is the work of deciding whether personal or sensitive data should cross them at all, how long it may remain, and how its use can be accounted for. Both begin by making the system’s boundaries visible.

A security and privacy memory aid maps actors, entry points, trust boundaries, assets, and audit evidence to authentication, authorization, input safety, minimization, and common threats.
Attach each control to a particular actor, entry point, boundary, and asset. A control with no named crossing is usually only an intention.

We will repair the export by following the route that produced it. The same method works for an image importer, support console, webhook, password reset, search index, file upload, or administrative tool.

Begin with harm, then draw the route

“Encrypt the invoices” sounds responsible, but it does not yet describe a security requirement. Which disclosure would encryption prevent? Where will plaintext still exist? Which service can decrypt it? Could that service read a different tenant’s invoices? A named control becomes useful only after the harm and boundary are named.

For the export, the important assets include invoice contents, the tenant boundary, the administrator’s authority, the durable CSV, and the evidence of who created and downloaded it. The actors include the administrator, the API, the worker, support staff, storage service, and anyone who obtains a download link. The route is small enough to draw:

administrator -> browser -> export API -> queue -> worker -> invoice database
                                  |          |          |
                                  |          |          -> audit stream
                                  |          -> object storage -> download API
                                  -> job-status API

Now ask what changes at each arrow. The browser crosses into a public service. The API turns a user request into a machine job. The worker receives authority that may outlive the original session. The database contains all tenants. Object storage turns query results into a durable new copy. The download path releases that copy again.

A lightweight threat model can be written in plain language:

  • Name what can be disclosed, altered, destroyed, abused, or made unavailable.
  • Name every person, service, job, vendor, and attacker that can act.
  • Mark entry points and places where identity, tenant, network, service, or data assumptions change.
  • Describe a few credible harm paths through those boundaries.
  • Attach prevention, detection, and recovery to each path.
  • State what remains possible when a control fails or is misconfigured.

This is more useful than starting from a catalogue of vulnerabilities. A catalogue can remind you to look for SSRF or injection. The route tells you where either could cause harm in this system.

Identity does not carry every permission

Authentication establishes the caller’s identity. Authorization decides whether that identity may perform this action on this resource in this context. The export failed partly because the system treated the first fact as if it implied the second.

The public API must validate the administrator’s session: issuer or session owner, intended audience, signature where applicable, expiry, revocation, and any stronger authentication required for a high-impact export. Those checks still do not answer whether this administrator may export these invoices.

Make the authorization decision inspectable:

subject:   account-admin-217
action:    create_invoice_export
resource:  invoices for tenant-aster, 2026-Q2
context:   active membership, export role, ordinary support mode
decision:  allow
evidence:  policy version 38, request 8d41...

The resource scope should come from trusted membership and policy state, not from a tenant identifier supplied by the browser. Database reads should carry that scope as well:

select invoice_id, issued_at, total, currency, billing_name
from invoices
where tenant_id = :authorized_tenant
  and issued_at >= :start_at
  and issued_at < :end_at
  and visibility = 'customer';

Parameter binding prevents invoice filters from changing the structure of the query. The tenant and visibility predicates enforce the authorized resource. They solve different problems, and the service needs both.

The queued job must preserve the subject, action, authorized tenant scope, filter bounds, policy version, and correlation ID. The worker then repeats the decision against current policy before reading. If the administrator has lost the export role, the account has been suspended, or the requested scope no longer exists, the job fails closed. A queue message is evidence that work was requested; it is not permanent permission to do it.

Apply the same reasoning to every release path. A single-invoice read, list, search result, CSV export, background job, support view, cached response, and download link can expose the same asset. Protecting one endpoint does not protect the resource.

Sessions, API keys, refresh tokens, signed URLs, and service identities are authority carriers. Their lifetime, audience, scope, storage, revocation, and replay behavior determine how far stolen authority travels. Short lifetime reduces an exposure window; it does not replace revocation after account takeover, offboarding, key compromise, or privilege reduction. A machine token may live longer when it is narrowly scoped, monitored, and safely rotatable. The design should say why that trade is acceptable.

Every export creates another data lifecycle

The CSV is not merely a different representation of the invoices. It is a new copy with its own readers, retention clock, deletion behavior, storage policy, and leak paths.

Begin by challenging its fields. The export may need invoice number, issue date, total, currency, and billing name. It probably does not need an internal fraud note, full payment instrument, support comment, password-reset token, or the free-form payload captured during an old debugging session. Data that never enters the file cannot leak from the file.

Then follow each retained field. Does it enter the queue message, worker log, metric label, trace, analytics event, temporary disk, object name, support tool, search index, backup, or third-party scanner? At each copy, prefer the least revealing form that still performs the job: an internal identifier instead of an email address, a correlation ID instead of a payload, a provider token instead of payment data, a count instead of a list, or no copy at all.

This is data minimization as architecture. Collect less, but also move less, retain less, expose less, and derive carefully. Minimization reduces privacy harm and simplifies the system: fewer access paths need policy, fewer stores need deletion, and fewer logs need protection.

The stale billing contact in the forty-third invoice reveals a deletion design that stopped at the primary table. A complete lifecycle names the authoritative record and every derived representation. A deletion or restriction may require an event, tombstone, cache purge, search removal, analytics policy, export expiry, and reconciliation job. Backups may use bounded retention or deletion on restore rather than immediate mutation, but that behavior should be explicit and tested. “Deleted” is a promise about the system, not one SQL statement.

Object storage for the CSV should be private, tenant-scoped, encrypted, and governed by a short retention policy. Prefer a download API that reauthorizes the actor before releasing the file. If a signed URL is necessary, constrain its lifetime and capability, avoid putting sensitive names in the object key, and treat possession of the link as possession of authority.

Untrusted data becomes dangerous at interpreters and fetchers

The export accepts dates, filters, sort choices, file names, and perhaps a logo URL. Invoice fields later enter SQL, CSV, HTML previews, logs, object keys, and spreadsheet software. A string is not safe or unsafe by itself. Its meaning depends on the parser or interpreter that receives it.

Use an API that keeps data separate from structure whenever possible:

  • bind values in database queries rather than assembling SQL;
  • call executables with argument arrays rather than building shell commands;
  • serialize JSON rather than concatenating it;
  • escape untrusted text for the exact HTML context in which it appears;
  • normalize file names under a controlled storage root;
  • keep trusted templates separate from user data;
  • neutralize spreadsheet formulas when untrusted fields are written to CSV.

Validation still matters for types, lengths, ranges, encodings, and permitted choices, but it cannot replace context-specific handling. A customer name may be valid text and still be unsafe when copied into an HTML attribute, shell command, SQL identifier, or spreadsheet formula.

Browser controls also have distinct jobs. Output escaping and a restrictive Content Security Policy reduce cross-site scripting risk. HttpOnly, Secure, and deliberate SameSite cookie settings constrain session exposure. CSRF defenses protect state-changing requests that a browser might send with ambient credentials. Narrow CORS rules control which origins may read responses. None of these makes client-side hiding an authorization policy; the server still owns every release decision.

Suppose the PDF version of the export fetches a customer logo from a supplied URL. That feature has turned the worker into a network client with the worker’s reach. An attacker may target loopback, link-local or private addresses, cloud metadata endpoints, internal DNS, or a redirect from an apparently safe host. For known integrations, allowlist destinations. Otherwise parse and resolve the destination, restrict schemes, reject prohibited address ranges, recheck after redirects and resolution, constrain method, headers, response size, content type, redirects, and time, and run the fetcher in a network segment that cannot reach sensitive services. Substring checks for localhost do not create a network boundary.

The queue message and uploaded attachments are untrusted too. Deserializing a format that can choose classes or invoke constructors may turn data into code or unbounded allocation. Prefer simple formats with schemas, explicit allowed types, size limits, and parsers that do not execute behavior. File processors need similar containment for decompression bombs, parser defects, executable content, path traversal, and metadata leaks. Give such workers little network, filesystem, and cloud authority so that a parser compromise has somewhere to stop.

Secrets and encryption need a named boundary

The export worker needs credentials for the queue, invoice store, object store, key service, and audit stream. One broad cloud role would be convenient. It would also let a flaw in logo fetching or file parsing reach far beyond the export.

Least privilege makes the worker’s task the unit of authority. It may consume only the export queue, read only the required invoice view, write only the export bucket prefix, request only the needed key operation, and append only to the audit stream. The database account should not mutate invoices. The object store role should not read unrelated buckets. A support tool should require a scoped, justified, time-bounded mode rather than permanent superuser access. These limits are blast-radius controls as much as preventive controls.

Secrets should live in controlled runtime storage, outside source code, images, logs, analytics, crash reports, and export files. Scope them by service, environment, and purpose where practical. Monitor unusual use. Design rotation and emergency revocation before exposure occurs; a secret that can be changed only by editing code and rebuilding every consumer is an operational trap.

Encryption protects particular crossings. TLS protects invoice data between endpoints. Storage encryption reduces some media, snapshot, and infrastructure exposure. Application or field encryption can narrow who may read especially sensitive values. Signing can detect tampering with a token or webhook. Passwords need a slow, salted password-hashing scheme rather than reversible encryption.

Yet the original worker had legitimate database access and could see plaintext. Encryption would not have stopped its unscoped query, nor would it remove sensitive fields from logs. Ask where plaintext exists, which identity may request decryption, how keys are generated and rotated, what restores and regional failover do, and whether one compromised service can decrypt unrelated data. “Encrypted” is incomplete until the protected boundary and remaining readers are named.

Dependencies enlarge these boundaries. The CSV library, PDF renderer, base image, package manager, build script, CI workflow, registry, and storage client can run code or receive credentials. Keep the dependency graph small enough to understand; pin versions where reproducibility matters; apply security updates deliberately; review build-time and post-install execution; separate build from runtime credentials; use minimal artifacts; and design a way to disable or replace a compromised provider. Vulnerability scanning helps locate known problems. It cannot prove that a dependency is trustworthy or harmless.

Evidence should explain a decision without repeating the secret

When the bad CSV is discovered, export complete answers none of the important questions. A useful audit event identifies the actor and action, authorized resource scope, decision, policy reason, request and job correlation, privileged mode if any, file identifier, row count, and time:

time:          2026-07-15T09:20:31Z
actor:         user/account-admin-217
action:        create_invoice_export
resource:      tenant-aster/invoices/2026-Q2
decision:      allow
policy:        invoice-export@38
request_id:    8d41...
job_id:        exp-9914
file_id:       file-5512
row_count:     42

The event does not need invoice contents, session tokens, download URLs, payment data, or the billing contact’s name. Auditability and privacy pull against each other only when teams confuse evidence with payload capture. Structured, redacted events can preserve decisions without making the audit system a second data leak.

Detection should follow the harm paths already identified. Alert or investigate unexpected cross-tenant denials, exports whose row count differs between API and worker, unusually large exports, downloads from new contexts, sensitive patterns in logs, service credentials used from the wrong environment, expired files that remain accessible, and deletion lag in derived stores. Each signal needs an owner and a normal baseline; an unactionable alert is not a control.

Recovery closes the model. For this incident, disable the download, quarantine affected files, identify every access from the audit trail, repair the worker’s scope, add cross-tenant and privilege-revocation tests, expire derived copies, reconcile deleted contacts, rotate credentials if exposure is plausible, and follow the organization’s notification process where required. The ability to revoke a token, rotate a key, disable an integration, purge a cache, or rebuild an index should exist before the incident that needs it.

Review the route again

The repaired export now has a claim at every crossing:

  1. The API authenticates the actor and derives tenant scope from trusted state.
  2. It authorizes the export as a subject, action, resource, and context, then records the decision and policy version.
  3. Bound parameters preserve query structure while tenant and visibility predicates constrain the resource.
  4. The queued job carries bounded context, and the worker reauthorizes against current policy.
  5. The worker runs with narrow database, storage, key, network, and audit privileges.
  6. The file contains only necessary fields, lives privately for a bounded time, and is released through another authorization decision.
  7. Logs and audit events preserve identifiers and decisions without copying the protected payload.
  8. Derived stores reconcile deletion, and operators can detect, contain, and repair drift.

Try the same pass on one feature you know. Draw its actor-to-asset route. Choose one sensitive field and follow every copy. Choose one protected resource and enumerate single reads, lists, searches, exports, jobs, support paths, caches, and signed links. Then pick the most credible misuse path and name prevention, detection, recovery, and residual risk.

The useful security question is rarely “Which controls do we have?” It is “Which crossing could create this harm, and what remains true when its control fails?” The forty-third invoice becomes preventable once every component has a bounded claim to make—and no component receives more data or authority than that claim requires.