Repository: rack/rack (Ruby)

Issue:
Is a boundary delimiter-only body an invalid request?
Sending the following request in a chrome browser will generate a boundary request with only delimiter-line.

```javascript
const formData = new FormData();
const request = new Request('http://127.0.0.1:8080/', {
  method: 'POST',
  body: formData,
});
const response = fetch(request);
```

```
curl 'http://127.0.0.1:8080/' -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryR1LC4tR6ayskIXJm' --data-raw $'------WebKitFormBoundaryR1LC4tR6ayskIXJm--\r\n'
```

This request causes `Rack::Multipart::EmptyContentError` in rack application.

As @jeremyevans mentions, it doesn't seem to match the RFC2046 specification.
https://github.com/rack/rack/issues/1603#issuecomment-683124204

> The body must then contain one or more body parts, each preceded by a boundary delimiter line, and the last one followed by a closing boundary delimiter line.

Unlike https://github.com/rack/rack/issues/1603#issuecomment-683101436, the body described in this issue is not completely empty and contains a delimiter.
I am creating this issue because I think there is a possibility that delimiter-only requests have not been considered before.

But as far as I read RFC2046, is it a bug in chromium?



Discussion:
For RFC 2046, it appears invalid:

> The body must then contain
> one or more body parts, each preceded by a boundary delimiter line,

If you consider the updated RFC (RFC 7578), it still appears invalid because RFC 7578 references section 5.1 of RFC 2046:

> The media type multipart/form-data follows the model of multipart
> MIME data streams as specified in [Section 5.1 of [RFC2046]](https://datatracker.ietf.org/doc/html/rfc2046#section-5.1); changes
> are noted in this document.

RFC 7578  does not state that zero parts are allowed as a change from RFC2046.

That being said, it doesn't appear to be just an issue with Chrome, I see identical behavior with Firefox.  So while not blessed by the RFCs, as a practical matter, I'm open to supporting this and having the multipart parser return an empty value (e.g. `{}`).



Patch:
diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb
index 284210fda..a05a6a1c5 100644
--- a/lib/rack/multipart/parser.rb
+++ b/lib/rack/multipart/parser.rb
@@ -209,6 +209,7 @@ def initialize(boundary, tempfile, bufsize, query_parser)
         @sbuf = StringScanner.new("".dup)
         @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
         @body_regex_at_end = /#{@body_regex}\z/m
+        @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
         @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
         @head_regex = /(.*?#{EOL})#{EOL}/m
       end
@@ -275,7 +276,14 @@ def handle_fast_forward
             @state = :MIME_HEAD
             return
           when :END_BOUNDARY
-            # invalid multipart upload, but retry for opening boundary
+            # invalid multipart upload
+            if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
+              # stop parsing a buffer if a buffer is only an end boundary.
+              @state = :DONE
+              return
+            end
+
+            # retry for opening boundary
           else
             # no boundary found, keep reading data
             return :want_read


Test patch:
diff --git a/test/spec_request.rb b/test/spec_request.rb
index bfaa0e9d7..661c45708 100644
--- a/test/spec_request.rb
+++ b/test/spec_request.rb
@@ -1363,6 +1363,24 @@ def initialize(*)
     req.env['rack.request.form_pairs'].must_equal [["reply", "yes"], ["fileupload", f]]
   end
 
+  it "parse multipart delimiter-only boundary" do
+    input = <<EOF
+--AaB03x--\r
+EOF
+    mr = Rack::MockRequest.env_for(
+      "/",
+      "CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x",
+      "CONTENT_LENGTH" => input.size,
+      :input => input
+    )
+
+    req = make_request mr
+    req.query_string.must_equal ""
+    req.GET.must_be :empty?
+    req.POST.must_be :empty?
+    req.params.must_equal({})
+  end
+
   it "MultipartPartLimitError when request has too many multipart file parts if limit set" do
     begin
       data = 10000.times.map { "--AaB03x\r\ncontent-type: text/plain\r\ncontent-disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n")