#10API4 · API8 · A01/A03
File Upload / Download
Ingest, media processing, and serving of user-supplied files.
How to use this prompt
- 1Install SecureNow in your project (then optionally
npx securenow login):$ npm install securenow - 2Copy the prompt below and paste it into your AI coding agent (Claude Code, Cursor, Codex…) opened at the root of your project.
- 3It generates four files into
threat/10-file-upload-download/— openfile-upload-download-code-findings.html(the audit) andfile-upload-download-detection-mitigation.html(the defenses) in your browser.
🔒Runs entirely in your environment — your codebase is never uploaded or shared. The generated HTML reports are self-contained and work offline.
The prompt
# File Upload / Download Threat Model — Generator Prompt
A **copy-paste prompt** for customers. Paste the entire prompt below into an AI coding agent
(Claude Code, Cursor, Codex, …) opened at the root of **any project** that accepts user files
and has the `securenow` CLI installed and logged in. The agent will inventory the upload/serve
pipeline, build an exhaustive **file upload / download** threat model mapped to **OWASP
API4:2023 + API8:2023** and **A01:2021 / A03:2021** (with **CWE-434** unrestricted upload and
**CWE-22** path traversal), audit the code for upload/serve flaws, and emit a SecureNow-branded
report in **Markdown + self-contained HTML** — including the detection rules to create, the
mitigation commands to run, how to test each one, the code-level findings (audited, **not**
fixed), and which threats still need the SecureNow team.
This model owns the **ingest → process → store → serve** pipeline for user-supplied files:
upload **validation** (type confusion, active content, archive bombs), media/document
**processing** as a cost & parser-exploit surface, **metadata** leakage, **filename / object-key**
safety, and **download** authorization and serving headers. Every rule and command it emits is
**grounded in the SecureNow SDK actually installed in the repo** (Phase 0.5), and every detection
is rendered as a **ready-to-copy command unit** (SQL → `rules/<name>.sql` → full create command →
dry-run). It does **not** re-model the
**at-rest** bucket/object posture (bucket ACLs, key layout, encryption, signed-URL infra),
**per-object access control**, or generic **path traversal as injection** — it references those
sibling models and detects their **traffic-observable** abuse only:
- At-rest storage posture (bucket/object ACLs, key layout, encryption, KMS, signed-URL infra) →
[storage & logs model](../23-storage-and-logs/) (folder 23).
- Per-object / per-tenant access control (the deep BOLA model behind download IDOR) →
[authorization model](../02-authorization/) (folder 02).
- Path traversal / null-byte as a generic **injection** class (filename → filesystem sink) →
[injection model](../06-injection/) (folder 06). This model still covers traversal **in
filenames/object keys at the upload/serve boundary** and defers the broader sink taxonomy.
> SecureNow is fundamentally an **API / traffic** security layer (firewall, rate-limit,
> challenge, exploit-signature instant-block, ASN enrichment, forensics). For file upload/download
> its **native** value is real but **bounded**: it detects the **abuse** — `api.upload.rejected`
> and `api.media.processing_limit` **events** the app emits, download-scraping and IDOR-probe
> **traffic**, abusive upload/download clients it can challenge or rate-limit — while the
> **validation, sandboxing, AV scanning, and metadata stripping are an application fix**.
> Edge-containment and the app fix are **paired** on every such row.
Requirements on the customer machine: `npm i -g securenow && securenow login` (admin auth +
app runtime connected). Everything else is discovered by the agent.
---
<!-- ════════════════ COPY EVERYTHING BELOW THIS LINE ════════════════ -->
# Generate a File Upload / Download Threat Model Report (SecureNow)
You are a senior application-security engineer specializing in file-handling security. Produce an
**exhaustive file upload / download threat model for THIS codebase**, organized along the threats
below, mapped to **OWASP API4:2023 (Unrestricted Resource Consumption)** and **API8:2023
(Security Misconfiguration)**, **A01:2021 (Broken Access Control)** and **A03:2021 (Injection)**,
and the CWEs **CWE-434 (Unrestricted Upload of File with Dangerous Type)** and **CWE-22 (Path
Traversal)**. Map each to **SecureNow** detections and mitigations, with a ready-to-run action
plan **and** a code-level audit of every upload/serve flaw you find. You write **four** deliverables
across **two tracks** into `threat/10-file-upload-download/` (create the folder if needed):
**Track A — Detection & Mitigation (the operational runbook: what to run in SecureNow):**
1. `file-upload-download-detection-mitigation.md` — the runbook in Markdown.
2. `file-upload-download-detection-mitigation.html` — the same, self-contained HTML (inline CSS +
JS, no network) with a **Copy button on every command/SQL block**.
**Track B — Code Findings & Recommendations (the code audit: issues + fixes, never applied):**
3. `file-upload-download-code-findings.md` — the code audit in Markdown.
4. `file-upload-download-code-findings.html` — the same, self-contained HTML.
The two tracks **cross-link**: gap/instrumentation rows in the detection runbook link to the
relevant code finding, and each code finding links back to the detection row it backs. Every alert
rule and command is **grounded in the installed `securenow` SDK** (Phase 0.5 — never guess flags,
subcommands, event names, or SQL columns), and every detection is emitted as a **ready-to-copy
command unit** (SQL → save to `rules/<name>.sql` → full `securenow alerts rules create …` →
dry-run test).
Work in the phases below, in order. **Never invent facts**: if something is not in the
codebase or not returned by a CLI command, say "not found" — do not guess. **Do not modify
application code.** You are auditing: every code-level fix is *described in the report*, never
applied to the repo.
**Scope discipline.** This model owns the **ingest/serve pipeline**: upload validation
(CWE-434), media/document processing cost & parser exploitation (API4), active content & polyglot
files, archive/decompression DoS, EXIF/document-metadata leakage, filename/object-key traversal
(CWE-22 at the boundary), download IDOR (A01, observable), signed-URL scope/lifetime, and serving
headers (`Content-Disposition`/inline-render XSS). For the **at-rest bucket/object posture**,
**per-object access control**, and the **broad path-traversal injection taxonomy**, do **not**
re-derive the deep model — list them in a "Deferred to sibling models" subsection, link those
reports by **numbered path**, and only model their **traffic-observable** symptoms (download
enumeration, IDOR-probe 403 patterns) where SecureNow adds value.
---
## Phase 0 — Verify SecureNow tooling
Run and record (use `--json` where supported):
```bash
securenow doctor # connectivity must be healthy
securenow whoami # admin auth + runtime app
securenow status --json # app key(s), environment, firewall state
securenow alerts rules --json # detection rules that already exist (incl. system signature rules)
securenow automation --json # blocklist automations that already exist
securenow challenge list --json # CAPTCHA / proof-of-work challenge rules
securenow env --json # resolved SDK config (service name, endpoints)
```
If the CLI is missing or not logged in, **stop** and tell the user to run
`npm i -g securenow && securenow login`, then re-run this prompt. Capture the **app key**
(UUID) — every rule and command in the report must use it. If multiple apps exist, ask the
user which app this codebase maps to before continuing. Note the **firewall state** and any
**system signature rules** (SQLi/XSS/RCE) already present — those matter because uploaded files
served from the app origin and SVG/HTML active content can carry the same payloads the signature
rules catch; do not duplicate them.
---
## Phase 0.5 — Ground every rule & command in the INSTALLED SDK
Before writing any SQL or CLI, read the SecureNow SDK that is actually installed in this repo so
every alert rule and command is correct for THIS version — never guess flags, subcommands, event
names, or SQL columns:
```bash
cat node_modules/securenow/package.json # installed SDK version (record it in both reports)
ls node_modules/securenow # exported modules: events, sessions, register, run, …
ls node_modules/securenow/dist 2>/dev/null # built entrypoints / bundled CLI
npx securenow --help # top-level commands available in this version
npx securenow alerts rules --help # exact create flags: --name/--sql/--apps/--severity/--schedule/--nlp
npx securenow event --help # `event send` shape for synthetic tests
npx securenow ratelimit --help; npx securenow challenge --help
npx securenow blocklist --help; npx securenow automation --help; npx securenow trusted --help
```
If `node_modules/securenow` is absent, run `npm ls securenow`; if still missing, tell the user to
`npm i securenow` (or `npm i -g securenow`) and stop. EVERY command, flag, `track('…')` event
name, and SQL column you emit MUST be one the installed SDK/CLI actually exposes. If the installed
version lacks a capability this prompt references, emit the rule but annotate it
`# requires securenow >= <version>` instead of a broken command. Record the resolved version in
the appendix of BOTH reports.
In Phase 4 and Phase 5, treat `node_modules/securenow` + `--help` as the source of truth: the
`securenow/events` `track()` signatures, the `securenow alerts rules` SQL columns, and every
mitigation subcommand are discoverable there. Cross-check before emitting.
---
## Phase 1 — Inventory the upload / download surface (codebase analysis)
File-handling security starts with mapping the **whole pipeline**: how a file enters, what
parses/processes it, where it lands, and how it is served back. Document what is **actually
exposed**, not what is intended. Cover at minimum:
- **Upload endpoints** — enumerate every route that accepts a file (method + path): multipart
form uploads, base64/JSON-embedded uploads, raw-body PUT, chunked/resumable (tus, Uppy),
GraphQL `Upload` scalars, server actions, and any drag-drop/paste handler. Public vs
authenticated vs admin. This catalog is a report deliverable.
- **Upload transport mode** — server-proxied (file flows through your app) vs **direct-to-storage**
(browser uploads straight to S3/GCS/Azure/R2 via a **presigned POST/PUT** your server issues).
For direct-to-storage: who sets the key, content-type, ACL, size condition, and expiry; whether
the POST policy pins `content-length-range`, `Content-Type`, and a key **prefix**.
- **Type / content validation** — for each upload, how is the file type decided? Extension only,
client-supplied `Content-Type`/multipart MIME only, **magic-byte/content sniffing**, an
**allowlist** of types, or nothing. Note where validation trusts attacker-controlled signals
(extension, client MIME). (CWE-434.)
- **Active-content handling** — are **SVG**, **HTML**, **XML**, **PDF**, and **Office** files
accepted? Are SVGs sanitized (script/`<foreignObject>`/event-handler stripping) or served
raw? Are PDFs/Office files opened by a processor that executes macros/JS or follows external
entities? Are HTML uploads rendered inline?
- **Archive handling** — are `.zip`/`.tar`/`.gz`/`.7z`/`.rar` accepted and **decompressed**?
Are there limits on decompressed size, entry count, nesting depth, and per-entry path (zip-slip
`../` traversal during extraction)? (Decompression bomb / archive DoS, CWE-22 on extract.)
- **Media / document processing** — every processor invoked on an uploaded file: image libs
(sharp, ImageMagick/`convert`, GraphicsMagick, Pillow, libvips), PDF (Ghostscript, pdf.js,
poppler, pdftk), Office (LibreOffice headless, unoconv), thumbnailers, transcoders (ffmpeg),
OCR. For each: is it **sandboxed**, version-pinned, and bounded by **pixel count / page count /
dimensions / processing time / memory / CPU**? (API4 cost + parser-exploit surface, e.g.
ImageMagick "ImageTragick"-class.)
- **Resource limits** — per-upload **max file size**, **max parts** in a multipart request,
**max total request size**, decompressed-size cap, pixel/page caps, processing timeout, and
concurrent-job limits. Note where **none** exist. (API4.)
- **Malware scanning** — is any AV/scan step present (ClamAV, a cloud scan API, EDR) before a
file is stored or served? If not, record it.
- **Filename & object-key handling** — how is the stored name derived? Is the **client filename**
used (directly or sanitized)? Is there a check for `../`, null bytes (`%00`), absolute paths,
Windows drive/UNC, overlong unicode, control chars? Are object keys **random** (UUID) or
**predictable/attacker-influenced** (sequential id, client filename, user-supplied key)?
(CWE-22 at the boundary; attacker-controlled key → overwrite/IDOR.)
- **Metadata handling** — is **EXIF** (GPS/camera/device/timestamps), **PDF/Office document
metadata** (author, software, revision, comments, tracked changes, hidden layers), or embedded
thumbnails **stripped** before storage/serving? If not, uploaded files may leak the uploader's
location/identity.
- **Download / serve pipeline** — enumerate every route that serves a file back: streamed from
the app, redirect to a signed URL, served from the storage origin directly, or via a CDN. For
each, how is **authorization** done (is the requester allowed *this* object?), and is the
identifier **predictable** (sequential id, raw key) or **unguessable** (random + ownership
check)? (Download IDOR → A01.)
- **Serving headers** — for served files: is `Content-Disposition` set to **attachment** (vs
inline) for untrusted content? Is `Content-Type` forced to a safe value or echoed from the
stored (attacker-influenced) type? Is `X-Content-Type-Options: nosniff` set? Are user files
served from the **app's own origin** (cookie/session domain) or an isolated sandbox domain?
(Stored XSS via served file → A03/A01.)
- **Signed-URL posture** — if signed/presigned URLs are issued for download or upload: their
**lifetime** (minutes vs days), **scope** (single object vs prefix/bucket), whether they encode
the requester's authorization, and whether they leak via logs/Referer/analytics. (Defer the
storage-infra detail to folder 23; model the **issuance scope/lifetime** here.)
- **Telemetry privacy & redaction** — confirm the SecureNow SDK/log pipeline redacts **file URLs,
object keys, signed-URL query strings, original filenames, and any PII in filenames/metadata**
before ingestion. Attributes feed detection; they must not become a new leak path. If not
found, create a high-severity finding.
- **SecureNow instrumentation already present** — `securenow/register` / `securenow run` /
`securenow init` (gives traffic spans automatically — upload/download requests, status codes,
IPs, sizes), any existing `securenow/events` `track()` calls (especially `api.upload.rejected`
/ `api.media.processing_limit`), and whether the firewall is engaged. This determines what
works *today* vs *after instrumentation*.
Output of this phase = the report's **Upload/Download surface & inventory** section: the upload
endpoint catalog (method/path/visibility/transport mode), the **validation matrix** (Type check /
Active content / Archive / Size caps / Pixel-page caps / AV scan / Metadata strip per endpoint),
the **processing-surface list** (library + version + sandbox state), the **download/serve table**
(authz model / key predictability / serving headers / origin), the **signed-URL posture**, the
**filename/object-key handling note**, the **telemetry redaction status**, and a short paragraph
naming the real file-handling attack surface for this stack.
---
## Phase 2 — Enumerate threats (exhaustive catalog)
Evaluate **every** threat below against the discovered surface. Each item is either **modeled**
(a row in the threat matrix) or **explicitly N/A** (one line in an "Out of scope" subsection
with the reason — e.g. "Archive items: N/A, no server-side decompression"). Never silently drop
an item. Add stack-specific threats you discover that are not listed — this catalog is the floor,
not the ceiling. Tag each modeled row with its framework code (**API4 / API8 / A01 / A03 /
CWE-434 / CWE-22**, or "—").
**A. Type confusion & validation bypass (CWE-434 · API8)**
1. Extension-only validation (rename `shell.php` → `shell.jpg`, or `.jpg.php` double extension)
2. Client-supplied MIME / multipart `Content-Type` trusted (attacker sets `image/png` on a script)
3. Magic-byte vs declared-type mismatch (declared `image/png`, bytes are a PE/ELF/HTML)
4. Polyglot file (valid GIF **and** valid HTML/JS, or a PDF that is also a ZIP) bypassing one check
5. Content-sniffing differential — server allowlists by MIME but the browser sniffs it as HTML/JS
6. Null-byte / encoded-extension trick in the filename to confuse the extension check (`x.php%00.jpg`)
**B. Active content in uploads (CWE-434 · A03)**
7. SVG with `<script>`, `on*` handlers, `<foreignObject>`, or external `<image href>` → stored XSS
8. HTML/XHTML upload served inline → stored XSS in the app origin
9. PDF with embedded JavaScript / launch actions / external-entity references
10. Office document with macros / DDE / remote-template injection
11. XML upload with external entities (XXE) parsed during processing/preview
12. File with embedded payload that triggers a downstream parser (e.g. malicious EXIF processed by a tool)
**C. Resource consumption & DoS (API4)**
13. Oversized upload — no per-file size cap (memory/disk exhaustion)
14. Multipart-part flooding — thousands of parts in one request (parser/memory DoS)
15. No total-request-size cap (slow large body, disk fill)
16. Archive bomb / zip bomb — small upload decompresses to gigabytes
17. Nested-archive bomb — recursive zip-in-zip exhausting depth handling
18. Decompression with no entry-count / decompressed-size / depth limit
19. Image "pixel flood" / decompression bomb (tiny file, enormous dimensions → huge bitmap in memory)
20. Multi-page/large-document processing with no page/time cap (PDF/Office → CPU/RAM DoS)
21. Expensive media processing amplification (one upload → heavy transcode/OCR/render job)
22. Concurrent-job flooding — many simultaneous processing jobs with no queue/concurrency limit
**D. Parser & processor exploitation (API4 · A03)**
23. ImageMagick/GraphicsMagick command/delegate injection (ImageTragick-class) via crafted file
24. Ghostscript / PDF-processor RCE via crafted PostScript/PDF
25. libvips/Pillow/sharp parser memory-safety bug via malformed image
26. ffmpeg/transcoder SSRF or file-read via crafted container/playlist (e.g. HLS/`concat`)
27. Office/LibreOffice headless processing of a weaponized document
28. SSRF via a processor that follows embedded URLs (SVG `<image href>`, PDF external resources)
**E. Malware & content trust (CWE-434)**
29. Malware/webshell upload with no AV/scan step before store or serve
30. Uploaded file served from the **app's own origin** (cookie/session domain) enabling XSS/drive-by
31. Phishing/abuse hosting — attacker uses your upload+serve as free file hosting for malicious payloads
32. Stored file later executed by the server (upload dir inside the web/exec root)
**F. Metadata leakage (API8 · A01)**
33. EXIF GPS/location not stripped — uploader's home/location leaked to all viewers
34. EXIF camera/device/serial/timestamps disclose uploader identity & habits
35. PDF/Office metadata (author, org, software, revision history, comments) leaked
36. Hidden content — tracked changes, hidden layers/slides, cropped-but-embedded image regions
37. Embedded thumbnail revealing original (un-redacted/un-cropped) image
**G. Filename & object-key safety (CWE-22 · A01)**
38. Path traversal in stored filename (`../../etc/...`) writing outside the intended dir
39. Null-byte / control-char / overlong-unicode filename truncating the extension check
40. Absolute path / Windows drive / UNC path in filename
41. Attacker-controlled object key on direct-to-storage upload (overwrite another object / escape prefix)
42. Predictable object key (sequential / filename-derived) enabling enumeration or overwrite
43. Filename collision / overwrite of another user's or a system file (no per-tenant prefix)
**H. Download authorization & serving (A01 · A03)**
44. Download IDOR — fetch another user's/tenant's file by id (`/files/123`) with no ownership check
45. Predictable-key download — guess/enumerate the storage key/path and pull files directly
46. Signed-URL lifetime too long (days) — leaked link stays valid far past need
47. Signed-URL scope too broad (prefix/bucket-wide, or not bound to the requester)
48. Signed URL leaks via logs/Referer/analytics/shared link → unauthorized download
49. `Content-Disposition: inline` (or missing) on untrusted content → browser renders → stored XSS
50. `Content-Type` echoed from stored/attacker value with no `nosniff` → MIME-sniffing XSS
51. Range/partial-content or path-normalization quirk serving an unintended object
**I. Direct-to-storage & presigned-upload misconfig (API8)**
52. Presigned **POST policy** missing `content-length-range` → unbounded upload to your bucket
53. Presigned policy not pinning `Content-Type` / key prefix → arbitrary type or key outside tenant
54. Presigned **PUT** with overly long expiry or reusable for many objects
55. Public-write or world-readable destination implied by the upload flow (defer at-rest detail to folder 23)
**J. Negative-space & evasion**
56. Validation done on the **first chunk** only (resumable/chunked upload swaps content later)
57. Re-validation skipped after server-side transform (sanitized on upload, mutated on processing)
58. Case / trailing-dot / alternate-data-stream extension tricks (`shell.PHP`, `shell.php.`, `:$DATA`)
59. Time-of-check/time-of-use — file validated, then a symlink/replace before it is read
60. Bypassing the upload size limit by spanning many small uploads (assembled later)
**K. Observable abuse (what telemetry actually catches — the workhorse rules)**
61. Upload-endpoint flood from one IP/ASN (mass upload, abuse-hosting, storage exhaustion)
62. Burst of rejected uploads from one client (probing validation: many `api.upload.rejected`)
63. Processing-limit trips from one client (crafted-file probing: many `api.media.processing_limit`)
64. Download **scraping** — one IP pulling an anomalous count of distinct files/objects
65. Download **IDOR probing** — one IP hitting many distinct file IDs with a high 403/404 ratio
66. Large/anomalous download volume from a single client (bulk file exfiltration)
67. Exploit-signature match in an upload request or in a served-file URL (XSS/SQLi/RCE marker) → instant block
**L. Deferred — modeled in sibling models (reference by numbered path, do not re-derive)**
68. At-rest bucket/object posture — ACLs, key layout, encryption/KMS, signed-URL infra → [storage & logs](../23-storage-and-logs/) (folder 23)
69. Per-object / per-tenant access-control deep model (the BOLA behind download IDOR) → [authorization](../02-authorization/) (folder 02)
70. Path traversal / null-byte as the broad **injection** sink taxonomy → [injection](../06-injection/) (folder 06)
> For 68–70, add **one** matrix row each marked *"deferred — see linked model"*, and only note
> the SecureNow traffic-observable symptom here (download enumeration, 403/404 IDOR-probe spikes).
> The full detection/mitigation lives in those reports. This model still covers traversal **in
> upload filenames / object keys** (rows G38–G43) and download IDOR's **observable** abuse (K65).
---
## Phase 3 — Audit the code (findings only — do not fix)
For **each** modeled threat that maps to real code, locate the responsible code and record a
**finding** for the report's "Code-level findings" section. A finding is:
- **Location** — `file:line` (clickable), the route/handler/middleware/processor name.
- **Pattern** — quote the 1–8 relevant lines. State the missing control precisely (e.g. "type
decided from `file.mimetype` (client-supplied) → CWE-434"; "`multer()` with no `limits.fileSize`
/ `limits.files` → unbounded upload & part flood"; "`sharp(buf)` with no `limitInputPixels` →
pixel bomb"; "`yauzl`/`unzipper` extract with no decompressed-size/entry/depth cap → zip bomb +
zip-slip"; "filename = `req.file.originalname` used in the storage path → CWE-22"; "`res.sendFile`
by `req.params.id` with no ownership check → download IDOR"; "SVG stored and served
`Content-Type: image/svg+xml` inline → stored XSS"; "presigned POST with no
`content-length-range` condition").
- **Why exploitable** — the concrete file/request an attacker sends and what they achieve.
- **Severity** — critical / high / medium / low (impact × reachability).
- **Recommended fix (described, not applied)** — the specific change. *Recommended fixes must
mention, where relevant*: content sniffing + a **type allowlist by magic bytes** (not extension
or client MIME); rejecting active content or sanitizing SVG (strip scripts/handlers/external
refs) and serving it as `text/plain`/attachment; **AV/malware scan** before store/serve;
**sandboxed**, version-pinned, resource-bounded processors (`policy.xml` for ImageMagick,
`-dSAFER` for Ghostscript, seccomp/container isolation); explicit **size / part-count /
decompressed-size / entry-count / depth / pixel / page / processing-time** limits; **random
object keys** (UUID) under a **per-tenant prefix**, never the client filename; sanitizing
filenames (reject `../`, null bytes, absolute/UNC, control chars); **stripping EXIF/document
metadata**; serving user files from an **isolated origin** with `Content-Disposition:
attachment`, a forced safe `Content-Type`, and `X-Content-Type-Options: nosniff`; an
**ownership/tenant check on every download** with unguessable identifiers; **short-lived,
object-scoped, requester-bound** signed URLs; and presigned POST policies that pin
`content-length-range`, `Content-Type`, and key prefix. Reference the secure pattern, not a
code diff. **You must not edit the codebase.**
If a control exists and is correct (magic-byte allowlist enforced, pixel/size caps set, metadata
stripped, downloads ownership-checked, files served from a sandbox origin as attachments), note
it as a **strength** — the posture must be honest. Absence of a control where the surface exists
is itself a finding ("no size limit on any upload route"; "no ownership check on `/files/:id`").
Look specifically for, and write findings for:
**Type / content validation** — type decided by extension or `req.file.mimetype`; no
content-sniffing; no allowlist; double-extension / null-byte filename handling; validation only on
the first chunk of a resumable upload; no re-validation after transform.
**Active content** — SVG/HTML/XML/PDF/Office accepted and served inline or processed without
sanitization; SVG served `image/svg+xml` from the app origin; XML parsed with entity expansion /
external entities enabled; PDF/Office opened by a processor that runs JS/macros.
**Resource & processing limits** — multipart parser with no `fileSize`/`files`/`fields` limits;
no total-request cap; decompression with no decompressed-size/entry/depth caps; image processing
with no `limitInputPixels`/dimension cap; PDF/Office processing with no page/time cap; processors
not sandboxed or version-pinned (cross-check lockfile for ImageMagick/Ghostscript/libvips/sharp
CVEs — **do not run destructive updates**); unbounded concurrent jobs.
**Filename / object key** — client `originalname` used in the storage path or returned key;
missing `../`/null-byte/absolute-path sanitization; predictable/sequential keys; client-supplied
key on direct-to-storage upload; no per-tenant prefix; possible overwrite of existing objects.
**Metadata** — no EXIF/document-metadata stripping before store/serve; embedded thumbnails
retained; crop applied in view layer only (original bytes retained).
**Download authz & serving** — `/files/:id`-style handlers with no ownership/tenant check;
predictable identifiers; `res.sendFile`/stream by user-supplied path; `Content-Disposition`
inline or absent; `Content-Type` echoed from stored value; no `nosniff`; user files on the app's
cookie origin; signed URLs with long expiry / broad scope / not requester-bound; signed-URL
strings logged or sent in Referer.
**Direct-to-storage / presigned** — presigned POST without `content-length-range`; policy not
pinning `Content-Type` or key prefix; presigned PUT with long expiry or reusable; ACL set to
public on upload. (Defer the at-rest bucket-policy audit to folder 23; here, audit the
**issuance** code.)
---
## Phase 4 — Map every modeled threat to SecureNow detection + mitigation
Classify each threat with exactly one coverage badge:
- 🟢 **COVERED** — detectable + mitigable with SecureNow today (existing rule, system signature
rule, or a rule you provide the SQL for, on telemetry that is already flowing). For this domain
the natively covered set is: the **`api.upload.rejected`** and **`api.media.processing_limit`**
events the app emits, **download-scraping** and **IDOR-probe** traffic detection, **upload-flood**
detection, and **challenge / rate-limit** on abusive upload/download clients.
- 🟡 **PARTIAL** — works after the customer adds instrumentation (`track('api.upload.*')` /
`track('api.media.*')` at the validation/processing point), or SecureNow can only *contain the
abuser at the edge* while the **real fix is app-level** (the validation, SVG sanitization, AV
scan, sandboxing, metadata stripping, ownership check, serving headers). **Pair the edge
control with the app fix on every such row.**
- 🔴 **GAP** — SecureNow cannot detect or mitigate this today (e.g. a parser RCE inside a crafted
file that never trips a validation event; metadata leakage with no abuse signal; a too-broad
signed URL that produces a valid, authorized-looking download). **Still include it**: give the
app/config-level fix, then add the line *"Requires SecureNow team — contact your SecureNow
account contact (or in-dashboard support) to request support for this threat."* Collect all gaps
in the report's "Known gaps & SecureNow feature requests" section.
> **Be honest about what is edge-detectable vs an app fix.** SecureNow sees **traffic** and
> **events**; it contains actors via firewall / rate-limit / challenge / block / signature
> instant-block. It **cannot** open a file and tell that the SVG has a script, that EXIF carries
> GPS, that the magic bytes don't match, or that a download was an IDOR — unless the **app emits
> the event** (`api.upload.rejected`, `api.media.processing_limit`) or the abuse is **visible in
> traffic** (a scrape, an upload flood, an IDOR-probe 403 pattern). The missing **validation /
> sandboxing / AV / metadata-strip / authz** is the **primary fix**; SecureNow detects the
> *abuse* and contains the source. A flaw that emits no traffic and no event is a 🔴 until the app
> emits the event in Phase 3's recommended fix.
Use **only** the SecureNow building blocks below. Never invent CLI flags, event names, or SQL
columns.
### 4a. Instrumentation (what these detections feed on)
Once the app runs under `securenow run` / `securenow/register` / `securenow init`, **HTTP traffic
is captured automatically** — upload/download requests, status codes (incl. **403/404** and
**5xx**), methods, paths, client IPs, response sizes. The **download-scraping, IDOR-probe, and
upload-flood** rules need **no events**.
For what traffic can't see — *why* an upload was rejected, *what* processing limit was hit — add
`securenow/events` `track()` at the validation/processing point (never throws):
```js
const { track } = require('securenow/events');
// An upload failed your own validation (type/size/parts/archive/SVG/malware/pixel/page/metadata):
track('api.upload.rejected', { userId, ip, attributes: { route: '/api/upload', reason: 'mime_mismatch|too_large|too_many_parts|archive_bomb|svg_blocked|malware|pixel_limit|page_limit|metadata', bytes: '<number_as_string>' } });
// Media/PDF/image/archive processing hit a cost/parser limit:
track('api.media.processing_limit', { userId, ip, attributes: { route: '/api/process', reason: 'timeout|pages|pixels|decompressed_size|archive_depth|cpu', file_type: 'image|pdf|zip|svg|office' } });
```
> **Reuse these two events verbatim — they are the native upload/media signals.** Hash or omit any
> PII before it becomes an attribute value (original filenames, file URLs, object keys, signed-URL
> query strings) — see the Phase 1 **telemetry privacy & redaction** check. Attributes feed
> detection; they must not become a new leak path.
Other events from the SecureNow taxonomy that fit this pipeline where the app enforces the gate
(emit at the enforcement point, never throws):
```js
// The app rejected an oversized/over-large body itself (size gate distinct from upload validation):
track('api.payload.rejected', { ip, attributes: { route: '/api/upload', reason: 'too_large', bytes: '9100000' } });
// A processor followed an embedded URL and your SSRF guard blocked it (SVG <image href>, PDF ext. resource):
track('ssrf.blocked', { ip, attributes: { route: '/api/process', target_host: '169.254.169.254', reason: 'link_local' } });
```
Recommended event taxonomy for this domain — rules match these **exact strings**:
| Event | Emit when |
|---|---|
| `api.upload.rejected` | an upload fails MIME/size/parts/archive/SVG/malware/pixel/page/metadata checks |
| `api.media.processing_limit` | media/PDF/image/archive processing hits a cost/parser limit |
| `api.payload.rejected` | a request body fails a size/content-type gate before upload validation |
| `ssrf.blocked` | a processor's embedded-URL fetch is denied by an SSRF allowlist/guard |
Custom `attributes` become queryable as `attributes_string['<key>']` (e.g.
`attributes_string['reason']`, `attributes_string['file_type']`, `attributes_string['bytes']`).
Ingest enriches every IP with **ASN/org** (`client.asn`, `client.as_org`) — enabling
datacenter-origin / botnet detection on upload floods and scrapes with no extra code.
### 4b. Detection rules — SQL conventions
Two query shapes. Both **must** keep the tenant scope and **must** select an `ip` column (per-IP
aggregation is what remediation/auto-block keys on). **The tenant-scope column differs by table** —
using the wrong one fails with `UNKNOWN_IDENTIFIER`:
- **logs/events** (`signoz_logs.distributed_logs_v2`) → `resources_string['service.name'] IN (__USER_APP_KEYS__)`
- **traces/HTTP** (`signoz_traces.distributed_signoz_index_v3`) → `` `resource_string_service$$name` IN (__USER_APP_KEYS__) ``
When grouping by `ip`, add `HAVING ip != '' AND …` so rows with no client IP don't aggregate into
an empty-key bucket. Traffic columns proven available: `response_status_code`, `kind` (server span
= 2), `ts_bucket_start`, `attributes_string['http.target']`, the `client_ip` coalesce below.
Confirm any other column (`http.method`, response size) with a `--mode dry_run` before relying on
it; OTEL SDK versions vary (`http.method` vs `http.request.method`).
**Traffic-based — upload-endpoint flood (single IP), no events needed:**
```sql
WITH coalesce(nullIf(attributes_string['http.client_ip'], ''), nullIf(attributes_string['net.peer.ip'], ''), nullIf(attributes_string['network.peer.address'], '')) AS client_ip
SELECT client_ip AS ip,
count() AS uploads
FROM signoz_traces.distributed_signoz_index_v3
WHERE `resource_string_service$$name` IN (__USER_APP_KEYS__)
AND timestamp >= now64(9) - INTERVAL 5 MINUTE
AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 5 MINUTE)) - 1800
AND kind = 2
AND attributes_string['http.target'] LIKE '%/upload%'
GROUP BY ip
HAVING ip != '' AND uploads >= 120
```
**Traffic-based — download scraping (one IP pulling many distinct files):**
```sql
WITH coalesce(nullIf(attributes_string['http.client_ip'], ''), nullIf(attributes_string['net.peer.ip'], ''), nullIf(attributes_string['network.peer.address'], '')) AS client_ip
SELECT client_ip AS ip,
count() AS downloads,
uniqExact(attributes_string['http.target']) AS distinct_files
FROM signoz_traces.distributed_signoz_index_v3
WHERE `resource_string_service$$name` IN (__USER_APP_KEYS__)
AND timestamp >= now64(9) - INTERVAL 15 MINUTE
AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 15 MINUTE)) - 1800
AND kind = 2
AND (attributes_string['http.target'] LIKE '%/files/%' OR attributes_string['http.target'] LIKE '%/download%' OR attributes_string['http.target'] LIKE '%/media/%')
GROUP BY ip
HAVING ip != '' AND distinct_files >= 100
```
**Traffic-based — download IDOR probing (many distinct file IDs + high 403/404 ratio):**
```sql
WITH coalesce(nullIf(attributes_string['http.client_ip'], ''), nullIf(attributes_string['net.peer.ip'], ''), nullIf(attributes_string['network.peer.address'], '')) AS client_ip
SELECT client_ip AS ip,
uniqExact(attributes_string['http.target']) AS distinct_targets,
countIf(response_status_code IN ('401','403','404')) AS denied
FROM signoz_traces.distributed_signoz_index_v3
WHERE `resource_string_service$$name` IN (__USER_APP_KEYS__)
AND timestamp >= now64(9) - INTERVAL 15 MINUTE
AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 15 MINUTE)) - 1800
AND kind = 2
AND (attributes_string['http.target'] LIKE '%/files/%' OR attributes_string['http.target'] LIKE '%/download%' OR attributes_string['http.target'] LIKE '%/media/%')
GROUP BY ip
HAVING ip != '' AND distinct_targets >= 40 AND denied >= 30
```
**Events-based — upload validation probing (API4/CWE-434; query the logs table):**
```sql
SELECT
attributes_string['http.client_ip'] AS ip,
attributes_string['route'] AS route,
attributes_string['reason'] AS reason,
count() AS rejections
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
AND attributes_string['event.type'] = 'api.upload.rejected'
AND timestamp >= now() - INTERVAL 15 MINUTE
GROUP BY ip, route, reason
HAVING ip != '' AND rejections >= 10
```
**Events-based — media/processing-limit abuse (API4 parser/cost probing; logs table):**
```sql
SELECT
attributes_string['http.client_ip'] AS ip,
attributes_string['file_type'] AS file_type,
attributes_string['reason'] AS reason,
count() AS trips
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
AND attributes_string['event.type'] = 'api.media.processing_limit'
AND timestamp >= now() - INTERVAL 15 MINUTE
GROUP BY ip, file_type, reason
HAVING ip != '' AND trips >= 5
```
**Events-based — processor SSRF attempt (embedded-URL fetch blocked; any hit high-signal):**
```sql
SELECT
attributes_string['http.client_ip'] AS ip,
attributes_string['target_host'] AS target_host,
count() AS attempts
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
AND attributes_string['event.type'] = 'ssrf.blocked'
AND timestamp >= now() - INTERVAL 30 MINUTE
GROUP BY ip, target_host
HAVING ip != '' AND attempts >= 1
```
The remaining events follow the **same logs-table shape** — swap the `event.type` filter and the
threshold: `api.payload.rejected` (≥20/15m → oversized-body probing). Tune the upload-flood and
scraping thresholds to the app's real volume after a baseline `--mode dry_run`.
**Active-content / payload-in-file (catalog B) — use the SecureNow system signature rules, don't
write SQL.** SVG/HTML uploads served from the app origin, and request URLs carrying XSS/SQLi/RCE
markers, are caught by the **system signature rules** with synchronous **`instant.block`** (blocks
a matching request in ~2.6s on ingest). Confirm they're present and enabled for this app via
`securenow alerts rules --json` (look for the signature/exploit rules); enable `instant.block`
rather than authoring duplicate pattern SQL. Note: the signature rules act on **request URLs/
bodies in traffic**, not on the bytes inside a stored file — the in-file checks remain the app fix.
Useful attributes/columns: `event.type`, `http.client_ip`, `http.target`, `response_status_code`,
`kind`, `client.asn`, `client.as_org`, and your upload attributes (`route`, `reason`, `file_type`,
`bytes`, `target_host`).
#### Ready-to-copy command unit (emit every detection this way)
Every detection becomes a **complete, copyable unit** — never a fragment. For each rule, emit in
order: (1) the SQL, (2) a line saving it to `rules/<name>.sql`, (3) the full
`securenow alerts rules create …` command, (4) the dry-run test. In Markdown each is its own fenced
block so it copies cleanly; in the Detection HTML each is a `.cmd` block with a Copy button. The
exact flags must match `securenow alerts rules --help` from Phase 0.5 — never invent flags. Save
each rule's SQL to `rules/<name>.sql` so `--sql @rules/<name>.sql` resolves. Note pre-existing /
system rules instead of duplicating them. Example:
```sql
-- rules/upload-rejected-probing.sql
SELECT
attributes_string['http.client_ip'] AS ip,
attributes_string['route'] AS route,
attributes_string['reason'] AS reason,
count() AS rejections
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
AND attributes_string['event.type'] = 'api.upload.rejected'
AND timestamp >= now() - INTERVAL 15 MINUTE
GROUP BY ip, route, reason
HAVING ip != '' AND rejections >= 10
```
```bash
securenow alerts rules create \
--name "Upload: validation probing (rejected uploads)" \
--sql @rules/upload-rejected-probing.sql \
--apps <APP_KEY> \
--severity high \
--schedule "*/5 * * * *" \
--nlp "single IP getting 10+ uploads rejected in 15 minutes"
securenow alerts rules test <RULE_ID> --mode dry_run --wait # validate before it runs live
```
#### Test mode for false-positive-prone rules (ship FP-prone rules in `--mode test` first)
Alert rules have a lifecycle **mode**: `test` = **detect-only, NO mitigation** vs `prod` = full
(mitigation / auto-action armed) — plus a **status** (`Active | Disabled | Paused`). Manage with:
```bash
securenow alerts rules update <RULE_ID> --mode test # detect-only: fires notifications, takes NO action
# …observe real traffic for several days; tune the threshold; add securenow fp exclusions for any FPs…
securenow alerts rules update <RULE_ID> --mode prod # promote: arm the mitigation / auto-action
securenow alerts rules update <RULE_ID> --status Paused # or --enable / --disable / --pause shortcuts
```
**Rule of thumb:** any detection that can **false-positive** — heuristic thresholds (upload-flood /
download-scrape / rejected-upload / processing-limit / IDOR-probe counts), broad patterns, anomaly /
volume rules, anything tuned to YOUR traffic — must ship in **`--mode test` first**. Run it
detect-only for **3–7 days of real traffic**, review what it flags, raise/lower the threshold and add
`securenow fp` exclusions for legitimate hits (a bulk uploader, a partner batch import, an internal
migration job), then `--mode prod` to arm mitigation. Only **high-precision** rules
(exploit-signature SQLi/XSS/RCE matches on upload/served-file URLs, exact-match IoCs, known-bad ASN
hits, `ssrf.blocked` single-hit) may go straight to `prod`. **Tag every rule `test-first` or
`prod-ready`** in the report and say why. (`securenow alerts rules test <id> --mode dry_run --wait`
is the separate one-off *query* validation — run it before either mode.)
### 4c. Mitigation commands (the only allowed remediation surface)
For file-handling abuse, SecureNow **contains the actor at the edge**; the **app fix** removes the
underlying weakness (validation, sandboxing, AV, metadata strip, ownership check, serving headers).
**Always pair them** — edge-containment WITH the app fix. Once a threat is confirmed, **choose the
narrowest effective mitigation(s) from ALL of these** and combine them (e.g. rate-limit `/api/upload`
+ block the worst IPs + challenge a NAT egress hitting `/files/`). Re-check every command/flag
against the installed SDK in Phase 0.5 (`securenow <cmd> --help`); annotate
`# requires securenow >= <ver>` if absent. Scope by **app / env / route / method / IP / duration** to
avoid hitting real users.
| # | Mitigation | Command (ready-to-copy) | Use / scope |
|---|---|---|---|
| 1 | **Free firewall (network)** | `securenow firewall enable --app <APP_KEY> --env production` · `securenow run --firewall-only` · test `securenow firewall test-ip <ip> --path /api/upload --method POST` | 500k+ known-bad IPs, hourly refresh; drop scanners/uploaders before the app. No app change. |
| 2 | **Exploit-signature instant block** | enable the `instant` config on the system SQLi/XSS/RCE signature rules (dashboard / MCP `securenow_alert_rule_instant_update`); custom rule → create with `--execution-mode instant` | synchronous ~2.6s block of the matching request (XSS/SQLi/RCE marker in an upload request or served-file URL). Don't duplicate pattern SQL. |
| 3 | **IP block — global** | `securenow blocklist add <ip> --app <APP_KEY> --env production --reason "..."` | confirmed-malicious uploader / scraper, all routes. |
| 4 | **IP block — scoped to route (+ method)** | `securenow blocklist add <ip> --route /files* --mode prefix --method ALL --app <APP_KEY> --env production --reason "..."` (`--mode exact\|prefix\|regex`, `--method GET\|POST\|…\|ALL`) | block an IP only on upload/download paths; least collateral. |
| 5 | **IP block — temporary / time-boxed** | `securenow blocklist add <ip> --duration 24h --reason "..."` (`30m`,`24h`,`7d`) · reverse `securenow blocklist unblock <id> --reason "..."` | auto-expiring containment; audit-preserving unblock. |
| 6 | **Rate limit — per IP** | `securenow ratelimit add <ip> --limit 100 --window 1m --duration 24h --reason "..."` | throttle one abusive client across the app. |
| 7 | **Rate limit — per route (all clients, per-IP budget)** | `securenow ratelimit add --route /api/upload --mode prefix --method POST --limit 30 --window 1m --key-by ip` | cap an expensive/abusable upload or download endpoint for everyone, budgeted per IP. |
| 8 | **Rate limit — per route + IP** | `securenow ratelimit add <ip> --route /api/upload --mode exact --method POST --limit 10 --window 1m --duration 24h` · NL `securenow ratelimit from-text "rate limit /api/upload to 10/min for 24h" --yes` · test `securenow ratelimit test <ip> --path /api/upload --method POST` | precise throttle of one client on one route. |
| 9 | **CAPTCHA / proof-of-work challenge** | `securenow challenge add --route /api/upload --difficulty 16 --clearance 30m` (route-wide) **or** `securenow challenge add <ip> --route /files --difficulty 18 --clearance 30m` · test `securenow challenge test <ip> --path /api/upload --method POST` | bot upload/scrape / business-flow abuse from **shared / NAT / CGNAT** egress — a human passes once, a script can't. Prefer over a hard block when real users share the IP. |
| 10 | **Auto-block (risk-scored)** | `securenow automation defaults --yes` (≥95→7d, 90–94→72h, 85–89→24h) · custom `securenow automation create --conditions '[...]' --actions '[...]'` · preview `securenow automation dry-run <id>` | hands-off blocking by risk score; actions include block / rate_limit / requireCaptcha. |
| 11 | **Session revocation** | `securenow revoke …` (SDK `securenow/sessions` `guard()` / `isRevoked()`) | session theft / account takeover behind an upload/download flow — kill the stolen session, not the IP. |
| 12 | **Trusted IP (suppress)** | `securenow trusted add <ip> --label "Partner batch import / monitor"` | stop false positives from known-good infra (partner batch upload jobs, internal scanners) — suppresses detection **and** mitigation. NOT deny-by-default. |
| 13 | **Allowlist (deny-by-default)** | `securenow allowlist add <ip> --label "..." --reason "..."` ⚠️ once any entry exists, ONLY listed IPs reach the app | lockdown of an internal/admin-only upload surface. Never for a public app. |
| 14 | **False-positive exclusion** | `securenow fp create --conditions '[...]' --rule-scope this_rule --reason "..."` · `securenow fp mark <notification-id> <ip> --rule-scope this_rule` · preview `securenow fp dry-run --conditions '[...]'` | keep a noisy rule quiet without weakening it (legit bulk uploader/exporter). |
| 15 | **App / config / code fix (primary for root cause)** | *described in the Code-Findings report, never auto-applied* | the actual fix: magic-byte allowlist, SVG sanitization / serve-as-attachment, AV scan, sandboxed+bounded processors, size/pixel/page/decompression limits, random per-tenant object keys, filename sanitization, EXIF/metadata stripping, ownership check on download, isolated serve origin + `nosniff` + `Content-Disposition: attachment`, short-lived requester-bound signed URLs, presigned-POST `content-length-range`/type/prefix pinning. SecureNow contains; the fix removes. |
**Choosing per threat** — by **confidence**: exploit-signature/exact IoC (XSS/SQLi/RCE marker on an
upload or served-file URL) → instant-block or block; probable bot uploading/scraping on shared egress
→ **challenge**; noisy/legit-mixed traffic (bulk uploader, batch import) → **rate-limit (test-mode
first)**; session compromise → **revoke**; known-good noise → **trusted / fp**. By **blast radius**:
always scope to the narrowest `route`/`method`/`IP`/`duration` that stops the abuse (e.g.
`/api/upload` POST only, `/files/*` prefix); on NAT/CGNAT/shared IPs prefer challenge/rate-limit over
a hard block. Always pair an edge mitigation with the **app/config fix** (Code-Findings report) when
SecureNow can only contain the actor — for type/active-content/AV/metadata/authz/serving the app fix
is **primary**.
### 4d. Testing every detection and mitigation
Only test against apps/environments the user owns; prefer `--env local`/staging. For synthetic
source IPs use TEST-NET ranges (`192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`).
```bash
# Synthetic upload-rejection probing — exercise the api.upload.rejected rule end to end:
for i in $(seq 1 12); do
securenow event send api.upload.rejected --ip 203.0.113.50 \
--attrs route=/api/upload,reason=mime_mismatch,bytes=204800,test=true
done
# Synthetic media processing-limit abuse:
for i in $(seq 1 6); do
securenow event send api.media.processing_limit --ip 203.0.113.51 \
--attrs route=/api/process,reason=pixels,file_type=image,test=true
done
# Synthetic processor SSRF block (SVG/PDF embedded URL):
securenow event send ssrf.blocked --ip 203.0.113.52 \
--attrs route=/api/process,target_host=169.254.169.254,reason=link_local,test=true
# Validate a rule query without waiting for the schedule:
securenow alerts rules test <RULE_ID> --mode dry_run --wait
# Traffic-based rules (upload flood / download scraping / IDOR probing) — generate spans, then check:
securenow test-span "threat-model.upload.smoke"
securenow forensics "uploads and downloads and 403/404 by IP in the last hour" --env production
# Active-content / served-file XSS signature — confirm a marker triggers the system rule + instant block (staging):
# request a served file URL carrying a benign-but-matching marker (e.g. ?name=<script>alert(1)</script>)
# to a staging URL, then verify the block fired:
securenow firewall test-ip 203.0.113.50 --app <APP_KEY> --env production
# Mitigation verification:
securenow ratelimit test 203.0.113.50 --path /api/upload --method POST
securenow challenge test 203.0.113.50 --path /api/upload --method POST
# Confirm + clean up:
securenow notifications list --limit 10
securenow blocklist list # then: securenow blocklist unblock <id> --reason "threat-model test"
securenow challenge list # then: securenow challenge remove <id>
```
Every 🟢/🟡 threat row in the report must have a concrete test recipe (commands + expected
outcome: which rule fires, which notification appears, what the mitigation does).
---
## Phase 5 — Write the FOUR reports (two tracks)
Write **four** files into `threat/10-file-upload-download/` — two per track, Markdown + HTML. The
two tracks **cross-link** each other: every gap / instrumentation row in the Detection runbook
links to the relevant Code Findings entry, and every code finding links back to the Detection row
it backs. `<slug>` = `file-upload-download`.
1. `file-upload-download-detection-mitigation.md` — Track A runbook (Markdown).
2. `file-upload-download-detection-mitigation.html` — Track A, self-contained HTML with Copy buttons.
3. `file-upload-download-code-findings.md` — Track B code audit (Markdown).
4. `file-upload-download-code-findings.html` — Track B, self-contained HTML.
### 5a. Track A — Detection & Mitigation report (the operational runbook)
Sections, in order, in **both** the `.md` and the `.html`:
1. **Executive summary** — stats line (threats modeled · covered · partial · gaps · rules to
create · mitigations), top 3 detectable file-handling risks for this stack, the installed
`securenow` version + app key + firewall state, and a one-line framework-coverage note (which of
API4/API8/A01/A03/CWE-434/CWE-22 are owned here vs deferred to siblings).
2. **SDK & environment** — installed SDK version (from `node_modules/securenow` per Phase 0.5),
app key(s), environment, firewall state, existing rules / automations / challenge rules (from
Phase 0), and which **system signature rules** (SQLi/XSS/RCE) are present.
3. **Threat → Detection → Mitigation matrix** — one row per modeled threat:
`# | Threat | Framework (API4/API8/A01/A03/CWE-434/CWE-22) | Coverage 🟢/🟡/🔴 | Detection rule (or "—") | Mode (test-first/prod-ready) | Signal (threshold+window) | Schedule | Sev | Mitigation`.
Severity ∈ {critical, high, medium, low}. Each row's **Mitigation** cell must pick **specific,
scoped mitigation(s) from the §4c toolbox** (e.g. "rate-limit `/api/upload` POST 30/min per IP +
app fix: magic-byte allowlist"; "challenge `/files/*` from the abusing IP") — never a generic
"block the IP." Each rule also carries a **`test-first` or `prod-ready`** tag in the Mode column
(per §4b: FP-prone heuristic/threshold/volume rules are `test-first`; high-precision
exploit-signature/exact-IoC/SSRF-hit rules are `prod-ready`). Then the "Out of scope" N/A list,
then the deferred rows (68–70) pointing to the sibling models by numbered path. Each
gap/instrumentation row **links to the Code Findings report**.
4. **Detection rules to create** — each as the **ready-to-copy command unit** from Phase 4
(SQL → save to `rules/<name>.sql` → full `securenow alerts rules create …` → `--mode dry_run`
test). **Mark each rule `test-first` or `prod-ready`** (per §4b): a `test-first` rule ships
detect-only — include the `securenow alerts rules update <RULE_ID> --mode test` create-state and
the `--mode prod` promotion step ("promote after 3–7 days of clean traffic"); a `prod-ready` rule
(high-precision exploit-signature/exact-IoC/SSRF) may arm immediately. Active-content /
payload-in-file rows reference the **system signature rules + `instant.block`**, not duplicate
SQL. Note rules that already exist (from Phase 0) instead of duplicating them.
5. **Instrumentation the detections need** — only the `track('…')` events the rules above consume
(`api.upload.rejected`, `api.media.processing_limit`, and where relevant `api.payload.rejected`
/ `ssrf.blocked`), each as a copyable snippet; point to the **Code Findings report** for *where*
to add them.
6. **Mitigation mechanisms** — render the **full §4c mitigation toolbox table verbatim** (all 15
rows: free firewall · exploit-signature `instant.block` · IP block [global / route+method /
temporary] · rate-limit [per-IP / per-route / route+IP] · challenge · auto-block · session
revoke · trusted · allowlist · fp exclusion · **app-config fix**) plus the "Choosing per threat"
paragraph — then a per-threat ready-to-copy mitigation command + reversibility. Make explicit that
the **app fix is primary** for type/active-content/AV/metadata/authz/serving and the SecureNow
control is **containment** — paired on every row.
7. **Action plan (copy-paste, ordered)** — ① engage the firewall + enable signature instant-block,
② add the upload/media event instrumentation where traffic is blind, ③ create rules — **every
FP-prone rule created in `--mode test` (detect-only)**, only high-precision `prod-ready` rules
armed straight away, ④ enable automations / challenge rules, ⑤ test, ⑥ verify in dashboard,
⑦ **promote step: after 3–7 days of clean traffic, run `securenow alerts rules update <RULE_ID>
--mode prod` for each test-first rule** (tune thresholds + add `securenow fp` exclusions first),
⑧ schedule the app/config fixes (from the Code Findings report). Real commands only, `<APP_KEY>`
already substituted.
8. **Testing & validation** — per-rule recipe from Phase 4d: `securenow event send …` / `test-span`
/ `--mode dry_run` + expected outcome + cleanup (TEST-NET IPs `192.0.2`/`198.51.100`/`203.0.113`).
9. **Response runbooks** — for each notification type (upload flood, rejected-upload probing,
processing-limit abuse, download scraping, IDOR probing, processor SSRF, signature instant-block):
confirm TP → respond command (copy) → reverse command (copy).
10. **Known gaps & SecureNow feature requests** — every 🔴 threat (e.g. in-file parser RCE, metadata
leakage with no abuse signal, too-broad signed URL): why it's not coverable today, the interim
fix (**link to the Code Findings report**), and the "contact the SecureNow team" line.
11. **Appendix** — resolved SDK/CLI version (from Phase 0.5), app key, environment, firewall state,
rule IDs created, sibling models referenced (folders 02/06/23), date, link to the Code Findings
report.
### 5b. Track B — Code Findings & Recommendations report (the code audit)
State at the top: *"Findings only — no application code was modified."* Sections, in order, in
**both** the `.md` and the `.html`:
1. **Executive summary** — findings by severity (critical / high / medium / low), top 3 code risks
for this stack, one-paragraph posture verdict.
2. **Surface & inventory** — the Phase 1 inventory for this domain: the upload endpoint catalog +
validation matrix + processing-surface list + download/serve table + signed-URL posture +
filename/object-key note + telemetry redaction status.
3. **Threat catalog** — the exhaustive Phase 2 catalog (groups A–L), each tagged with its framework
code (API4/API8/A01/A03/CWE-434/CWE-22, or "—"), modeled or explicit N/A — nothing dropped.
4. **Code-level findings (audit)** — table
`# | Location (file:line) | Threat | Framework | Sev | Issue | Recommended fix`, each with the
quoted 1–8 line snippet and the described fix (never applied). Each finding **links to the
Detection row** it backs.
5. **Strengths** — controls already present and correct (magic-byte allowlist enforced, pixel/size
caps set, metadata stripped, downloads ownership-checked, files served as attachments from an
isolated origin) — the posture must be honest.
6. **App / config fixes (primary remediation)** — the config/code changes that remove the root
cause (validation, SVG sanitization / serve-as-attachment, AV scan, sandboxed+bounded
processors, size/pixel/page/decompression limits, random per-tenant object keys, filename
sanitization, EXIF/metadata stripping, ownership check on download, isolated serve origin +
`nosniff` + `Content-Disposition: attachment`, short-lived requester-bound signed URLs,
presigned-POST `content-length-range`/type/prefix pinning), described not applied, each linked
to the Detection report row it backs.
7. **Instrumentation recommendations** — the `track('api.upload.rejected')` /
`track('api.media.processing_limit')` / `ssrf.blocked` / `api.payload.rejected` calls to add and
the exact `file:line` to add them, so the Detection rules light up.
8. **Appendix** — files reviewed, resolved SDK version (from Phase 0.5), date, link to the
Detection & Mitigation report.
### 5c. HTML skeletons — SecureNow branding, self-contained, offline copy buttons
Both HTML files share the same `<head>` (brand tokens + copy-button styles) and the same copy
`<script>` at the end of `<body>`; **change only** the `<title>`, the sidebar subtitle, the `<h1>`,
and the section content. **Wrap EVERY command/SQL block as a `.cmd`** so it gets a Copy button.
Inline CSS + JS only — no external fonts/scripts/network. The Detection HTML mirrors §5a's sections;
the Code Findings HTML mirrors §5b's. Stats-card numbers MUST equal the matrix/finding row counts.
#### Shared skeleton (head + copy CSS + copy script) — use for BOTH files
```html
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
<title><!-- "Detection & Mitigation — File Upload / Download — SecureNow" OR "Code Findings — File Upload / Download — SecureNow" --></title>
<style>
:root{--bg:#0f1419;--panel:#161c24;--panel2:#1b2330;--border:#26303d;--txt:#dbe3ec;--muted:#8b97a7;
--accent:#3ea6ff;--accent2:#16c79a;--crit:#ff5c6c;--high:#ff9f43;--med:#f7c948;--low:#8b97a7;
--ok:#16c79a;--info:#3ea6ff;--rev:#b388ff;}
*{box-sizing:border-box}html{scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--txt);font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
a{color:var(--accent);text-decoration:none}
code{background:#0b0f14;border:1px solid var(--border);border-radius:5px;padding:.08em .4em;font:13px/1.4 ui-monospace,"SF Mono",Menlo,Consolas,monospace;color:#9fe0c0}
.wrap{display:grid;grid-template-columns:240px 1fr;max-width:1280px;margin:0 auto}
nav{position:sticky;top:0;align-self:start;height:100vh;overflow:auto;padding:28px 18px;border-right:1px solid var(--border);background:var(--panel)}
nav .brand{font-weight:700;font-size:15px;letter-spacing:.3px}nav .brand span{color:var(--accent)}
nav .sub{color:var(--muted);font-size:12px;margin-bottom:22px}
nav a{display:block;color:var(--muted);padding:7px 10px;border-radius:7px;font-size:13.5px}
nav a:hover{background:var(--panel2);color:var(--txt)}
main{padding:36px 40px 80px;min-width:0}
header.top h1{margin:0 0 6px;font-size:26px}header.top p{margin:0;color:var(--muted)}
.pill{display:inline-block;font-size:11px;font-weight:600;padding:3px 9px;border-radius:999px;border:1px solid var(--border);color:var(--muted);background:var(--panel)}
.stats{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin:26px 0 34px}
.stat{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px 18px}
.stat .n{font-size:26px;font-weight:700}.stat .l{color:var(--muted);font-size:12.5px;margin-top:2px}
section{margin:0 0 40px}
h2{font-size:18px;margin:0 0 14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
h2 .num{color:var(--accent);font-weight:700;margin-right:8px}
table{width:100%;border-collapse:collapse;font-size:13.5px;background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
th,td{text-align:left;padding:11px 13px;border-bottom:1px solid var(--border);vertical-align:top}
th{background:var(--panel2);color:var(--muted);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.4px}
tr:last-child td{border-bottom:none}tr:hover td{background:#19212c}
.rid{font:12px ui-monospace,Menlo,Consolas,monospace;color:#7fd1ff;white-space:nowrap}
.b{display:inline-block;font-size:11px;font-weight:700;padding:2px 8px;border-radius:6px;white-space:nowrap}
.b.crit{background:rgba(255,92,108,.15);color:var(--crit);border:1px solid rgba(255,92,108,.35)}
.b.high{background:rgba(255,159,67,.13);color:var(--high);border:1px solid rgba(255,159,67,.32)}
.b.med{background:rgba(247,201,72,.13);color:var(--med);border:1px solid rgba(247,201,72,.32)}
.b.low{background:rgba(139,151,167,.13);color:var(--low);border:1px solid rgba(139,151,167,.32)}
.c{display:inline-block;font-size:11px;font-weight:700;padding:2px 8px;border-radius:6px;white-space:nowrap}
.c.cov{background:rgba(22,199,154,.13);color:var(--ok);border:1px solid rgba(22,199,154,.35)}
.c.part{background:rgba(247,201,72,.13);color:var(--med);border:1px solid rgba(247,201,72,.32)}
.c.gap{background:rgba(255,92,108,.15);color:var(--crit);border:1px solid rgba(255,92,108,.35)}
.owasp,.cwe{display:inline-block;font:11px ui-monospace,Menlo,Consolas,monospace;color:var(--accent);border:1px solid rgba(62,166,255,.3);border-radius:6px;padding:1px 6px;white-space:nowrap}
.cwe{color:var(--rev);border-color:rgba(179,136,255,.3)}
.m{display:inline-block;font-size:11px;font-weight:600;padding:2px 8px;border-radius:6px;border:1px solid var(--border)}
.m.block{color:var(--crit);border-color:rgba(255,92,108,.35)}.m.rate{color:var(--info);border-color:rgba(62,166,255,.35)}
.m.challenge{color:var(--accent2);border-color:rgba(22,199,154,.35)}.m.firewall{color:var(--ok);border-color:rgba(22,199,154,.35)}
.m.signature{color:var(--crit);border-color:rgba(255,92,108,.35)}.m.notify{color:var(--muted)}.m.appfix{color:var(--high);border-color:rgba(255,159,67,.35)}
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px 20px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
pre{background:#0b0f14;border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow:auto;font:13px ui-monospace,Menlo,Consolas,monospace;color:#cfe8da;margin:0}
.cmd{position:relative;margin:10px 0}
.copy{position:absolute;top:8px;right:8px;font:11px ui-monospace,Menlo,Consolas,monospace;color:var(--muted);background:var(--panel2);border:1px solid var(--border);border-radius:6px;padding:3px 9px;cursor:pointer}
.copy:hover{color:var(--txt);border-color:var(--accent)}.copy.done{color:var(--ok);border-color:var(--ok)}
.flow{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:6px 0 14px}
.flow .step{background:var(--panel2);border:1px solid var(--border);border-radius:9px;padding:8px 12px;font-size:13px}.flow .arr{color:var(--accent);font-weight:700}
.note{border-left:3px solid var(--high);background:rgba(255,159,67,.06);padding:10px 14px;border-radius:0 8px 8px 0;color:#e7d3bd;font-size:13.5px;margin:10px 0}
footer{color:var(--muted);font-size:12px;border-top:1px solid var(--border);padding-top:18px;margin-top:30px}
@media(max-width:880px){.wrap{grid-template-columns:1fr}nav{display:none}.stats,.grid2{grid-template-columns:1fr 1fr}main{padding:24px 18px}}
</style></head>
<body>
<div class="wrap">
<nav>
<div class="brand">Secure<span>Now</span></div>
<div class="sub"><!-- "Detection & Mitigation · File Upload / Download" OR "Code Findings · File Upload / Download" --></div>
<!-- one <a href="#…"> per section of THIS track -->
</nav>
<main>
<header class="top"><h1><!-- report title for THIS track --></h1>
<p><code><!-- app name / domain --></code> · <span class="pill">securenow <!-- installed version --></span></p></header>
<div class="stats"><!-- 5 .stat cards; numbers MUST equal the table/finding counts --></div>
<!-- <section id="…"> blocks mirroring the Markdown sections of THIS track (5a or 5b) -->
<footer>Generated by the SecureNow file upload / download threat-model prompt · <!-- date --> · securenow <!-- version --> · app <code><!-- APP_KEY --></code></footer>
</main>
</div>
<script>
document.querySelectorAll('.copy').forEach(function(b){b.addEventListener('click',function(){
var pre=b.parentElement.querySelector('pre'); if(!pre)return; var t=pre.innerText;
function done(){b.textContent='Copied';b.classList.add('done');setTimeout(function(){b.textContent='Copy';b.classList.remove('done');},1500);}
function fb(){var ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();try{document.execCommand('copy');}catch(e){}document.body.removeChild(ta);done();}
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).then(done,fb);}else{fb();}
});});
</script>
</body></html>
```
#### Track A body (Detection & Mitigation) — title/subtitle/stats
```html
<title>Detection & Mitigation — File Upload / Download — SecureNow</title>
<!-- nav .sub: --> <div class="sub">Detection & Mitigation · File Upload / Download</div>
<!-- header: --> <h1>File Upload / Download — Detection & Mitigation</h1>
<!-- stats cards (5): -->
<div class="stats">
<div class="stat"><div class="n"><!-- N --></div><div class="l">threats modeled</div></div>
<div class="stat"><div class="n" style="color:var(--ok)"><!-- N --></div><div class="l">covered</div></div>
<div class="stat"><div class="n" style="color:var(--med)"><!-- N --></div><div class="l">partial</div></div>
<div class="stat"><div class="n" style="color:var(--crit)"><!-- N --></div><div class="l">gaps — SecureNow team</div></div>
<div class="stat"><div class="n" style="color:var(--accent)"><!-- N --></div><div class="l">rules to create</div></div>
</div>
<!-- sections mirror §5a 1–11. Every SQL/command block uses the copyable wrapper below. -->
```
#### Track B body (Code Findings) — title/subtitle/stats
```html
<title>Code Findings — File Upload / Download — SecureNow</title>
<!-- nav .sub: --> <div class="sub">Code Findings · File Upload / Download</div>
<!-- header: --> <h1>File Upload / Download — Code Findings & Recommendations</h1>
<!-- top line: --> <p class="note">Findings only — no application code was modified.</p>
<!-- stats cards (5): findings by severity + total -->
<div class="stats">
<div class="stat"><div class="n" style="color:var(--crit)"><!-- N --></div><div class="l">critical</div></div>
<div class="stat"><div class="n" style="color:var(--high)"><!-- N --></div><div class="l">high</div></div>
<div class="stat"><div class="n" style="color:var(--med)"><!-- N --></div><div class="l">medium</div></div>
<div class="stat"><div class="n" style="color:var(--low)"><!-- N --></div><div class="l">low</div></div>
<div class="stat"><div class="n"><!-- N --></div><div class="l">total findings</div></div>
</div>
<!-- sections mirror §5b 1–8. Wrap any example/fix command in .cmd; prose needs no Copy button. -->
```
#### Copyable command wrapper (used for every SQL/command block in the Detection HTML)
```html
<div class="cmd"><button class="copy" type="button">Copy</button><pre>securenow alerts rules create \
--name "..." --sql @rules/<name>.sql --apps <APP_KEY> --severity high \
--schedule "*/5 * * * *" --nlp "..."</pre></div>
```
Badge usage: severity → `<span class="b crit|high|med|low">`; coverage →
`<span class="c cov|part|gap">COVERED|PARTIAL|GAP</span>`; OWASP tag → `<span class="owasp">API4</span>`;
CWE tag → `<span class="cwe">CWE-434</span>`; mitigation type →
`<span class="m firewall|signature|rate|challenge|block|notify|appfix">`; rule IDs →
`<span class="rid">`. Wrap every SQL/command block in `.cmd` with a Copy button (Detection HTML);
the Code Findings HTML may omit Copy buttons on prose but still wraps any example/fix command in
`.cmd`. Stats numbers must equal the matrix/findings row counts.
---
## Quality bar (the report is rejected if any of these fail)
- Every catalog item A1–K67 is either a matrix row or an explicit N/A line; each modeled row
carries its framework tag (**API4 / API8 / A01 / A03 / CWE-434 / CWE-22**, or "—").
- The download-IDOR-observable, scraping, upload-flood, rejected-upload, and processing-limit
rows are each modeled, with the matching `api.upload.rejected` / `api.media.processing_limit`
event where the detection needs instrumentation.
- The at-rest storage posture (folder 23), per-object access control (folder 02), and the broad
path-traversal injection taxonomy (folder 06) are **deferred** to the sibling models (rows
present, linked by numbered path, not re-derived) — this model does not duplicate them.
- Every matrix row has a concrete signal (threshold + window), severity, and mitigation — no
"monitor for suspicious uploads" filler.
- Every code finding in section 4 has a `file:line`, the quoted snippet, and a described fix —
and **no application code was modified** (this is an audit).
- Every detection SQL keeps `__USER_APP_KEYS__` scoping (correct table column — `service.name` for
logs vs `` `resource_string_service$$name` `` for traces), uses the `client_ip` coalesce, selects
an `ip` column, ends with `HAVING ip != ''`, and traffic queries keep the `ts_bucket_start` +
`kind = 2` guards; rules are validated with `--mode dry_run`.
- Active-content / payload-in-file coverage references the **system signature rules +
`instant.block`**, not duplicate pattern SQL, and is honest that signatures act on request
traffic, not stored-file bytes.
- Only commands, flags, events, and SQL columns from this prompt's building blocks appear
(including `securenow challenge …`, `firewall`, `event send`, and the `api.upload.rejected` /
`api.media.processing_limit` / `api.payload.rejected` / `ssrf.blocked` events).
- Detection vs. fix is honest: where SecureNow can only contain the actor at the edge, the row
**pairs the control with the app fix** (validation / sandboxing / AV / metadata-strip / authz /
serving headers is primary).
- Every 🔴 gap appears in the gaps section with an interim app/config fix **and** the "contact the
SecureNow team" line.
- The action plan runs top-to-bottom with `<APP_KEY>` substituted in.
- **Phase 0.5 ran**: the resolved installed `securenow` version appears in BOTH reports' appendix,
and no command/flag/event/column is emitted that the installed SDK/CLI does not expose (else it
is annotated `# requires securenow >= <version>`).
- Every detection rule is a **complete copyable unit** (SQL → `rules/<name>.sql` → full
`securenow alerts rules create …` → `--mode dry_run` test); flags match `alerts rules --help`.
- **Four** files are written to `threat/10-file-upload-download/` (detection-mitigation .md+.html,
code-findings .md+.html); the two tracks **cross-link**; both HTML files are self-contained
(inline CSS/JS, no CDN/fonts/scripts/network) and every command block in the Detection HTML has a
working **Copy** button. Stats cards match the table/finding counts.
- The split is **honest**: SecureNow-runnable detections/mitigations live in the Detection report;
code/config changes live in the Code Findings report; nothing security-relevant is dropped.
- A one-line summary is printed back: per-track file paths, threat counts, rules-to-create count,
code findings by severity, gaps, framework coverage, and resolved SDK version.
- The Detection report's mitigation section presents the **full toolbox** (§4c: firewall · instant-block ·
block [global / route / method / temporary] · rate-limit [IP / route / IP+route] · challenge · auto-block ·
revoke · trusted · allowlist · fp · app-fix), and **each modeled threat's matrix row selects specific,
scoped mitigation(s) from it** — never a generic "block the IP."
- **Every false-positive-prone rule is tagged `test-first`** and carries the `--mode test` → observe (3–7
days) → `--mode prod` promotion workflow; only high-precision rules are `prod-ready`. The action plan
creates the test-first rules in `--mode test` and has an explicit "promote after N days" step.
<!-- ════════════════ END OF PROMPT ════════════════ -->