ruby/ruby · error · ArgumentError

field content type cannot include CR/LF

Error message

field content type cannot include CR/LF

What it means

In multipart form encoding, a part's per-field content type (h[:content_type], defaulting to application/octet-stream) is written verbatim into a `Content-Type:` MIME header line. If it contains CR or LF, headers could be injected, so /[\r\n]/ triggers ArgumentError('field content type cannot include CR/LF').

Source

Thrown at lib/net/http/generic_request.rb:357

    if /[\r\n]/.match?(boundary.to_s)
      raise ArgumentError, "multipart boundary cannot include CR/LF"
    end
    chunked_p = chunked?

    buf = +''
    params.each do |key, value, h={}|
      key = quote_string(key, charset)
      filename =
        h.key?(:filename) ? h[:filename] :
        value.respond_to?(:to_path) ? File.basename(value.to_path) :
        nil

      buf << "--#{boundary}\r\n"
      if filename
        filename = quote_string(filename, charset)
        type = (h[:content_type] || 'application/octet-stream').to_s
        if /[\r\n]/.match?(type)
          raise ArgumentError, "field content type cannot include CR/LF"
        end
        buf << "Content-Disposition: form-data; " \
          "name=\"#{key}\"; filename=\"#{filename}\"\r\n" \
          "Content-Type: #{type}\r\n\r\n"
        if !out.respond_to?(:write) || !value.respond_to?(:read)
          # if +out+ is not an IO or +value+ is not an IO
          buf << (value.respond_to?(:read) ? value.read : value)
        elsif value.respond_to?(:size) && chunked_p
          # if +out+ is an IO and +value+ is a File, use IO.copy_stream
          flush_buffer(out, buf, chunked_p)
          out << "%x\r\n" % value.size if chunked_p
          IO.copy_stream(value, out)
          out << "\r\n" if chunked_p
        else
          # +out+ is an IO, and +value+ is not a File but an IO
          flush_buffer(out, buf, chunked_p)
          1 while flush_buffer(out, value.read(4096), chunked_p)
        end

View on GitHub (pinned to 0e5b888e1c)

Solutions

  1. Sanitize before use: `type = type.to_s.gsub(/[\r\n]+/, ' ').strip`.
  2. Validate against a MIME allowlist (`type =~ /\A[\w.+-]+\/[\w.+-]+\z/`) and fall back to 'application/octet-stream'.
  3. Never forward raw client headers into multipart metadata.

Example fix

# before
req.set_multipart_form_data([['f', io, { content_type: untrusted_ct }]])

# after
ct = untrusted_ct.to_s
ct = 'application/octet-stream' unless ct.match?(%r{\A[\w.+\-/]+\/[\w.+\-/]+\z})
req.set_multipart_form_data([['f', io, { content_type: ct }]])
Defensive patterns

Strategy: validation

Validate before calling

type = (h[:content_type] || 'application/octet-stream').to_s
type = 'application/octet-stream' unless type.match?(%r{\A[\w.+\-/;= ]+\z})
opt = opt.merge(content_type: type)

Type guard

def safe_content_type?(t)
  !t.to_s.match?(/[\r\n]/)
end

Prevention

When it happens

Trigger: `req.set_multipart_form_data([['file', file, { content_type: "text/plain\r\nX-Evil: 1" }]])`. Passing content_type strings built from untrusted metadata (uploaded file content-type headers, EXIF, or filename-derived guesses) that embed newlines. Helpers that forward a content_type option verbatim.

Common situations: Server code echoing a client-supplied Content-Type back into an outgoing multipart upload (proxying uploads). Fuzz/security tests deliberately probing CRLF injection surfaces. MIME maps or config containing embedded newlines.

Related errors


AI-assisted analysis of ruby/ruby@0e5b888e1c (2026-08-21). Data as JSON: /api/errors/dedc87582844067d. Report an issue: GitHub.