hermes

Implement secure file uploads

Implements secure file uploads with direct-to-storage signed URLs, type and size validation, a malware-scan hook, safe naming and orphan cleanup. Use for backends accepting user files.

context

File uploads are a classic source of breaches and outages. Typical failures: trusting the file extension or the client's Content-Type, so an HTML or SVG file with script is served from the app's own domain; using the user's file name in the storage path (path traversal, overwrites, leaking names); streaming large files through the app server until it runs out of memory; signed upload URLs with no size limit or a long expiry; files that are uploaded but never attached to anything, piling up forever; and serving uploads publicly when they should be private. A sound design uploads straight to object storage with short-lived, constrained credentials, validates the actual bytes after upload, quarantines until scanned, and only then makes the file available.

task

Implement file uploads for this use case:

use case

Storage: Only if [STACK] is given: Stack: Only if [MAX_SIZE] is given: Maximum size:

  1. Read the repo's storage client, auth, models, background jobs and config, and reuse them. If the allowed file types, maximum size or who may read the files are not clear from the use case, ask before implementing.
  2. Implement this flow:
  3. Request: the client asks the API for an upload, sending the intended file name, size and declared type. The API checks authorization, the allowed type list and the size, creates an upload record in a pending state, and generates a random object key under a quarantine prefix (for example pending/<uuid>); never use the user's file name in the key.
  4. Upload: the API returns a short-lived signed URL (minutes, not hours) that is constrained as tightly as the storage allows: a presigned POST policy with a content-length range and fixed content type for S3-compatible stores, or the equivalent conditions on other providers. For files above the provider's single-request limit, or large files on mobile networks, use multipart or resumable uploads.
  5. Confirm: the client tells the API the upload finished (or a storage event notifies it). The API checks the object exists and its real size matches.
  6. Validate and scan: a background job reads the file's magic bytes to detect the real type and rejects mismatches, enforces content rules (image dimensions, page count, CSV row limit), calls a malware-scan hook (an interface with a no-op implementation for development and a place to plug in a scanner), and for images re-encodes them to strip metadata such as GPS location and neutralise polyglot files.
  7. Promote: clean files move to the final prefix and the record becomes available; failed files are deleted or kept in quarantine with the reason, and the user gets a clear error.
  8. Serve files safely: private by default with short-lived signed download URLs after an authorization check; Content-Disposition: attachment for anything that is not a safe inline type; the correct Content-Type plus X-Content-Type-Options: nosniff; and ideally a separate domain for user content. Store the original file name only as sanitised metadata for display.
  9. Clean up orphans: a storage lifecycle rule that expires objects under the pending prefix after a day or so, plus a scheduled job that removes pending records with no object and objects whose owning record was deleted.
  10. Configure CORS on the bucket for the web origin only, with just the methods and headers the upload needs.
  11. Write tests: the request endpoint rejects disallowed types, oversize files and unauthorised users; the signed URL has the expected constraints and expiry; the validation job rejects a file whose magic bytes do not match its declared type; a scan failure leaves the file unavailable; promotion makes it available; download requires authorization; and cleanup removes expired pending uploads. Use a local emulator or a fake storage client; no real cloud calls.
  12. Run the tests and linter and report the real results.
constraints
  • Never accept SVG, HTML or other active content for inline display unless the use case requires it; if it does, say how it will be sanitised or served from an isolated domain.
  • Never trust the client's file name, extension or Content-Type for security decisions.
  • Never make the bucket public to make uploads work.
  • Do not claim a malware scanner is integrated if only the hook exists; say what is left to wire up.
  • Do only what was asked. If you notice something else worth changing, mention it in one line at the end instead of changing it.
  • Keep the change as small as it can be while still being correct.
  • Before saying the work is done, run the check that proves it (tests, build, type check or the command the user gave) and report the real result.
  • If you could not run a check, say so plainly and say which one.
output format

Flow

A Mermaid sequence diagram of request, upload, confirm, scan, promote and download.

Changes

One line per file.

Security checks

Table: threat, control, where it is implemented.

Tests

One line per test and the real result of the run.

Configuration

Allowed types, size limits, URL expiries, prefixes, lifecycle rule and CORS settings.

Operational notes

What to monitor (quarantine backlog, rejection rate, scan failures) and what is still to wire up.

1 required value still a placeholder; the assistant will ask for it.

details

kind
Prompt: a task you run by name to get one finished thing back
domain
Software engineering
category
Implementation
level
Intermediate
made for
Backend engineer, Full-stack engineer, Mobile engineer, Software engineer
needs
repo-read, file-write, shell
risk
runs-commands
version
v1.0.0 · incubating
reviewed
2026-10-02
works in
Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Antigravity, OpenCode, Windsurf, Zed, Continue, AGENTS.md

Edit on GitHubReport a problem

use in

Hodios CLI
npx @hermes-hq/hodios install implement-file-upload --target claude-code
Agent Skills
npx skills add hermes-hq/hodios-dist --skill implement-file-upload -a claude-code
Add the Hodios marketplace (once)
claude plugin marketplace add hermes-hq/hodios-dist
Install the software-engineering plugin
claude plugin install hodios-software-engineering@hodios

The plugin brings every entry in this domain at once.

pairs well with

All of Implementation
PersonaImplementation

Backend engineer

Acts as a backend engineer focused on correct data handling, clear API contracts, explicit failure modes and services that are easy to operate. Use as a builder or reviewer persona for server code.

backend-engineer
PersonaSecurity

Security auditor

Reviews code for exploitable weaknesses and reports only issues with a concrete attack path. Use as a reviewer persona or subagent for security-sensitive changes.

security-auditor
PromptImplementation

Build a REST endpoint end to end

Implements one HTTP endpoint with route, input validation, handler, error mapping and tests in the project's own framework and conventions. Use when adding an API route.

build-rest-endpoint
PromptImplementation

Implement a background job

Implements a background or scheduled job with idempotency, retries with backoff, dead-letter handling, timeouts, concurrency limits and observability. Use to move slow work off the request path.

implement-background-job
PromptSecurity

Review a pull request for security

Reviews a diff for exploitable vulnerabilities and reports only findings with a concrete attack path. Use before merging changes to input handling, auth, data access or dependencies.

review-pr-for-security
PromptImplementation

Put a change behind a feature flag

Wraps new behaviour behind a feature flag with a safe default, a kill switch, tests for both paths and a cleanup ticket. Use when shipping a risky change incrementally.

add-feature-flag