Case Study — Security Engineering

Zero-Trust File Upload at the Edge

A production-hardened audit evidence submission system — 12 audit areas, streaming to S3-compatible storage, six defense layers from transport to webhook signing, with zero bytes buffered in server RAM.

0 bytes
RAM buffering
6
Security layers
HMAC
Signed webhooks
CSP
No unsafe-inline
Role Lead Engineer
Stack Node.js / Express / MinIO / Docker
Version v1.1.0 — Hardened
Context Retail enterprise audit system
01 — The Problem

File Upload Is the Widest Attack Surface in Any Web App

A retail enterprise running branch audits across dozens of locations needed a way for field staff to submit photo evidence for 12 audit criteria per visit. The naive solution — a standard multipart form with an S3 presigned URL — opens attack vectors that most teams never think about until something goes wrong.

Without path validation, a crafted objectKey can escape the intended bucket prefix and overwrite arbitrary objects. Without server-side MIME enforcement, a renamed executable slides through browser-only checks. Without rate limiting, the submission endpoint becomes a vector for storage exhaustion. And without HMAC-signed webhooks, any downstream consumer of audit submissions has no way to verify the payload wasn't tampered with in transit.

The v1.0 form worked. It was not safe. The v1.1.0 hardening sprint addressed every layer of that attack surface — without touching the user-facing interface.

02 — Architecture

Streaming Pipeline From Browser to Object Storage

Files flow from the browser through a hardened Express server directly into MinIO — never landing on disk, never accumulating in RAM.

Browser multipart/form-data esc() XSS guard data-* attributes HTTPS EXPRESS SERVER Node.js 20 Alpine · non-root ① rate-limit check ② objectKey prefix validate ③ MIME allowlist enforce ④ multer stream → MinIO ⑤ late flag compute ⑥ HMAC-SHA256 sign ⑦ webhook dispatch MinIO S3-compatible streamed — 0 bytes RAM n8n Webhook HMAC-SHA256 verified NocoDB audit records localStorage draft metadata 30min grace window
01
Inbound
Rate Gate
10 req/15min per IP
02
Validate
Path & MIME
objectKey prefix check
03
Stream
MinIO Upload
0 bytes RAM
04
Compute
Late Flag
server-side only
05
Dispatch
HMAC Webhook
SHA-256 signature
03 — Security Layers

Six Layers. Each One Blocks a Real Attack Vector.

Each layer below corresponds to a specific vulnerability class. None of them are theoretical — all were identified during a structured code review of the v1.0 codebase.

1

Path Traversal Prevention — objectKey Prefix Validation

The presigned URL flow accepts an objectKey from the client to generate a pre-signed MinIO URL. In v1.0, the key was used verbatim — meaning a crafted key like ../../other-tenant/file.jpg could escape the intended bucket path. v1.1.0 validates the objectKey against a server-derived prefix based on the authenticated sucursal_id/ciclo_id. Any key that doesn't start with the correct prefix is rejected with 400 before a presigned URL is ever generated.

2

Server-Side MIME Allowlist Enforcement

Browsers can be instructed to enforce file type restrictions via the accept attribute — but any client can bypass this. v1.1.0 enforces a MIME allowlist server-side in the multer configuration. Files with types outside the allowlist (image/jpeg, image/png, image/heic, video/mp4, etc.) are rejected before a single byte reaches MinIO.

3

XSS Prevention — esc() Helper + data-* Pattern

The form builds dynamic HTML from server-supplied field data (criteria names, branch names, status strings). v1.0 used string interpolation directly in innerHTML and onclick attributes — a textbook stored XSS vector. v1.1.0 uses an esc() helper that HTML-encodes all injected values, and replaces onclick string interpolation with data-* attributes read via dataset.* in delegated event handlers.

4

CSP Without unsafe-inline

The iOS camera shutter fix previously required an inline <style> block — forcing unsafe-inline in the Content Security Policy, which defeats the entire purpose of having a CSP. v1.1.0 moves those styles to an external ios-camera.css file, allowing the CSP to enforce strict no-inline rules and giving the browser a meaningful injection defense surface.

5

Rate Limiting Per IP on Sensitive Endpoints

Without rate limiting, the submission endpoint is a storage exhaustion vector — a single automated client can flood the bucket with garbage files. v1.1.0 applies independent rate limiters: 10 submissions per 15 minutes per IP, 60 criteria check requests per 15 minutes per IP. Limits are tuned to field staff usage patterns rather than worst-case developer testing assumptions.

6

HMAC-SHA256 Webhook Signing

Downstream consumers of audit submission webhooks (n8n workflows processing audit results) previously had no way to verify payload authenticity. v1.1.0 signs every outgoing webhook payload with HMAC-SHA256 using a shared secret stored only in the server environment. Consumers verify the X-Webhook-Signature header before processing. A tampered payload fails verification and is dropped.

"The best security hardening is invisible to users and fatal to attackers."
Node.js 20 Alpine Express 4 multer (streaming) AWS SDK v3 MinIO express-validator helmet CORS express-rate-limit HMAC-SHA256 Docker non-root container
04 — Proof

What the Hardening Sprint Delivered

Every metric here is directly traceable to a security decision in v1.1.0. No UX was changed. No feature was removed. Only the attack surface shrank.

0
Bytes RAM buffered
6
Security layers
5
Fraud vectors blocked
0
unsafe-inline in CSP

Path Traversal: Eliminated

Server-derives the required objectKey prefix from authenticated session data. Any presigned URL request with a key outside that prefix returns 400. The bucket cannot be escaped by crafted keys.

XSS Surface: Closed

All DOM injection now goes through esc(). All dynamic event handlers use data-* attributes. String interpolation in innerHTML and onclick strings is entirely absent from the codebase.

Draft Persistence Without Server State

localStorage stores upload metadata client-side. The server recomputes whether a draft submission is within the 30-minute grace window on every request — no server-side session required, no database round-trip for deadline state.

Webhook Consumers Can Now Verify Payloads

HMAC-SHA256 signatures on every outgoing webhook give downstream n8n workflows a cryptographic proof of payload origin. Tampered payloads are rejected before processing. Key rotation requires only a shared secret update in both environments.

Need Your Upload Stack Hardened?

I build and harden backend infrastructure for production environments where correctness is not optional.

Let's Work Together
VIEW LIVE → VERIFIED DELIVERY RECORD →