Prompt and Applicable Scenarios
A multi-tenant B2B document service lets authenticated users upload PDF, JPEG, and PNG attachments, with a 20 MiB limit per file. After processing, only authorized members of the same tenant may download a file. The client controls the bytes, original filename, extension, Content-Type, and declared size, so every one of those values is untrusted.
Design secure upload, scanning, publishing, and download APIs. Explain how the design prevents:
- web shells, spoofed types, double extensions, path traversal, and object overwrite;
- malicious PDFs, image-parser exploits, malware, and polyglot files;
- cross-tenant access, leaked public object URLs, and dangerous inline rendering;
- oversized requests, exhausted storage quotas, scan contention, and scanner outages;
- a file being replaced after it passes scanning, plus state corruption from duplicate completion calls.
The 20 MiB limit, the three allowed formats, and the B2B tenant model are interview constraints rather than universal security recommendations. The candidate must connect identity, the upload protocol, object storage, asynchronous work, and download authorization into a defensible boundary. The core competency is backend API and service security, so the category is backend.
What the Interviewer Evaluates
First, can the candidate establish invariants? Bytes that have not passed a security decision remain in quarantine and cannot be read by business users, parsed by the application, or served from the application's primary origin. A client filename never becomes a storage path. Every read is authorized again.
Second, do they understand defense in depth for type validation? An extension, request Content-Type, file signature, and parser result each provide only partial evidence. The service should combine a business allowlist, normalization, structural parsing, malware scanning, and, where appropriate, re-encoding or content disarm and reconstruction.
Third, can they design an explicit asynchronous state machine? Receiving bytes only means that an object reached quarantine. It does not mean that the file is available. The API needs processing, available, rejected, and technical-failure states, with precise retry, timeout, and cleanup rules.
Fourth, do they catch the scanning race? A scanner judges one specific sequence of bytes. If the same object key can be overwritten afterward, an AVAILABLE record may point to content that was never scanned. The verdict must bind to an immutable object version or content hash, and publishing must be conditional.
Fifth, do they include retrieval in the threat model? A random key reduces guessability but does not replace tenant and object authorization. Downloads need a controlled handler or short-lived signed URL, safe response headers, an isolated origin, and an intentional caching policy.
Clarifying Questions Before Answering
- How will the file be used? Download-only, browser preview, text extraction, thumbnail generation, and third-party processing have different risks. Every additional parser adds an attack surface.
- Are the three formats sufficient? This scenario can reject ZIP, Office, and every other format. Supporting archives would require limits on depth, member count, expanded size, and compression ratio.
- How does the user authenticate? Cookie authentication introduces CSRF controls. A bearer token still needs correct CORS, scope, and leakage controls.
- Is direct-to-object-storage upload required? An API can stream small files. At higher concurrency, it can issue a short-lived credential scoped to one quarantine object and verify the real object after upload.
- Who may upload, inspect status, download, and delete? Define tenant roles, object ownership, and audit requirements separately; checking only that a user is logged in is insufficient.
- What is the scanning SLA? Define wait time, retry count, fail-closed behavior, and the state shown to the user during an outage.
- Are there compliance or residency requirements? Encryption keys, retention, audit records, deletion, and backup cleanup may be constrained.
- What constitutes success? Bytes stored, scan passed, and authorized download are three milestones that deserve separate API semantics.
30-Second Answer Framework
"I would start with three invariants: an unscanned file exists only in quarantine; the server generates the storage key while the original filename remains restricted metadata; and every download performs tenant and object authorization. The lifecycle is PENDING_UPLOAD → QUARANTINED → SCANNING → AVAILABLE/REJECTED/FAILED.
When creating an upload, the service checks the user's role, quota, allowed format, and 20 MiB limit, then generates an unguessable, non-overwritable object key. Bytes stream into non-executable, non-public quarantine storage. The server checks actual size, normalized extension, file signature, and constrained parser output, then an isolated worker scans for malware. The verdict binds to the object version or SHA-256, and publishing succeeds only while those values still match.
For download, the service rechecks tenant and object permission, then returns a short-lived signed URL or streams the object with Content-Disposition: attachment, an accurate safe type, and X-Content-Type-Options: nosniff. A scanner failure leaves the object unavailable and triggers bounded retries. Quotas, rate limits, concurrency, timeouts, and lifecycle cleanup constrain resources. I would validate the design with type spoofing, hostile filenames, a test malware sample, oversized streams, duplicate completion, object replacement, and cross-tenant access."
Step-by-Step Deep Dive
Step 1: Define the trust boundary and security invariants
The client controls every multipart byte, filename, Content-Type, declared size, and request sequence. API gateways, application services, object-store events, and scan queues can also duplicate, delay, or reorder work. Build the design around these invariants:
- A quarantine object has no public read access and cannot be executed by a web server.
- Only an authorized
AVAILABLEobject can be downloaded. - The business record's
tenant_idcomes from the authenticated context, not a freely supplied request field. - The original filename never participates in path construction or object lookup.
- A security decision binds to immutable bytes; publishing cannot transfer that verdict to another version.
- An exception, timeout, or indeterminate result always fails closed.
Randomizing a filename and keeping an object private solve separate problems. A random object key prevents overwrite, path manipulation, and easy guessing. Authorization and private storage provide confidentiality. The design needs both.
Step 2: Separate receipt from availability with a state machine
A minimal state machine is:
PENDING_UPLOAD
├─ bytes verified ─> QUARANTINED ─> SCANNING
│ ├─ safe ─> AVAILABLE
│ ├─ malicious/invalid ─> REJECTED
│ └─ scanner unavailable ─> FAILED
└─ expired/abandoned ─> EXPIREDPOST /uploads creates a session and returns an upload_id. For direct upload, it also returns a short-lived credential that can write only the designated quarantine object. POST /uploads/{id}/complete triggers trusted verification and scanning, so 202 Accepted is appropriate. GET /uploads/{id} reports status. A download entry point appears only for AVAILABLE.
Each transition uses a database condition. For example, only a record currently in QUARANTINED may enter SCANNING. A duplicate queue delivery or repeated complete call therefore applies the same transition at most once. FAILED means a technical processing failure; REJECTED means the file or policy was invalid. Combining them into one vague error weakens recovery and auditability.
Step 3: Receive bytes safely and bound resources
At session creation, check upload permission, remaining tenant quota, user rate, and the requested business format. Generate a random upload_id and object key such as quarantine/{tenant-id}/{uuid}. Normalize the original filename for length, control characters, and Unicode, then retain it only as display metadata.
When the API proxies uploads, stream bytes into quarantine and count them while reading. Abort immediately after 20 MiB instead of buffering the whole file in process memory. With direct upload, scope the signature as tightly as the storage system allows by method, object key, expiry, and size. Afterward, a trusted service still reads object metadata and verifies actual length, version, and ownership. It never trusts the client's complete request.
The quarantine bucket or directory is private by default, non-executable, isolated from the main application origin, and accessed through least-privilege credentials. Expired sessions, abandoned uploads, and terminal scan states need lifecycle cleanup so orphaned objects do not consume quota indefinitely.
Step 4: Combine type validation, parsing, and malicious-content scanning
Checks may run from cheap to expensive, but no single layer can produce AVAILABLE:
- Apply a PDF, JPEG, and PNG allowlist to the normalized extension.
- Treat client
Content-Typeas a hint. Reject obvious mismatches, but never use it as proof. - Inspect signatures and the complete structure so a valid magic prefix is not enough.
- Use an updated, constrained parser to confirm that the entire file can be parsed.
- Run malware scanning in an isolated worker and record engine and rules versions.
- Re-encode decoded images, and consider content disarm and reconstruction for PDFs when the risk model justifies it.
Scanners and parsers process attacker-controlled bytes. Run them in a sandbox with bounded CPU, memory, temporary disk, time, and network access. This scenario rejects archives, which removes decompression-bomb, nesting, and archive path-traversal branches. Antivirus does not prove absolute safety, so storage isolation, safe retrieval, and minimal parsing remain necessary.
Step 5: Bind the verdict to immutable bytes before publishing
The scan job reads an immutable quarantine version_id and computes SHA-256. Its result includes at least the upload_id, object version, hash, actual size, detected type, scanning engine version, and verdict.
Publishing performs a conditional database transition. The record must still be SCANNING, and its object version and hash must still equal the scan input before it can become AVAILABLE. Object storage also prevents that version from being overwritten. If the system copies a file into an approved area, the target gets a new random key and the copy operation identifies the exact source version. Any mismatch causes re-quarantine or rejection rather than reusing an old verdict.
This binding closes a time-of-check-to-time-of-use gap. An attacker cannot upload safe bytes, obtain a passing result, and then replace the same key with malicious bytes. Without storage versioning, use write-once object keys or content-addressed storage, and let the business record point only to the final immutable object.
Step 6: Authorize downloads and control browser interpretation
GET /files/{id}/download derives the current tenant from authentication, then checks object ownership, role, deletion state, and AVAILABLE status. A UUID does not remove any of those checks. After authorization, the application may stream the object or issue a short-lived signed URL bound to one object and operation.
For attachments that do not need preview, return Content-Disposition: attachment, a safely encoded display filename, the server-confirmed media type, and X-Content-Type-Options: nosniff. Isolating the file origin from the main application's cookie domain reduces the chance that active content affects the primary session. Keep signed URLs short-lived because changing a user's permission usually does not revoke an already issued URL immediately.
The retrieval path also needs rate and bandwidth controls, authorization audit events, and an explicit decision on whether proxies or caches may retain private responses. Logs contain the object ID, tenant, actor, and result, but not file contents or reusable signed URLs.
Step 7: Design failure, idempotency, quotas, and observability
When scanning times out or is unavailable, the file remains unavailable. A worker may retry a bounded number of times with backoff. Exhausted retries enter FAILED for controlled manual or automated recovery. A definite malicious or structurally invalid result enters REJECTED, and the quarantine bytes are deleted according to retention policy.
The create endpoint can accept an idempotency key so a client timeout does not create several sessions. Completion, scan consumption, and deletion use conditional state transitions for idempotency. Concurrency limits cover per-user uploads, total tenant bytes, per-file size, queue depth, and per-tenant scan slots so one tenant cannot monopolize global capacity.
Useful metrics include time in each state, quarantine bytes, expired sessions, scan pass/reject/failure rates, retries, queue age, parser timeouts, cross-tenant denials, and failed download authorization. Audit events record transitions, object versions, hashes, policies, and scanner versions, allowing an operator to answer which exact bytes were judged under which rules.
Step 8: Validate the complete path with an adversarial matrix
At minimum, test:
.jpg.php, mixed case, null bytes, and oversized Unicode filenames;- spoofed
Content-Type, a valid prefix with a hostile tail, malformed PDFs, and polyglot files; - the EICAR test file in a safe environment, plus samples that hit parser timeout or resource limits;
- exactly 20 MiB, one byte over, missing length, slow streams, and many concurrent uploads;
- the same idempotency key, concurrent complete calls, duplicate queue messages, and stale scan results;
- attempted object replacement during scanning, proving that a version or hash mismatch cannot publish;
- another tenant reading status, downloading, or deleting the object, with every request denied;
- scanner outage, job timeout, application restart, and orphan cleanup;
- download behavior for attachment, media type,
nosniff, caching, and signed-URL expiry.
Passing requires security and business correctness together: rejected files never receive a download path, safe files eventually become available, retries create no duplicate records, unauthorized requests leak no data, failures remain closed, resource use stays bounded, and the audit trail reconstructs the entire decision chain.
Strong Sample Answer
"I would divide upload into quarantine, decision, and publishing stages, with an invariant that business users can never read bytes that have not passed the decision. When creating a session, I derive tenant_id from authentication, check the role, 20 MiB limit, tenant quota, and PDF/JPEG/PNG allowlist, and generate a random, non-overwritable quarantine key. The original filename remains length-limited, normalized display metadata.
Bytes either stream through the API or use a short-lived credential scoped to that quarantine key. On complete, the server verifies actual size, object version, and ownership, conditionally moves the record from PENDING_UPLOAD to QUARANTINED, and scans asynchronously. Validation combines the extension, server-detected type, full structural parsing, malware scanning, and appropriate re-encoding. The scanner runs with bounded resources and network access.
The scanner reads an immutable version and computes SHA-256. A record can become AVAILABLE only if it is still SCANNING and both the version and hash match the scan input. Replacing a safe file after its scan therefore cannot reuse the old verdict. A scanner outage fails closed with bounded retries; definite malicious or nonconforming content becomes REJECTED.
Every download checks tenant, object permission, and AVAILABLE status before streaming or issuing a short-lived URL. Attachments use an isolated origin, Content-Disposition: attachment, and X-Content-Type-Options: nosniff. I would then test type spoofing, EICAR, oversized streams, duplicate completion, object-replacement races, cross-tenant reads, and scanner outages while monitoring state duration, quarantine bytes, queue age, and scanner version."
Common Mistakes
- Checking only the extension → double extensions, case changes, and disguised content bypass it → combine a business allowlist, normalization, signatures, structural parsing, and scanning.
- Trusting
Content-Type→ the client can choose the request header → use it only for early filtering and detect the final type on the server. - Using the original filename as a path → path traversal, overwrite, and filesystem-normalization bugs become possible → generate a random object key and retain the name only as restricted metadata.
- Making stored bytes immediately downloadable → malicious bytes are exposed before scanning finishes → use quarantine and make
AVAILABLEthe only publishing condition. - Claiming an antivirus pass guarantees safety → new samples, parser bugs, and active content remain possible → keep isolation, minimal parsing, safe headers, and authorization.
- Scanning an overwritable key → a passing result may apply to later replacement bytes → bind an immutable version or content hash and publish conditionally.
- Treating a UUID as authorization → a leaked object ID may enable cross-tenant reads → perform object authorization for status, download, and deletion.
- Trusting client completion after direct upload → the client can lie about size, type, or ownership → read trusted object metadata and byte evidence on the server.
- Failing open during scanner outage → an infrastructure failure becomes a security bypass → fail closed, retry within bounds, and expose a precise processing state.
- Limiting only per-file size → many individually valid files can exhaust storage and scanning → enforce tenant quotas, rates, concurrency, and queue backpressure.
- Rendering arbitrary files inline on the primary origin → browser interpretation and active content can affect the main session → default to attachment download and isolate the file origin.
Follow-up Questions and Responses
Follow-up 1: Why parse the complete structure when you already check magic bytes?
A signature usually covers only a few leading bytes. An attacker can put another format after a valid header or build a file accepted by multiple parsers. Full structural parsing checks internal lengths, relationships, and end markers. The parser still processes untrusted input, so it must remain updated, resource-bounded, and isolated.
Follow-up 2: Does direct-to-object-storage upload bypass backend security?
It changes only the byte-transfer path. The backend still creates a short-lived session bound to a tenant and object key, and the credential writes only to quarantine. The backend then verifies the real object, size, version, and hash before scanning. The object has no read permission before AVAILABLE, so direct upload cannot directly publish it.
Follow-up 3: What should the user experience be during a scanner outage?
Completion returns a processing state, while status reports either that security checks continue or that processing failed and can be retried. The system retries with bounded backoff and monitors queue age. After the threshold, the record enters FAILED and remains unavailable. Recovery retries the same immutable version through a controlled path; availability never skips the decision.
Follow-up 4: Why do downloads still need attachment and nosniff?
Authorization decides who receives bytes. Response headers influence how a browser interprets them. attachment favors download, and nosniff prevents the browser from overriding the server-declared type through content sniffing. An isolated file origin further limits active content's access to the main application's cookies and script context.
Follow-up 5: What controls become necessary if ZIP is allowed?
Inspect every member path in an isolated sandbox, reject absolute paths and .., and bound nesting depth, member count, individual size, total expanded size, compression ratio, CPU, and time. Run type and malicious-content checks again for every extracted member. This scenario has no ZIP requirement, so rejecting it gives the smaller attack surface.
Follow-up 6: How do you prove that a race did not contaminate the verdict?
Record the immutable input version and SHA-256 in the scan job. Publishing uses a conditional database update that requires the current record to remain SCANNING with exactly that version and hash. A test overwrites the logical object during scanning and delivers a stale result; publishing must fail. Audit events connect the input version, verdict, and final approved object.