Skip to content

Commit cedb025

Browse files
Fix header parsing, allowing for \t characters between VCHAR.
1 parent b01fea1 commit cedb025

File tree

2 files changed

+111
-2
lines changed

2 files changed

+111
-2
lines changed

lib/protocol/http1/connection.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@ module HTTP1
3737

3838
# HTTP/1.x header parser:
3939
FIELD_NAME = TOKEN
40-
FIELD_VALUE = /[^\000-\037]*/.freeze
41-
HEADER = /\A(#{FIELD_NAME}):\s*(#{FIELD_VALUE})\s*\z/.freeze
40+
WS = /[ \t]/ # Whitespace.
41+
OWS = /#{WS}*/ # Optional whitespace.
42+
VCHAR = /[!-~]/ # Match visible characters from ASCII 33 to 126.
43+
FIELD_VALUE = /(?:#{VCHAR}+(?:#{WS}+#{VCHAR})*)*/.freeze
44+
HEADER = /\A(#{FIELD_NAME}):#{OWS}(#{FIELD_VALUE})#{OWS}\z/.freeze
4245

4346
VALID_FIELD_NAME = /\A#{FIELD_NAME}\z/.freeze
4447
VALID_FIELD_VALUE = /\A#{FIELD_VALUE}\z/.freeze
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require "protocol/http1/connection"
7+
require "connection_context"
8+
9+
describe Protocol::HTTP1::Connection do
10+
include_context ConnectionContext
11+
12+
let(:headers) {Array.new}
13+
14+
before do
15+
client.stream.write "GET / HTTP/1.1\r\nHost: localhost\r\n#{headers.join("\r\n")}\r\n\r\n"
16+
client.stream.close
17+
end
18+
19+
with "header that contains tab characters" do
20+
let(:headers) {[
21+
"user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) \t\t\tChrome/55.0.2883.95 Safari/537.36"
22+
]}
23+
24+
it "can parse the header" do
25+
authority, method, target, version, headers, body = server.read_request
26+
27+
expect(headers).to have_keys(
28+
"user-agent" => be == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) \t\t\tChrome/55.0.2883.95 Safari/537.36"
29+
)
30+
end
31+
end
32+
33+
with "header that contains obsolete folding whitespace" do
34+
let(:headers) {[
35+
"user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko)\n\tChrome/55.0.2883.95 Safari/537.36"
36+
]}
37+
38+
it "rejects the request" do
39+
expect do
40+
server.read_request
41+
end.to raise_exception(Protocol::HTTP1::BadHeader)
42+
end
43+
end
44+
45+
with "header that contains invalid characters" do
46+
let(:headers) {[
47+
"user-agent: Mozilla\x00Hacker Browser"
48+
]}
49+
50+
it "rejects the request" do
51+
expect do
52+
server.read_request
53+
end.to raise_exception(Protocol::HTTP1::BadHeader)
54+
end
55+
end
56+
57+
with "header that contains invalid high characters" do
58+
let(:headers) {[
59+
"user-agent: Mozilla\x7FHacker Browser"
60+
]}
61+
62+
it "rejects the request" do
63+
expect do
64+
server.read_request
65+
end.to raise_exception(Protocol::HTTP1::BadHeader)
66+
end
67+
end
68+
69+
with "header that has empty value" do
70+
let(:headers) {[
71+
"user-agent: "
72+
]}
73+
74+
it "can parse the header" do
75+
authority, method, target, version, headers, body = server.read_request
76+
77+
expect(headers).to have_keys(
78+
"user-agent" => be == ""
79+
)
80+
end
81+
end
82+
83+
with "header that has invalid name" do
84+
let(:headers) {[
85+
"invalid name: value"
86+
]}
87+
88+
it "rejects the request" do
89+
expect do
90+
server.read_request
91+
end.to raise_exception(Protocol::HTTP1::BadHeader)
92+
end
93+
end
94+
95+
with "header that has empty name" do
96+
let(:headers) {[
97+
": value"
98+
]}
99+
100+
it "rejects the request" do
101+
expect do
102+
server.read_request
103+
end.to raise_exception(Protocol::HTTP1::BadHeader)
104+
end
105+
end
106+
end

0 commit comments

Comments
 (0)