CVE-2023-38697 (published 2023-08-04T18:15Z)

protocol-http1 provides a low-level implementation of the HTTP/1 protocol. RFC 9112 Section 7.1 defined the format of chunk size, chunk data and chunk extension. The value of Content-Length header should be a string of 0-9 digits, the chunk size should be a string of hex digits and should split from chunk data using CRLF, and the chunk extension shouldn't contain any invisible character. However, Falcon has following behaviors while disobey the corresponding RFCs: accepting Content-Length header values that have `+` prefix, accepting Content-Length header values that written in hexadecimal with `0x` prefix, accepting `0x` and `+` prefixed chunk size, and accepting LF in chunk extension. This behavior can lead to desync when forwarding through multiple HTTP parsers, potentially results in HTTP request smuggling and firewall bypassing. This issue is fixed in `protocol-http1` v0.15.1. There are no known workarounds.

CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N)

Weakness information:
cwe_id: CWE-444
cwe_name: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')
description: The product acts as an intermediary HTTP agent
         (such as a proxy or firewall) in the data flow between two
         entities such as a client and server, but it does not
         interpret malformed HTTP requests or responses in ways that
         are consistent with how the messages will be processed by
         those entities that are at the ultimate destination.
extended_description: 
	   HTTP requests or responses ("messages") can be
	   malformed or unexpected in ways that cause web servers or
	   clients to interpret the messages in different ways than
	   intermediary HTTP agents such as load balancers, reverse
	   proxies, web caching proxies, application firewalls,
	   etc. For example, an adversary may be able to add duplicate
	   or different header fields that a client or server might
	   interpret as one set of messages, whereas the intermediary
	   might interpret the same sequence of bytes as a different
	   set of messages. For example, discrepancies can arise in
	   how to handle duplicate headers like two Transfer-encoding
	   (TE) or two Content-length (CL), or the malicious HTTP
	   message will have different headers for TE and
	   CL.
	   The inconsistent parsing and interpretation of messages
	   can allow the adversary to "smuggle" a message to the
	   client/server without the intermediary being aware of it.
	   This weakness is usually the result of the usage
	   of outdated or incompatible HTTP protocol versions in the
	   HTTP agents.
         
         

Fix information:
repo_url: https://github.com/socketry/protocol-http1
commit_details: repo_url: https://github.com/socketry/protocol-http1
author: Samuel Williams
author_date: 2023-07-30 12:46:58+12:00
msg: Strict validation of content length and chunk length. (#20)
repository: repo_url: https://github.com/socketry/protocol-http1
repo_name: socketry/protocol-http1
repo_language: Ruby
owner: socketry
file_changes: filename: chunked.rb
change_type: ModificationType.MODIFY
diff: @@ -35,12 +35,20 @@ module Protocol
 					super
 				end
 				
+				VALID_CHUNK_LENGTH = /\A[0-9a-fA-F]+\z/
+				
 				# Follows the procedure outlined in https://tools.ietf.org/html/rfc7230#section-4.1.3
 				def read
 					return nil if @finished
 					
+					length, extensions = read_line.split(";", 2)
+					
+					unless length =~ VALID_CHUNK_LENGTH
+						raise BadRequest, "Invalid chunk length: #{length.dump}"
+					end
+					
 					# It is possible this line contains chunk extension, so we use `to_i` to only consider the initial integral part:
-					length = read_line.to_i(16)
+					length = Integer(length, 16)
 					
 					if length == 0
 						@finished = true

programming_language: Ruby
filename: connection.rb
change_type: ModificationType.MODIFY
diff: @@ -398,10 +398,12 @@ module Protocol
 			HEAD = "HEAD"
 			CONNECT = "CONNECT"
 			
+			VALID_CONTENT_LENGTH = /\A\d+\z/
+			
 			def extract_content_length(headers)
 				if content_length = headers.delete(CONTENT_LENGTH)
-					if length = Integer(content_length, exception: false) and length >= 0
-						yield length
+					if content_length =~ VALID_CONTENT_LENGTH
+						yield Integer(content_length, 10)
 					else
 						raise BadRequest, "Invalid content length: #{content_length}"
 					end

programming_language: Ruby
filename: connection.rb
change_type: ModificationType.MODIFY
diff: @@ -208,7 +208,7 @@ describe Protocol::HTTP1::Connection do
 		
 		with "HEAD" do
 			it "can read length of head response" do
-				body = client.read_response_body("HEAD", 200, {'content-length' => 3773})
+				body = client.read_response_body("HEAD", 200, {'content-length' => '3773'})
 				
 				expect(body).to be_a ::Protocol::HTTP::Body::Head
 				expect(body.length).to be == 3773
@@ -216,7 +216,7 @@ describe Protocol::HTTP1::Connection do
 			end
 			
 			it "ignores zero length body" do
-				body = client.read_response_body("HEAD", 200, {'content-length' => 0})
+				body = client.read_response_body("HEAD", 200, {'content-length' => '0'})
 				
 				expect(body).to be_nil
 			end

programming_language: Ruby
filename: bad.rb
change_type: ModificationType.ADD
diff: @@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+# Released under the MIT License.
+# Copyright, 2019-2023, by Samuel Williams.
+
+require 'protocol/http1/connection'
+require 'connection_context'
+
+describe Protocol::HTTP1::Connection do
+	include_context ConnectionContext
+	
+	def before
+		super
+		
+		client.stream.write(input)
+		client.stream.close
+	end
+	
+	with "invalid hexadecimal content-length" do
+		def input
+			<<~HTTP.gsub("\n", "\r\n")
+			POST / HTTP/1.1
+			Host: a.com
+			Content-Length: 0x10
+			Connection: close
+			
+			0123456789abcdef
+			HTTP
+		end
+		
+		it "should fail to parse the request body" do
+			expect do
+				server.read_request
+			end.to raise_exception(Protocol::HTTP1::BadRequest)
+		end
+	end
+	
+	with "invalid +integer content-length" do
+		def input
+			<<~HTTP.gsub("\n", "\r\n")
+			POST / HTTP/1.1
+			Host: a.com
+			Content-Length: +16
+			Connection: close
+			
+			0123456789abcdef
+			HTTP
+		end
+		
+		it "should fail to parse the request body" do
+			expect do
+				server.read_request
+			end.to raise_exception(Protocol::HTTP1::BadRequest)
+		end
+	end
+	
+	with "invalid -integer content-length" do
+		def input
+			<<~HTTP.gsub("\n", "\r\n")
+			POST / HTTP/1.1
+			Host: a.com
+			Content-Length: -16
+			Connection: close
+			
+			0123456789abcdef
+			HTTP
+		end
+		
+		it "should fail to parse the request body" do
+			expect do
+				server.read_request
+			end.to raise_exception(Protocol::HTTP1::BadRequest)
+		end
+	end
+	
+	with "invalid hexidecimal chunk size" do
+		def input
+			<<~HTTP.gsub("\n", "\r\n")
+			POST / HTTP/1.1
+			Host: a.com
+			Transfer-Encoding: chunked
+			Connection: close
+			
+			0x10
+			0123456789abcdef
+			0
+			HTTP
+		end
+		
+		it "should fail to parse the request body" do
+			authority, method, target, version, headers, body = server.read_request
+			
+			expect(body).to be_a(Protocol::HTTP1::Body::Chunked)
+			
+			expect do
+				body.read
+			end.to raise_exception(Protocol::HTTP1::BadRequest)
+		end
+	end
+	
+	with "invalid +integer chunk size" do
+		def input
+			<<~HTTP.gsub("\n", "\r\n")
+			POST / HTTP/1.1
+			Host: a.com
+			Transfer-Encoding: chunked
+			Connection: close
+			
+			+10
+			0123456789abcdef
+			0
+			HTTP
+		end
+		
+		it "should fail to parse the request body" do
+			authority, method, target, version, headers, body = server.read_request
+			
+			expect(body).to be_a(Protocol::HTTP1::Body::Chunked)
+			
+			expect do
+				body.read
+			end.to raise_exception(Protocol::HTTP1::BadRequest)
+		end
+	end
+end

programming_language: Ruby