From d3804939c47304cf1e64558f1d713d3116396ae9 Mon Sep 17 00:00:00 2001
From: Samuel Williams <samuel.williams@oriontransfer.co.nz>
Date: Tue, 31 Mar 2026 17:53:11 +1300
Subject: [PATCH] Raise error for multipart requests with multiple boundary
 parameters

RFC 1341 specifies there should be a single boundary parameter.
Requests with multiple boundary parameters are unlikely to be
legitimate, and likely are attempts to exploit parsing differences
between rack and web application firewalls.

* Disallow whitespace between boundary and = when parsing multipart boundaries

Rack has historically not accepted these. To avoid security issues
when parsing multiple boundaries, check for boundary cases that may
have whitespace, but explicitly disallow the parsing if there is
whitespace.
---
 CHANGELOG.md                 |  1 +
 lib/rack/multipart.rb        |  2 +-
 lib/rack/multipart/parser.rb | 10 +++++++++-
 test/spec_multipart.rb       | 25 ++++++++++++++++++++++++-
 4 files changed, 35 insertions(+), 3 deletions(-)

--- a/lib/rack/multipart.rb
+++ b/lib/rack/multipart.rb
@@ -12,7 +12,7 @@ module Rack
 
     EOL = "\r\n"
     MULTIPART_BOUNDARY = "AaB03x"
-    MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
+    MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
     TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
     CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
     VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
--- a/lib/rack/multipart/parser.rb
+++ b/lib/rack/multipart/parser.rb
@@ -81,7 +81,15 @@ module Rack
         return unless content_type
         data = content_type.match(MULTIPART)
         return unless data
-        data[1]
+
+        unless data[1].empty?
+          raise EOFError, "whitespace between boundary parameter name and equal sign"
+        end
+        if data.post_match =~ /boundary\s*=/i
+          raise EOFError, "multiple boundary parameters found in multipart content type"
+        end
+
+        data[2]
       end
 
       def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
--- a/test/spec_multipart.rb
+++ b/test/spec_multipart.rb
@@ -37,6 +37,31 @@ describe Rack::Multipart do
     params["text"].must_equal "contents"
   end
 
+  it "raises an exception if there are multiple boundries" do
+    env = multipart_fixture(:content_type_and_no_filename)
+    env["CONTENT_TYPE"] += "; Boundary=FooBar42x"
+    env = Rack::MockRequest.env_for("/", env)
+    lambda {
+      Rack::Multipart.parse_multipart(env)
+    }.must_raise EOFError
+
+
+    env = multipart_fixture(:content_type_and_no_filename)
+    env["CONTENT_TYPE"] = "#{env["CONTENT_TYPE"].sub("boundary=", "boundary =")}; Boundary=FooBar42x"
+    env = Rack::MockRequest.env_for("/", env)
+    lambda {
+      Rack::Multipart.parse_multipart(env)
+    }.must_raise EOFError
+
+
+    env = multipart_fixture(:content_type_and_no_filename)
+    env["CONTENT_TYPE"] = "#{env["CONTENT_TYPE"].sub("boundary=", "boundary =")}; Boundary =FooBar42x"
+    env = Rack::MockRequest.env_for("/", env)
+    lambda {
+      Rack::Multipart.parse_multipart(env)
+    }.must_raise EOFError
+  end
+
   it "set US_ASCII encoding based on charset" do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename))
     params = Rack::Multipart.parse_multipart(env)
