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.
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.
Files flow from the browser through a hardened Express server directly into MinIO — never landing on disk, never accumulating in RAM.
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.
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.
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.
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.
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.
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.
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.
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.
multer is configured to pipe file streams directly to the MinIO SDK upload — no intermediate disk write, no memory accumulation. A field team member uploading a 50MB video from a branch location consumes the same server RAM as uploading a 50KB photo. This eliminates OOM risk on the constrained Alpine container and removes the attack surface of files sitting on disk between receive and upload.
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.
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.
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.
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.
I build and harden backend infrastructure for production environments where correctness is not optional.
Let's Work Together