Skip to content

Commit bc74e65

Browse files
jeremyevanswtn
authored andcommitted
Implement OBS unfolding for multipart requests per RFC 5322 2.2.3
Do this for both the Content-Disposition and Content-Type lines. Co-authored-by: "William T. Nelson" <35801+wtn@users.noreply.github.com>
1 parent b26a314 commit bc74e65

3 files changed

Lines changed: 32 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ All notable changes to this project will be documented in this file. For info on
1919
- [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
2020
- [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
2121
- [CVE-2026-34827](https://github.com/advisories/GHSA-v6x5-cg8r-vv6x) Quadratic-time multipart header parsing allows denial of service via escape-heavy quoted parameters.
22+
- [CVE-2026-26962](https://github.com/advisories/GHSA-rx22-g9mx-qrhv) Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
2223

2324
### SPEC Changes
2425

lib/rack/multipart/parser.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,13 +373,21 @@ def handle_consume_token
373373

374374
CONTENT_DISPOSITION_MAX_PARAMS = 16
375375
CONTENT_DISPOSITION_MAX_BYTES = 1536
376+
OBS_UNFOLD = /\r\n([ \t])/
377+
private_constant :OBS_UNFOLD
378+
376379
def handle_mime_head
377380
if @sbuf.scan_until(@head_regex)
378381
head = @sbuf[1]
379382
content_type = head[MULTIPART_CONTENT_TYPE, 1]
383+
content_type.gsub!(OBS_UNFOLD, '\1') if content_type
384+
380385
if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
381386
disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
382387

388+
# Implement OBS unfolding (RFC 5322 Section 2.2.3)
389+
disposition.gsub!(OBS_UNFOLD, '\1')
390+
383391
# ignore actual content-disposition value (should always be form-data)
384392
if i = disposition.index(';')
385393
disposition.slice!(0, i + 1)

test/spec_multipart.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,4 +1440,27 @@ def initialize(*)
14401440
params = Rack::Multipart.parse_multipart(env)
14411441
params["field"].must_equal "data"
14421442
end
1443+
1444+
it "prevents CRLF injection in parameter values via obs-fold" do
1445+
data = <<~EOF
1446+
--AaB03x\r
1447+
Content-Disposition: form-data; name="upload"; filename="test\r
1448+
\t.txt"\r
1449+
Content-Type: application/octet-stream;\r
1450+
name="file.php"\r
1451+
\r
1452+
<?php eval($_POST['x']); ?>\r
1453+
--AaB03x--\r
1454+
EOF
1455+
1456+
options = {
1457+
"CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
1458+
"CONTENT_LENGTH" => data.length.to_s,
1459+
:input => StringIO.new(data)
1460+
}
1461+
env = Rack::MockRequest.env_for("/", options)
1462+
params = Rack::Multipart.parse_multipart(env)
1463+
params["upload"][:filename].must_equal "test\t.txt"
1464+
params["upload"][:type].must_equal 'application/octet-stream; name="file.php"'
1465+
end
14431466
end

0 commit comments

Comments
 (0)