skip to content

Why can a CSRF token filter that runs before body parsing not validate a token carried in a multipart/form-data body?

level: seniorimportance: should knowfreq 46%

answer

  1. the value lives inside the body
  2. nothing to compare before parsing
  3. order of filter and body parser
  4. bytes accepted before provenance is known
  5. or demand a script-set request header

basics

~20 s

The value exists only once the body has been parsed. A check that runs ahead of the parser sees headers and an unread byte stream, so it has nothing to compare - and running it after the parser means the upload is already buffered or stored before provenance is known.

solid answer

~40 s

A `multipart/form-data` body is a sequence of parts separated by a boundary declared in `Content-Type`, and a form field inside it is readable only after something walks that structure. A filter placed ahead of the body-parsing stage has the request line, the headers and an unread stream; the field it wants is somewhere in the middle of several megabytes of photograph. So the filter either runs after parsing, which means the upload has been buffered in memory or streamed to temporary storage before anyone knows the request is legitimate, or it requires the value in a request header that the submitting page's own script sets, which a plain cross-site form submission cannot produce. Both are defensible; the cost is different and neither is free.

code

http · 16 lines
http
POST /claims/4812/documents HTTP/1.1
Host: claims.example.com
Content-Type: multipart/form-data; boundary=----Xa9f2
Content-Length: 5241880
Cookie: session=8c1d...

------Xa9f2
Content-Disposition: form-data; name="csrf_token"

9f1c4b77e4
------Xa9f2
Content-Disposition: form-data; name="photo"; filename="damage-1.jpg"
Content-Type: image/jpeg

(5 MB of image data)
------Xa9f2--

go deeper

for a junior

Hold on to the shape: a multipart body is a series of boundary-separated parts, and a field inside it is not available until something reads that structure.

for a middle

Explain the ordering as a dependency, not a preference - the check consumes an output of the parser, so it cannot precede it, and the alternative is to carry the value where it is readable sooner.

for a senior

Argue the trade explicitly: an upload buffered or stored before provenance is known is a real cost, and the mitigations are body-size limits, temporary storage and a cleanup path on the rejection branch.

for a principal

Treat it as a contract on the submission surface. Deciding that write endpoints carry the value in a header and never in a body shapes every form, every client and every size limit downstream of that choice.

An insurance claim form posts photographs of the damage, so it posts `multipart/form-data`. The forgery token travels as one field of that body, alongside the files. That one decision determines where in the request pipeline the token check can possibly sit. ## What a multipart body is, and when a field inside it exists A `multipart/form-data` body, defined by RFC 7578, is a sequence of parts separated by a delimiter built from the `boundary` parameter carried in the `Content-Type` header field. Each part has its own headers - a `Content-Disposition` naming the form control, and for a file part a `filename` and its own `Content-Type` - followed by that part's bytes. The parts appear in the order the controls appear in the form. The consequence is blunt. Before something walks that structure there is no field named `csrf_token`; there is a stream of bytes whose shape is not yet known. A filter placed ahead of the body-parsing stage has the method, the target, the header fields and a socket it has not read. It has nothing to compare, so the check it would perform is not merely awkward - it has no input. ## The three placements, and what each costs 1. **Run the check after body parsing.** The token is readable and the check is exact. The price is that the whole submission - five megabytes of photographs, or fifty - has been accepted before the server knows the request came from a page it rendered. The bytes are buffered in memory or streamed to temporary storage, and if the check then fails, everything that was written is discarded. 2. **Require the value in a request header for this submission.** The header is in front of the body, so the check can run before a single byte of the upload is read, and a rejection costs nothing but the headers. The price is that a plain HTML form cannot add an arbitrary header to its own submission, so the page has to submit through its own script rather than as a document form - which is exactly why the header is a meaningful proof of provenance, since a cross-site document cannot add it either. 3. **Stream the body and check as soon as the field appears.** A parser that consumes parts in order can hand the filter the token part the moment it completes, and if the hidden control is placed before the file control in the markup, that happens after a few hundred bytes. This narrows the window considerably rather than closing it, and it couples the check to the author's field ordering, which is a fragile thing to depend on. | Placement | Can it read a body token? | Bytes accepted before the verdict | |---|---|---| | Before the body parser | No - the field does not exist yet | none | | After the body parser | Yes | the entire upload | | Streaming, part by part | Yes, once that part completes | everything up to that part | | Header instead of body | Not applicable - no body read | none | ## Why 'just reject it earlier' is not available A server cannot refuse a request it has not received. Even a rejection issued the instant the headers arrive usually involves draining or closing the connection, and a client that has begun transmitting a large body does not learn about the refusal in time to stop sending. So the honest framing is not 'how do I avoid receiving the upload' but 'how much of it am I willing to accept, and where do I put it, before I know the request is legitimate'. That is a capacity question as much as a security one: temporary storage and request-body limits are what keep a forged or hostile upload from being a resource problem, and they belong in the same conversation as the placement. ## What this does not license Moving the value into the URL query string to make it readable early is a trade you should refuse. Query strings end up in server logs, in proxy logs and in the referrer a browser may attach to the next request, and a forgery token that leaks is not a token. Keep it in the body or in a request header, and choose the placement of the check to match.

  • If the check must run after parsing, what limits keep a forged upload from becoming a capacity problem?
    A maximum request-body size and a maximum per-part size, enforced by the parser itself so it abandons the stream rather than filling memory; temporary storage on disk rather than in the heap for large parts; and a cleanup path that deletes those temporary files on the rejection branch, which is the one most often forgotten because it is the branch nobody exercises.
  • Why is a value carried in a request header readable earlier than one carried in a body field?
    Header fields arrive ahead of the body and are parsed as part of receiving the message, so they are available before any body byte is read. A body field only exists after something has interpreted the body's structure, which for a multipart submission means walking boundary-separated parts.
  • Does placing the hidden field before the file control in the markup solve the problem?
    It narrows the window rather than closing it. Parts arrive in control order, so a streaming parser can complete the token part after a few hundred bytes and reject early. But the check still runs inside the parse, and the guarantee now rests on the order of two elements in a template - which a later edit can silently reverse.

saying these in an interview costs you the question

  • Thinks a filter can read a body field before anything parses the body
  • Claims the server can refuse a multipart upload without receiving any of it
  • Moves the value into the URL query string to make it readable earlier
  • Says parse order is an implementation detail with no cost
  • Assumes a header-carried value is also unreadable until the body is parsed