CVE-2026-49756
Description
Improper Neutralization of CRLF Sequences ('CRLF Injection') vulnerability in wojtekmach Req allows multipart parameter smuggling via attacker-influenced part metadata.
Req.Utils.encode_form_part/2 in lib/req/utils.ex builds the per-part headers by interpolating the caller-supplied name, filename, and content_type values directly into the content-disposition and content-type lines with no escaping or CRLF stripping. A value containing ", \r, or \n closes the surrounding quoted value and starts a new header line; an additional \r\n-- terminates the current part and prepends a smuggled part of the attacker's choosing.
This is reachable through every supported way of supplying a part. It is particularly easy when value is a %File.Stream{}, because filename then defaults to Path.basename(stream.path) and POSIX filenames may legitimately contain \r and \n. Any application that forwards user-controlled filenames (or field names / MIME types) through Req.post/2 with form_multipart: lets an attacker inject arbitrary headers into the outgoing multipart body or smuggle additional fields and parts into the request the victim service sends downstream.
This issue affects req: from 0.5.3 before 0.6.0.
Affected products
2(expand)+ 1 more
- (no CPE)
- (no CPE)range: >=0.5.3 <0.6.0
Patches
174506ff2c5ad`encode_body`: Security fix for `:form_multipart` header injection
3 files changed · +43 −4
CHANGELOG.md+11 −0 modified@@ -2,6 +2,17 @@ ## Unreleased + * [`encode_body`]: Security fix for `:form_multipart` header injection + ([GHSA-px9f-whj3-246m](https://github.com/wojtekmach/req/security/advisories/GHSA-px9f-whj3-246m)). + + The multipart encoder interpolated the per-part `name`, `filename`, and + `content_type` into the part headers without escaping, so an + attacker-controlled value could inject extra headers or smuggle additional + parts into the request. These values are now escaped per RFC 7578 / WHATWG + form-data (`"`, CR, and LF are percent-encoded). + + Thanks to @PJUllrich for reporting it. + ## v0.5.18 (2026-05-20) * [`run_finch`]: Allow :finch option with IPv6 URLs.
lib/req/utils.ex+10 −4 modified@@ -534,7 +534,7 @@ defmodule Req.Utils do {Stream.concat(parts1, parts2), add_sizes(size1, size2)} end - defp encode_form_part({name, {value, options}}, boundary) do + defp encode_form_part({name, {value, options}}, boundary) when is_atom(name) do options = Keyword.validate!(options, [:filename, :content_type, :size]) {parts, parts_size, options} = @@ -574,18 +574,19 @@ defmodule Req.Utils do params = if filename = options[:filename] do - ["; filename=\"", filename, "\""] + ["; filename=\"", escape_form_param(filename), "\""] else [] end headers = if content_type = options[:content_type] do - ["content-type: ", content_type, @crlf] + ["content-type: ", escape_form_param(content_type), @crlf] else [] end + name = escape_form_param(Atom.to_string(name)) headers = ["content-disposition: form-data; name=\"#{name}\"", params, @crlf, headers] header = [["--", boundary, @crlf, headers, @crlf]] @@ -594,10 +595,15 @@ defmodule Req.Utils do |> add_form_parts({[@crlf], 2}) end - defp encode_form_part({name, value}, boundary) do + defp encode_form_part({name, value}, boundary) when is_atom(name) do encode_form_part({name, {value, []}}, boundary) end + # Per RFC 7578 / WHATWG form-data behavior, escape `"`, CR, and LF. + defp escape_form_param(value) when is_binary(value) do + URI.encode(value, &(&1 not in [?", ?\r, ?\n])) + end + @doc """ Loads .netrc file.
test/req/utils_test.exs+22 −0 modified@@ -231,6 +231,28 @@ defmodule Req.UtilsTest do """ end + test "escapes name, filename, and content_type to prevent header injection" do + %{body: body} = + Req.Utils.encode_form_multipart( + [ + "na\"me\r\nX-Evil: 1": + {"value", filename: ~s(ev"il\r\n--foo\r\n), content_type: "text/plain\r\nX-Evil: 2"} + ], + boundary: "foo" + ) + + body = IO.iodata_to_binary(body) + + assert body == """ + --foo\r\n\ + content-disposition: form-data; name=\"na%22me%0D%0AX-Evil: 1\"; filename=\"ev%22il%0D%0A--foo%0D%0A\"\r\n\ + content-type: text/plain%0D%0AX-Evil: 2\r\n\ + \r\n\ + value\r\n\ + --foo--\r\n\ + """ + end + test "it works with size" do %{content_type: content_type, body: body, size: size} = Req.Utils.encode_form_multipart([field1: {"value", size: 5}], boundary: "foo")
Vulnerability mechanics
Root cause
"The multipart encoder did not escape special characters in user-supplied part metadata."
Attack vector
An attacker can influence the `name`, `filename`, or `content_type` parameters when constructing a multipart form request. By including CRLF sequences or quotes within these parameters, an attacker can inject arbitrary headers or smuggle additional parts into the outgoing request [ref_id=1]. This is particularly effective when filenames are derived from user-controlled paths, as POSIX filenames can legitimately contain CRLF characters [ref_id=1].
Affected code
The vulnerability resides in the `Req.Utils.encode_form_part/2` function within `lib/req/utils.ex`. This function directly interpolates user-supplied `name`, `filename`, and `content_type` values into the multipart headers without proper sanitization or escaping [ref_id=1]. The fix is applied in the same file, introducing the `escape_form_param/1` helper function.
What the fix does
The patch modifies the `encode_form_part/2` function to escape special characters in the `name`, `filename`, and `content_type` parameters using `URI.encode/2` [patch_id=5238292]. This ensures that characters like double quotes, carriage returns, and newlines are percent-encoded, preventing them from breaking out of quoted values and injecting new headers or parts into the multipart body, adhering to RFC 7578 / WHATWG form-data specifications [ref_id=2].
Generated on Jun 8, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.