{"record":{"id":"dedc87582844067d","repo":"ruby/ruby","slug":"field-content-type-cannot-include-cr-lf","errorCode":null,"errorMessage":"field content type cannot include CR/LF","messagePattern":"field content type cannot include CR/LF","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/net/http/generic_request.rb","lineNumber":357,"sourceCode":"    if /[\\r\\n]/.match?(boundary.to_s)\n      raise ArgumentError, \"multipart boundary cannot include CR/LF\"\n    end\n    chunked_p = chunked?\n\n    buf = +''\n    params.each do |key, value, h={}|\n      key = quote_string(key, charset)\n      filename =\n        h.key?(:filename) ? h[:filename] :\n        value.respond_to?(:to_path) ? File.basename(value.to_path) :\n        nil\n\n      buf << \"--#{boundary}\\r\\n\"\n      if filename\n        filename = quote_string(filename, charset)\n        type = (h[:content_type] || 'application/octet-stream').to_s\n        if /[\\r\\n]/.match?(type)\n          raise ArgumentError, \"field content type cannot include CR/LF\"\n        end\n        buf << \"Content-Disposition: form-data; \" \\\n          \"name=\\\"#{key}\\\"; filename=\\\"#{filename}\\\"\\r\\n\" \\\n          \"Content-Type: #{type}\\r\\n\\r\\n\"\n        if !out.respond_to?(:write) || !value.respond_to?(:read)\n          # if +out+ is not an IO or +value+ is not an IO\n          buf << (value.respond_to?(:read) ? value.read : value)\n        elsif value.respond_to?(:size) && chunked_p\n          # if +out+ is an IO and +value+ is a File, use IO.copy_stream\n          flush_buffer(out, buf, chunked_p)\n          out << \"%x\\r\\n\" % value.size if chunked_p\n          IO.copy_stream(value, out)\n          out << \"\\r\\n\" if chunked_p\n        else\n          # +out+ is an IO, and +value+ is not a File but an IO\n          flush_buffer(out, buf, chunked_p)\n          1 while flush_buffer(out, value.read(4096), chunked_p)\n        end","sourceCodeStart":339,"sourceCodeEnd":375,"githubUrl":"https://github.com/ruby/ruby/blob/0e5b888e1c355f3f728f2659f085820937dada48/lib/net/http/generic_request.rb#L339-L375","documentation":"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').","triggerScenarios":"`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.","commonSituations":"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.","solutions":["Sanitize before use: `type = type.to_s.gsub(/[\\r\\n]+/, ' ').strip`.","Validate against a MIME allowlist (`type =~ /\\A[\\w.+-]+\\/[\\w.+-]+\\z/`) and fall back to 'application/octet-stream'.","Never forward raw client headers into multipart metadata."],"exampleFix":"# before\nreq.set_multipart_form_data([['f', io, { content_type: untrusted_ct }]])\n\n# after\nct = untrusted_ct.to_s\nct = 'application/octet-stream' unless ct.match?(%r{\\A[\\w.+\\-/]+\\/[\\w.+\\-/]+\\z})\nreq.set_multipart_form_data([['f', io, { content_type: ct }]])","handlingStrategy":"validation","validationCode":"type = (h[:content_type] || 'application/octet-stream').to_s\ntype = 'application/octet-stream' unless type.match?(%r{\\A[\\w.+\\-/;= ]+\\z})\nopt = opt.merge(content_type: type)","typeGuard":"def safe_content_type?(t)\n  !t.to_s.match?(/[\\r\\n]/)\nend","tryCatchPattern":null,"preventionTips":["Validate content types against a MIME token regex before passing them into multipart APIs.","Never echo raw client Content-Type headers into outgoing multipart metadata."],"tags":["ruby","net-http","multipart","crlf-injection","security","content-type","stdlib"],"backgroundTag":"crlf-injection","analyzedSha":"0e5b888e1c355f3f728f2659f085820937dada48","analyzedAt":"2026-08-21T14:25:43.473Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}