Auditing file upload and content handling: when the type one layer trusts another layer contradicts
A file upload becomes a vulnerability when the layer that decides a file's type disagrees with the layer
that later serves or parses it. The upload validator trusts the extension or the declared content-type;
the web server sniffs the bytes, or the storage path serves it inline, or a parser acts on embedded
content, and the two disagreements meet in the attacker's favor. You audit it by resolving how the type is
decided at upload and how the file is served and parsed afterward, then finding where those two views
diverge. The strongest false-positive killers live in the serve layer: a no-execute sandboxed origin with
a forced download, or a server-side re-encode, neutralizes the content regardless of its bytes. The
client-side DOM sink and the archive-extraction write primitive have their own skills; this skill owns the
upload-to-serve-or-parse decision.
When to use
- You are reviewing an upload endpoint, its type validation, where files are stored, and how they are served.
- A file's bytes, declared type, name, or embedded content reach a parser, a renderer, or a served location.
- You want to know whether an uploaded file executes or is parsed dangerously somewhere downstream.
Scope check
Audit only applications you own or are authorized to assess, and upload a crafted file only to a system in
scope, a stored active file can execute against real users. Adjudicate on the validation, the storage, and
the serve path. If you can't name the authorization, stop.
The loop
Resolve the type-decision layer and the serve behavior first. Determine how the upload decides the
file's type (an extension check, a declared content-type, a magic-byte sniff, or a combination) and how
the file is served afterward (inline or as an attachment, from the app origin or a separate no-execute
origin, with what content-type). The bug is a divergence between these two, so establish both before
judging any single check.
Check the type-decision contradiction. Look for an extension or a client-declared content-type
trusted for the type decision while a second layer (the web server's own sniffing, a content-disposition
header, or the storage path's handler) treats the file differently, so a double extension, a null-byte
or case trick, or an appended-data file is stored as one type and served as another.
Check active-content serving. Look for an SVG, HTML, or XML file stored and served inline as active
content, giving a stored cross-site-scripting or an external-entity parse, when it should be served as an
inert attachment or sanitized.
Check parser exploitation. Look for an uploaded image or document fed to a parser or converter that
acts on its embedded content (a delegate command, an external reference, an embedded script), turning a
crafted file into code execution or a server-side request in the parser.
Check polyglots, path, and archives. Look for a polyglot file that passes one content check yet
executes in another context, an upload path that writes outside the intended directory (traversal in the
filename), and an archive uploaded and expanded to a write primitive (hand the extraction internals to
the archive-extraction skill, keep the upload framing here).
Confirm and record. Confirm by uploading a crafted file and showing it is stored and then executed or
parsed dangerously along the reachable path. Kill the lead if the file is served from a no-execute
separate origin with a forced attachment disposition and a fixed non-active content-type so the execution
path is cut regardless of the bytes, if the server re-encodes the file (an image recompressed through a
hardened pipeline) stripping any polyglot or parser payload, if the type is decided by a magic-byte
allowlist and the extension and the serve layer all agree, if the stored filename is randomized and the
path is outside the web root so a direct request cannot execute it, if the parser is hardened (delegates
and external entities disabled) so the crafted content is inert, or if an SVG is sanitized or served with
a policy that blocks inline script. Record the upload, the type divergence, and the serve or parse sink.
Where content handling leaks
- The bug is a divergence, not a single check. The upload validator and the serve or parse layer holding
different views of the file's type is the vulnerability; one layer alone rarely tells you.
- The serve layer is the strongest killer. A no-execute sandboxed origin with a forced download, or a
fixed inert content-type, neutralizes the file whatever its bytes; resolve the serve behavior first.
- Re-encoding strips payloads. Recompressing an image through a hardened pipeline destroys polyglot and
parser content; a validated-then-stored-verbatim file does not.
- Active content served inline is stored injection. An SVG, HTML, or XML served inline runs or parses;
the same file forced as an attachment from a sandbox origin does not.
- The parser is a sink too. An image or document converter that acts on embedded delegates or external
references is code execution or a server-side request; hardening the parser closes it.
Worked example (a confirm and a kill)
Confirm. An avatar upload validates only the file extension, stores the file under the application's
web root with its original name, and the server serves it with a sniffable content-type. An attacker
uploads an SVG (named to pass the extension check) containing a script; fetching the stored file executes
the script in the application origin. Confirmed stored cross-site-scripting through an upload validated
by extension and served inline as active content, high, remediation = serve user uploads as inert
attachments from a separate no-execute origin, decide type by magic bytes, and sanitize or re-encode.
Kill. The same upload re-encodes the image to a fixed format through a hardened pipeline, stores it
under a randomized name outside the web root, and serves it from a separate user-content origin with a
forced attachment disposition and a fixed non-active content-type. The crafted bytes do not survive the
re-encode and the file cannot execute where it is served. Killed, kill_reason = "the file is
re-encoded and served no-execute cross-origin with a forced download, so the content-handling sink is
neutralized regardless of the uploaded bytes."
Rationalizations to reject
- "We check the extension." -> Does the serve layer agree? A double extension or a sniffed type diverges
from the extension; the contradiction between the layers is the bug.
- "It is just an image." -> Served inline from the app origin, or re-encoded and served as an attachment
from a sandbox origin? An SVG or a polyglot served inline is active content.
- "The parser is standard." -> Are its delegates and external entities disabled? A converter that acts on
embedded content is a code-execution or server-side-request sink.
- "The filename is validated." -> Against traversal, and stored where? A traversal in the name or a
web-root path lets a direct request reach the file.
- "It is a zip we extract." -> The extraction write primitive is the archive-extraction skill's; here
confirm the upload accepts and routes it, and hand the expansion there.
Executing this in practice
You need the upload endpoint and its type validation, whether the file is re-encoded, where it is stored
(web root or not, original or randomized name), how it is served (inline or attachment, which origin, which
content-type), and any parser or converter the content reaches. For each upload, decide whether the
type-decision layer and the serve or parse layer diverge in the attacker's favor. Reading the validator
tells you what is accepted; reading the serve path tells you what happens to it.
Related
hunting-unsafe-archive-extraction - owns the archive-expansion write primitive (path traversal and
resource exhaustion on extract); this skill routes an uploaded archive there rather than duplicating it.
testing-client-side-dom-vulnerabilities - owns the client-side DOM sink taxonomy; the SVG-as-active-
content shape here is the server-served, stored variant, framed as content handling rather than a DOM sink.
reviewing-content-security-policy - the policy that blocks inline script in served active content is a
mitigating layer this skill checks when adjudicating an SVG or HTML upload.
- FINDING-SCHEMA.md - source = an uploaded file, sink = a serve or parse operation
acting on its content, evidence = the type-decision layer, the serve behavior, and the divergence between
them.
1---2name: auditing-file-upload-and-content-handling3description: Audit a file-upload and content-handling path for an attacker-supplied file whose bytes, declared type, name, or embedded content reach a sink that stores it in a served or executable location or feeds it to a parser that acts on its content, after the type-decision layer and the serve behavior are resolved. Covers an extension or content-type trusted for a type decision that a second layer contradicts, an SVG, HTML, or XML file stored and served inline as active content, image or document parser exploitation, a polyglot file passing one content check yet executing in another context, an upload path writing outside the intended directory, and an archive expanding to a write primitive. Use when reviewing upload validation, storage, and serving, not the client-side DOM sink or the archive-extraction write primitive their own skills own. An uploaded file is the source, a serve or parse sink acting on its content is the sink, and a type decision one layer contradicts is the bug.4license: MIT5---67# Auditing file upload and content handling: when the type one layer trusts another layer contradicts89A file upload becomes a vulnerability when the layer that decides a file's type disagrees with the layer10that later serves or parses it. The upload validator trusts the extension or the declared content-type;11the web server sniffs the bytes, or the storage path serves it inline, or a parser acts on embedded12content, and the two disagreements meet in the attacker's favor. You audit it by resolving how the type is13decided at upload and how the file is served and parsed afterward, then finding where those two views14diverge. The strongest false-positive killers live in the serve layer: a no-execute sandboxed origin with15a forced download, or a server-side re-encode, neutralizes the content regardless of its bytes. The16client-side DOM sink and the archive-extraction write primitive have their own skills; this skill owns the17upload-to-serve-or-parse decision.1819## When to use2021- You are reviewing an upload endpoint, its type validation, where files are stored, and how they are served.22- A file's bytes, declared type, name, or embedded content reach a parser, a renderer, or a served location.23- You want to know whether an uploaded file executes or is parsed dangerously somewhere downstream.2425## Scope check2627Audit only applications you own or are authorized to assess, and upload a crafted file only to a system in28scope, a stored active file can execute against real users. Adjudicate on the validation, the storage, and29the serve path. If you can't name the authorization, stop.3031## The loop32331. **Resolve the type-decision layer and the serve behavior first.** Determine how the upload decides the34 file's type (an extension check, a declared content-type, a magic-byte sniff, or a combination) and how35 the file is served afterward (inline or as an attachment, from the app origin or a separate no-execute36 origin, with what content-type). The bug is a divergence between these two, so establish both before37 judging any single check.38392. **Check the type-decision contradiction.** Look for an extension or a client-declared content-type40 trusted for the type decision while a second layer (the web server's own sniffing, a content-disposition41 header, or the storage path's handler) treats the file differently, so a double extension, a null-byte42 or case trick, or an appended-data file is stored as one type and served as another.43443. **Check active-content serving.** Look for an SVG, HTML, or XML file stored and served inline as active45 content, giving a stored cross-site-scripting or an external-entity parse, when it should be served as an46 inert attachment or sanitized.47484. **Check parser exploitation.** Look for an uploaded image or document fed to a parser or converter that49 acts on its embedded content (a delegate command, an external reference, an embedded script), turning a50 crafted file into code execution or a server-side request in the parser.51525. **Check polyglots, path, and archives.** Look for a polyglot file that passes one content check yet53 executes in another context, an upload path that writes outside the intended directory (traversal in the54 filename), and an archive uploaded and expanded to a write primitive (hand the extraction internals to55 the archive-extraction skill, keep the upload framing here).56576. **Confirm and record.** Confirm by uploading a crafted file and showing it is stored and then executed or58 parsed dangerously along the reachable path. Kill the lead if the file is served from a no-execute59 separate origin with a forced attachment disposition and a fixed non-active content-type so the execution60 path is cut regardless of the bytes, if the server re-encodes the file (an image recompressed through a61 hardened pipeline) stripping any polyglot or parser payload, if the type is decided by a magic-byte62 allowlist and the extension and the serve layer all agree, if the stored filename is randomized and the63 path is outside the web root so a direct request cannot execute it, if the parser is hardened (delegates64 and external entities disabled) so the crafted content is inert, or if an SVG is sanitized or served with65 a policy that blocks inline script. Record the upload, the type divergence, and the serve or parse sink.6667## Where content handling leaks6869- **The bug is a divergence, not a single check.** The upload validator and the serve or parse layer holding70 different views of the file's type is the vulnerability; one layer alone rarely tells you.71- **The serve layer is the strongest killer.** A no-execute sandboxed origin with a forced download, or a72 fixed inert content-type, neutralizes the file whatever its bytes; resolve the serve behavior first.73- **Re-encoding strips payloads.** Recompressing an image through a hardened pipeline destroys polyglot and74 parser content; a validated-then-stored-verbatim file does not.75- **Active content served inline is stored injection.** An SVG, HTML, or XML served inline runs or parses;76 the same file forced as an attachment from a sandbox origin does not.77- **The parser is a sink too.** An image or document converter that acts on embedded delegates or external78 references is code execution or a server-side request; hardening the parser closes it.7980## Worked example (a confirm and a kill)8182> **Confirm.** An avatar upload validates only the file extension, stores the file under the application's83> web root with its original name, and the server serves it with a sniffable content-type. An attacker84> uploads an SVG (named to pass the extension check) containing a script; fetching the stored file executes85> the script in the application origin. **Confirmed** stored cross-site-scripting through an upload validated86> by extension and served inline as active content, `high`, remediation = serve user uploads as inert87> attachments from a separate no-execute origin, decide type by magic bytes, and sanitize or re-encode.88>89> **Kill.** The same upload re-encodes the image to a fixed format through a hardened pipeline, stores it90> under a randomized name outside the web root, and serves it from a separate user-content origin with a91> forced attachment disposition and a fixed non-active content-type. The crafted bytes do not survive the92> re-encode and the file cannot execute where it is served. **Killed**, `kill_reason` = "the file is93> re-encoded and served no-execute cross-origin with a forced download, so the content-handling sink is94> neutralized regardless of the uploaded bytes."9596## Rationalizations to reject9798- *"We check the extension."* -> Does the serve layer agree? A double extension or a sniffed type diverges99 from the extension; the contradiction between the layers is the bug.100- *"It is just an image."* -> Served inline from the app origin, or re-encoded and served as an attachment101 from a sandbox origin? An SVG or a polyglot served inline is active content.102- *"The parser is standard."* -> Are its delegates and external entities disabled? A converter that acts on103 embedded content is a code-execution or server-side-request sink.104- *"The filename is validated."* -> Against traversal, and stored where? A traversal in the name or a105 web-root path lets a direct request reach the file.106- *"It is a zip we extract."* -> The extraction write primitive is the archive-extraction skill's; here107 confirm the upload accepts and routes it, and hand the expansion there.108109## Executing this in practice110111You need the upload endpoint and its type validation, whether the file is re-encoded, where it is stored112(web root or not, original or randomized name), how it is served (inline or attachment, which origin, which113content-type), and any parser or converter the content reaches. For each upload, decide whether the114type-decision layer and the serve or parse layer diverge in the attacker's favor. Reading the validator115tells you what is accepted; reading the serve path tells you what happens to it.116117## Related118119- `hunting-unsafe-archive-extraction` - owns the archive-expansion write primitive (path traversal and120 resource exhaustion on extract); this skill routes an uploaded archive there rather than duplicating it.121- `testing-client-side-dom-vulnerabilities` - owns the client-side DOM sink taxonomy; the SVG-as-active-122 content shape here is the server-served, stored variant, framed as content handling rather than a DOM sink.123- `reviewing-content-security-policy` - the policy that blocks inline script in served active content is a124 mitigating layer this skill checks when adjudicating an SVG or HTML upload.125- [FINDING-SCHEMA.md](../../FINDING-SCHEMA.md) - source = an uploaded file, sink = a serve or parse operation126 acting on its content, evidence = the type-decision layer, the serve behavior, and the divergence between127 them.