Every browser download ends with a decision about what to call the file and what to do with it. The server communicates that decision in a response header, and the client acts on it without further prompting. Content-Disposition is a small header with a surprisingly long reach: it names the file, it can force a download instead of a render, and it is frequently built by string concatenation from data an attacker supplied.
What the header contains, per the specifications
RFC 6266 defines Content-Disposition for HTTP and registers two dispositions: inline, which suggests the content be displayed, and attachment, which suggests it be saved. Both accept a filename parameter. RFC 5987 and its successor define how to encode non-ASCII parameter values, which is where the filename*=UTF-8''... form comes from.
The practical consequence of that history is that a correct implementation has to handle two parameter forms, an encoding declaration, and a quoting rule, and must decide what to do when a client understands one form but not the other. Browsers made different choices about that fallback, and those choices are the reason a header produced by naive concatenation can behave differently in two clients.
Where the filename reaches the filesystem
A download that a user saves is a file the user chose to write. That is not a vulnerability; it is the feature. The problems appear when the application, not the user, writes the file.
The first case is a server-side export job. An API endpoint generates a report and returns it with a filename derived from a request parameter. A client that saves the response with the supplied name is writing a path the server chose, and a path containing separators or traversal sequences escapes the intended directory if the client sanitises nothing.
The second case is an import. A file uploaded with a Content-Disposition header on the request side is parsed by frameworks that read that header to derive the stored name. A name with a leading separator, a Windows drive letter, or a null byte is a path traversal attempt against the upload handler, and handlers that trust the header differ in whether they normalise before or after the check.
The third case is the browser's own behaviour. A downloaded filename containing a leading dot becomes a hidden file on Unix-like systems, and one with an executable extension plus a user who double-clicks produces execution. Neither is a server bug in isolation, and both are consequences of letting a remote party name a local file.
The header that is missing
A subtler failure is the absence of Content-Disposition on a response that should be a download. When a browser receives an HTML or an image content type, it renders it. An upload endpoint that serves a user-supplied file back with the browser-detected content type can turn an uploaded HTML page into script execution in the application's origin.
That is why download endpoints commonly pair three headers: Content-Disposition: attachment, a restrictive Content-Type such as application/octet-stream, and X-Content-Type-Options: nosniff. Each closes a different gap. The disposition asks the browser to save rather than render, the content type removes the ambiguity that sniffing would resolve, and nosniff forbids the client from second-guessing the type. Removing any one of them weakens the other two.
Controls that hold
Sanitise the filename on the server, and sanitise it to an allowlist. Strip path separators, control characters, and leading dots; cap the length; and reject anything that does not match the expected pattern. Do not attempt to clean an arbitrary filename into a safe one by removing a fixed list of characters, because the list is always incomplete.
Never use a client-supplied filename as the storage path. Generate an identifier on the server, store the file under that identifier, and keep the display name as metadata. This separates the write path from the name, which removes the traversal question entirely.
Serve user content from a separate origin or a separate domain. This is the control that limits what a rendered file can do even when the disposition headers are wrong, because script in a file-serving origin has no session to reach the application with.
Set the disposition explicitly on every download route. A default that lets the framework infer it from the content is a default that changes when a user uploads something new.
Log the generated filename alongside the request identity. When a download is named from attacker-supplied input, the log is the only place the two are correlated after the fact.
The review question
For every download endpoint, the useful question is who chose the filename and who chose the directory. If the answer to either is a remote party, the endpoint is a write primitive with a friendly interface. That framing makes the fix obvious, and it also explains why the header alone is never the whole control.
References
RFC 6266, Use of the Content-Disposition Header Field in HTTP: https://www.rfc-editor.org/rfc/rfc6266
RFC 5987, Character Set and Language Encoding for HTTP Header Field Parameters: https://www.rfc-editor.org/rfc/rfc5987
MDN, Content-Disposition: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Disposition
OWASP, File Upload Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
RFC 9110, HTTP Semantics: https://www.rfc-editor.org/rfc/rfc9110