Skip to content

Commit 1ac3280

Browse files
authored
Allow parsing cookies with space in the value (#14455)
This is an optional feature enhancement of https://datatracker.ietf.org/doc/html/rfc6265#section-5.2
1 parent 6c8542a commit 1ac3280

File tree

2 files changed

+69
-14
lines changed

2 files changed

+69
-14
lines changed

spec/std/http/cookie_spec.cr

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ module HTTP
132132
it "raises on invalid value" do
133133
cookie = HTTP::Cookie.new("x", "")
134134
invalid_values = {
135-
'"', ',', ';', '\\', # invalid printable ascii characters
136-
' ', '\r', '\t', '\n', # non-printable ascii characters
135+
'"', ',', ';', '\\', # invalid printable ascii characters
136+
'\r', '\t', '\n', # non-printable ascii characters
137137
}.map { |c| "foo#{c}bar" }
138138

139139
invalid_values.each do |invalid_value|
@@ -235,12 +235,6 @@ module HTTP
235235
cookie.to_set_cookie_header.should eq("key=value")
236236
end
237237

238-
it "parse_set_cookie with space" do
239-
cookie = parse_set_cookie("key=value; path=/test")
240-
parse_set_cookie("key=value;path=/test").should eq cookie
241-
parse_set_cookie("key=value; \t\npath=/test").should eq cookie
242-
end
243-
244238
it "parses key=" do
245239
cookie = parse_first_cookie("key=")
246240
cookie.name.should eq("key")
@@ -285,9 +279,60 @@ module HTTP
285279
first.value.should eq("bar")
286280
second.value.should eq("baz")
287281
end
282+
283+
it "parses cookie with spaces in value" do
284+
parse_first_cookie(%[key=some value]).value.should eq "some value"
285+
parse_first_cookie(%[key="some value"]).value.should eq "some value"
286+
end
287+
288+
it "strips spaces around value only when it's unquoted" do
289+
parse_first_cookie(%[key= some value ]).value.should eq "some value"
290+
parse_first_cookie(%[key=" some value "]).value.should eq " some value "
291+
parse_first_cookie(%[key= " some value " ]).value.should eq " some value "
292+
end
288293
end
289294

290295
describe "parse_set_cookie" do
296+
it "with space" do
297+
cookie = parse_set_cookie("key=value; path=/test")
298+
parse_set_cookie("key=value;path=/test").should eq cookie
299+
parse_set_cookie("key=value; \t\npath=/test").should eq cookie
300+
end
301+
302+
it "parses cookie with spaces in value" do
303+
parse_set_cookie(%[key=some value]).value.should eq "some value"
304+
parse_set_cookie(%[key="some value"]).value.should eq "some value"
305+
end
306+
307+
it "removes leading and trailing whitespaces" do
308+
cookie = parse_set_cookie(%[key= \tvalue \t; \t\npath=/test])
309+
cookie.name.should eq "key"
310+
cookie.value.should eq "value"
311+
cookie.path.should eq "/test"
312+
313+
cookie = parse_set_cookie(%[ key\t =value \n;path=/test])
314+
cookie.name.should eq "key"
315+
cookie.value.should eq "value"
316+
cookie.path.should eq "/test"
317+
end
318+
319+
it "strips spaces around value only when it's unquoted" do
320+
cookie = parse_set_cookie(%[key= value ; \tpath=/test])
321+
cookie.name.should eq "key"
322+
cookie.value.should eq "value"
323+
cookie.path.should eq "/test"
324+
325+
cookie = parse_set_cookie(%[key=" value "; \tpath=/test])
326+
cookie.name.should eq "key"
327+
cookie.value.should eq " value "
328+
cookie.path.should eq "/test"
329+
330+
cookie = parse_set_cookie(%[key= " value "\t ; \tpath=/test])
331+
cookie.name.should eq "key"
332+
cookie.value.should eq " value "
333+
cookie.path.should eq "/test"
334+
end
335+
291336
it "parses path" do
292337
cookie = parse_set_cookie("key=value; path=/test")
293338
cookie.name.should eq("key")

src/http/cookie.cr

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ module HTTP
9797
private def validate_value(value)
9898
value.each_byte do |byte|
9999
# valid characters for cookie-value per https://tools.ietf.org/html/rfc6265#section-4.1.1
100-
# all printable ASCII characters except ' ', ',', '"', ';' and '\\'
101-
if !byte.in?(0x21...0x7f) || byte.in?(0x22, 0x2c, 0x3b, 0x5c)
100+
# all printable ASCII characters except ',', '"', ';' and '\\'
101+
if !byte.in?(0x20...0x7f) || byte.in?(0x22, 0x2c, 0x3b, 0x5c)
102102
raise IO::Error.new("Invalid cookie value")
103103
end
104104
end
@@ -196,9 +196,9 @@ module HTTP
196196
module Parser
197197
module Regex
198198
CookieName = /[^()<>@,;:\\"\/\[\]?={} \t\x00-\x1f\x7f]+/
199-
CookieOctet = /[!#-+\--:<-\[\]-~]/
199+
CookieOctet = /[!#-+\--:<-\[\]-~ ]/
200200
CookieValue = /(?:"#{CookieOctet}*"|#{CookieOctet}*)/
201-
CookiePair = /(?<name>#{CookieName})=(?<value>#{CookieValue})/
201+
CookiePair = /\s*(?<name>#{CookieName})\s*=\s*(?<value>#{CookieValue})\s*/
202202
DomainLabel = /[A-Za-z0-9\-]+/
203203
DomainIp = /(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/
204204
Time = /(?:\d{2}:\d{2}:\d{2})/
@@ -230,9 +230,11 @@ module HTTP
230230
def parse_cookies(header, &)
231231
header.scan(CookieString).each do |pair|
232232
value = pair["value"]
233-
if value.starts_with?('"')
233+
if value.starts_with?('"') && value.ends_with?('"')
234234
# Unwrap quoted cookie value
235235
value = value.byte_slice(1, value.bytesize - 2)
236+
else
237+
value = value.strip
236238
end
237239
yield Cookie.new(pair["name"], value)
238240
end
@@ -251,8 +253,16 @@ module HTTP
251253
expires = parse_time(match["expires"]?)
252254
max_age = match["max_age"]?.try(&.to_i64.seconds)
253255

256+
# Unwrap quoted cookie value
257+
cookie_value = match["value"]
258+
if cookie_value.starts_with?('"') && cookie_value.ends_with?('"')
259+
cookie_value = cookie_value.byte_slice(1, cookie_value.bytesize - 2)
260+
else
261+
cookie_value = cookie_value.strip
262+
end
263+
254264
Cookie.new(
255-
match["name"], match["value"],
265+
match["name"], cookie_value,
256266
path: match["path"]?,
257267
expires: expires,
258268
domain: match["domain"]?,

0 commit comments

Comments
 (0)