Skip to content

Commit 5b8c692

Browse files
net/http: add field Cookie.Quoted bool
The current implementation of the http package strips double quotes from the cookie-value during parsing, resulting in the serialized cookie not including them. This patch addresses this limitation by introducing a new field to track whether the original value was enclosed in quotes. Additionally, the internal representation of a cookie in the cookiejar package has been adjusted to align with the new representation. The syntax of cookies is outlined in RFC 6265 Section 4.1.1: https://datatracker.ietf.org/doc/html/rfc6265\#section-4.1.1 Fixes #46443 Co-authored-by: Fábio Mata <[email protected]>
1 parent d0051be commit 5b8c692

File tree

7 files changed

+117
-44
lines changed

7 files changed

+117
-44
lines changed

api/next/46443.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pkg net/http, type Cookie struct, Quoted bool #46443
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The new [`net/http.Cookie`](/pkg/net/http#Cookie) field indicates whether the `Cookie.Value` was originally quoted.

src/net/http/cookie.go

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ import (
2121
//
2222
// See https://tools.ietf.org/html/rfc6265 for details.
2323
type Cookie struct {
24-
Name string
25-
Value string
24+
Name string
25+
Value string
26+
Quoted bool // indicates whether the Value was originally quoted
2627

2728
Path string // optional
2829
Domain string // optional
@@ -77,14 +78,15 @@ func readSetCookies(h Header) []*Cookie {
7778
if !isCookieNameValid(name) {
7879
continue
7980
}
80-
value, ok = parseCookieValue(value, true)
81+
value, ok, quoted := parseCookieValue(value, true)
8182
if !ok {
8283
continue
8384
}
8485
c := &Cookie{
85-
Name: name,
86-
Value: value,
87-
Raw: line,
86+
Name: name,
87+
Value: value,
88+
Quoted: quoted,
89+
Raw: line,
8890
}
8991
for i := 1; i < len(parts); i++ {
9092
parts[i] = textproto.TrimString(parts[i])
@@ -97,7 +99,7 @@ func readSetCookies(h Header) []*Cookie {
9799
if !isASCII {
98100
continue
99101
}
100-
val, ok = parseCookieValue(val, false)
102+
val, ok, _ = parseCookieValue(val, false)
101103
if !ok {
102104
c.Unparsed = append(c.Unparsed, parts[i])
103105
continue
@@ -187,7 +189,7 @@ func (c *Cookie) String() string {
187189
b.Grow(len(c.Name) + len(c.Value) + len(c.Domain) + len(c.Path) + extraCookieLength)
188190
b.WriteString(c.Name)
189191
b.WriteRune('=')
190-
b.WriteString(sanitizeCookieValue(c.Value))
192+
b.WriteString(sanitizeCookieValue(c.Value, c.Quoted))
191193

192194
if len(c.Path) > 0 {
193195
b.WriteString("; Path=")
@@ -299,11 +301,11 @@ func readCookies(h Header, filter string) []*Cookie {
299301
if filter != "" && filter != name {
300302
continue
301303
}
302-
val, ok := parseCookieValue(val, true)
304+
val, ok, quoted := parseCookieValue(val, true)
303305
if !ok {
304306
continue
305307
}
306-
cookies = append(cookies, &Cookie{Name: name, Value: val})
308+
cookies = append(cookies, &Cookie{Name: name, Value: val, Quoted: quoted})
307309
}
308310
}
309311
return cookies
@@ -388,6 +390,8 @@ func sanitizeCookieName(n string) string {
388390
}
389391

390392
// sanitizeCookieValue produces a suitable cookie-value from v.
393+
// It receives a quoted bool indicating whether the value was originally
394+
// quoted.
391395
// https://tools.ietf.org/html/rfc6265#section-4.1.1
392396
//
393397
// cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
@@ -397,15 +401,14 @@ func sanitizeCookieName(n string) string {
397401
// ; and backslash
398402
//
399403
// We loosen this as spaces and commas are common in cookie values
400-
// but we produce a quoted cookie-value if and only if v contains
401-
// commas or spaces.
404+
// thus we produce a quoted cookie-value if v contains commas or spaces.
402405
// See https://golang.org/issue/7243 for the discussion.
403-
func sanitizeCookieValue(v string) string {
406+
func sanitizeCookieValue(v string, quoted bool) string {
404407
v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v)
405408
if len(v) == 0 {
406409
return v
407410
}
408-
if strings.ContainsAny(v, " ,") {
411+
if strings.ContainsAny(v, " ,") || quoted {
409412
return `"` + v + `"`
410413
}
411414
return v
@@ -447,17 +450,28 @@ func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string {
447450
return string(buf)
448451
}
449452

450-
func parseCookieValue(raw string, allowDoubleQuote bool) (string, bool) {
453+
// parseCookieValue parses a cookie value according to RFC 6265.
454+
// If allowDoubleQuote is true, parseCookieValue will consider that it
455+
// is parsing the cookie-value;
456+
// otherwise, it will consider that it is parsing a cookie-av value
457+
// (cookie attribute-value).
458+
//
459+
// It returns the parsed cookie value, a boolean indicating whether the
460+
// parsing was successful, and a boolean indicating whether the parsed
461+
// value was enclosed in double quotes.
462+
func parseCookieValue(raw string, allowDoubleQuote bool) (string, bool, bool) {
463+
quoted := false
451464
// Strip the quotes, if present.
452465
if allowDoubleQuote && len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' {
453466
raw = raw[1 : len(raw)-1]
467+
quoted = true
454468
}
455469
for i := 0; i < len(raw); i++ {
456470
if !validCookieValueByte(raw[i]) {
457-
return "", false
471+
return "", false, quoted
458472
}
459473
}
460-
return raw, true
474+
return raw, true, quoted
461475
}
462476

463477
func isCookieNameValid(raw string) bool {

src/net/http/cookie_test.go

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,19 @@ var writeSetCookiesTests = []struct {
146146
&Cookie{Name: "a\rb", Value: "v"},
147147
``,
148148
},
149+
// Quoted values (issue #46443)
150+
{
151+
&Cookie{Name: "cookie", Value: "quoted", Quoted: true},
152+
`cookie="quoted"`,
153+
},
154+
{
155+
&Cookie{Name: "cookie", Value: "quoted with spaces", Quoted: true},
156+
`cookie="quoted with spaces"`,
157+
},
158+
{
159+
&Cookie{Name: "cookie", Value: "quoted,with,commas", Quoted: true},
160+
`cookie="quoted,with,commas"`,
161+
},
149162
}
150163

151164
func TestWriteSetCookies(t *testing.T) {
@@ -214,6 +227,15 @@ var addCookieTests = []struct {
214227
},
215228
"cookie-1=v$1; cookie-2=v$2; cookie-3=v$3",
216229
},
230+
// Quoted values (issue #46443)
231+
{
232+
[]*Cookie{
233+
{Name: "cookie-1", Value: "quoted", Quoted: true},
234+
{Name: "cookie-2", Value: "quoted with spaces", Quoted: true},
235+
{Name: "cookie-3", Value: "quoted,with,commas", Quoted: true},
236+
},
237+
`cookie-1="quoted"; cookie-2="quoted with spaces"; cookie-3="quoted,with,commas"`,
238+
},
217239
}
218240

219241
func TestAddCookie(t *testing.T) {
@@ -325,37 +347,42 @@ var readSetCookiesTests = []struct {
325347
},
326348
{
327349
Header{"Set-Cookie": {`special-2=" z"`}},
328-
[]*Cookie{{Name: "special-2", Value: " z", Raw: `special-2=" z"`}},
350+
[]*Cookie{{Name: "special-2", Value: " z", Quoted: true, Raw: `special-2=" z"`}},
329351
},
330352
{
331353
Header{"Set-Cookie": {`special-3="a "`}},
332-
[]*Cookie{{Name: "special-3", Value: "a ", Raw: `special-3="a "`}},
354+
[]*Cookie{{Name: "special-3", Value: "a ", Quoted: true, Raw: `special-3="a "`}},
333355
},
334356
{
335357
Header{"Set-Cookie": {`special-4=" "`}},
336-
[]*Cookie{{Name: "special-4", Value: " ", Raw: `special-4=" "`}},
358+
[]*Cookie{{Name: "special-4", Value: " ", Quoted: true, Raw: `special-4=" "`}},
337359
},
338360
{
339361
Header{"Set-Cookie": {`special-5=a,z`}},
340362
[]*Cookie{{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}},
341363
},
342364
{
343365
Header{"Set-Cookie": {`special-6=",z"`}},
344-
[]*Cookie{{Name: "special-6", Value: ",z", Raw: `special-6=",z"`}},
366+
[]*Cookie{{Name: "special-6", Value: ",z", Quoted: true, Raw: `special-6=",z"`}},
345367
},
346368
{
347369
Header{"Set-Cookie": {`special-7=a,`}},
348370
[]*Cookie{{Name: "special-7", Value: "a,", Raw: `special-7=a,`}},
349371
},
350372
{
351373
Header{"Set-Cookie": {`special-8=","`}},
352-
[]*Cookie{{Name: "special-8", Value: ",", Raw: `special-8=","`}},
374+
[]*Cookie{{Name: "special-8", Value: ",", Quoted: true, Raw: `special-8=","`}},
353375
},
354376
// Make sure we can properly read back the Set-Cookie headers
355377
// for names containing spaces:
356378
{
357379
Header{"Set-Cookie": {`special-9 =","`}},
358-
[]*Cookie{{Name: "special-9", Value: ",", Raw: `special-9 =","`}},
380+
[]*Cookie{{Name: "special-9", Value: ",", Quoted: true, Raw: `special-9 =","`}},
381+
},
382+
// Quoted values (issue #46443)
383+
{
384+
Header{"Set-Cookie": {`cookie="quoted"`}},
385+
[]*Cookie{{Name: "cookie", Value: "quoted", Quoted: true, Raw: `cookie="quoted"`}},
359386
},
360387

361388
// TODO(bradfitz): users have reported seeing this in the
@@ -424,15 +451,15 @@ var readCookiesTests = []struct {
424451
Header{"Cookie": {`Cookie-1="v$1"; c2="v2"`}},
425452
"",
426453
[]*Cookie{
427-
{Name: "Cookie-1", Value: "v$1"},
428-
{Name: "c2", Value: "v2"},
454+
{Name: "Cookie-1", Value: "v$1", Quoted: true},
455+
{Name: "c2", Value: "v2", Quoted: true},
429456
},
430457
},
431458
{
432459
Header{"Cookie": {`Cookie-1="v$1"; c2=v2;`}},
433460
"",
434461
[]*Cookie{
435-
{Name: "Cookie-1", Value: "v$1"},
462+
{Name: "Cookie-1", Value: "v$1", Quoted: true},
436463
{Name: "c2", Value: "v2"},
437464
},
438465
},
@@ -485,23 +512,26 @@ func TestCookieSanitizeValue(t *testing.T) {
485512
log.SetOutput(&logbuf)
486513

487514
tests := []struct {
488-
in, want string
515+
in string
516+
quoted bool
517+
want string
489518
}{
490-
{"foo", "foo"},
491-
{"foo;bar", "foobar"},
492-
{"foo\\bar", "foobar"},
493-
{"foo\"bar", "foobar"},
494-
{"\x00\x7e\x7f\x80", "\x7e"},
495-
{`"withquotes"`, "withquotes"},
496-
{"a z", `"a z"`},
497-
{" z", `" z"`},
498-
{"a ", `"a "`},
499-
{"a,z", `"a,z"`},
500-
{",z", `",z"`},
501-
{"a,", `"a,"`},
519+
{"foo", false, "foo"},
520+
{"foo;bar", false, "foobar"},
521+
{"foo\\bar", false, "foobar"},
522+
{"foo\"bar", false, "foobar"},
523+
{"\x00\x7e\x7f\x80", false, "\x7e"},
524+
{`withquotes`, true, `"withquotes"`},
525+
{`"withquotes"`, true, `"withquotes"`}, // double quotes are not valid octets
526+
{"a z", false, `"a z"`},
527+
{" z", false, `" z"`},
528+
{"a ", false, `"a "`},
529+
{"a,z", false, `"a,z"`},
530+
{",z", false, `",z"`},
531+
{"a,", false, `"a,"`},
502532
}
503533
for _, tt := range tests {
504-
if got := sanitizeCookieValue(tt.in); got != tt.want {
534+
if got := sanitizeCookieValue(tt.in, tt.quoted); got != tt.want {
505535
t.Errorf("sanitizeCookieValue(%q) = %q; want %q", tt.in, got, tt.want)
506536
}
507537
}

src/net/http/cookiejar/jar.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ func New(o *Options) (*Jar, error) {
9292
type entry struct {
9393
Name string
9494
Value string
95+
Quoted bool
9596
Domain string
9697
Path string
9798
SameSite string
@@ -146,6 +147,14 @@ func (e *entry) pathMatch(requestPath string) bool {
146147
return false
147148
}
148149

150+
// Serialize entry in the form "name=value".
151+
func (e *entry) serialize() string {
152+
if strings.ContainsAny(e.Value, " ,") || e.Quoted {
153+
return e.Name + "=" + `"` + e.Value + `"`
154+
}
155+
return e.Name + "=" + e.Value
156+
}
157+
149158
// hasDotSuffix reports whether s ends in "."+suffix.
150159
func hasDotSuffix(s, suffix string) bool {
151160
return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix
@@ -220,7 +229,7 @@ func (j *Jar) cookies(u *url.URL, now time.Time) (cookies []*http.Cookie) {
220229
return s[i].seqNum < s[j].seqNum
221230
})
222231
for _, e := range selected {
223-
cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value})
232+
cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value, Quoted: e.Quoted})
224233
}
225234

226235
return cookies
@@ -429,6 +438,7 @@ func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e e
429438
}
430439

431440
e.Value = c.Value
441+
e.Quoted = c.Quoted
432442
e.Secure = c.Secure
433443
e.HttpOnly = c.HttpOnly
434444

src/net/http/cookiejar/jar_test.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ func (test jarTest) run(t *testing.T, jar *Jar) {
404404
if !cookie.Expires.After(now) {
405405
continue
406406
}
407-
cs = append(cs, cookie.Name+"="+cookie.Value)
407+
cs = append(cs, cookie.serialize())
408408
}
409409
}
410410
sort.Strings(cs)
@@ -421,7 +421,7 @@ func (test jarTest) run(t *testing.T, jar *Jar) {
421421
now = now.Add(1001 * time.Millisecond)
422422
var s []string
423423
for _, c := range jar.cookies(mustParseURL(query.toURL), now) {
424-
s = append(s, c.Name+"="+c.Value)
424+
s = append(s, c.String())
425425
}
426426
if got := strings.Join(s, " "); got != query.want {
427427
t.Errorf("Test %q #%d\ngot %q\nwant %q", test.description, i, got, query.want)
@@ -639,6 +639,23 @@ var basicsTests = [...]jarTest{
639639
{"https://[::1%25.example.com]:80/", ""},
640640
},
641641
},
642+
{
643+
"Retrieval of cookies with quoted values", // issue #46443
644+
"http://www.host.test/",
645+
[]string{
646+
`cookie-1="quoted"`,
647+
`cookie-2="quoted with spaces"`,
648+
`cookie-3="quoted,with,commas"`,
649+
`cookie-4= ,`,
650+
},
651+
`cookie-1="quoted" cookie-2="quoted with spaces" cookie-3="quoted,with,commas" cookie-4=" ,"`,
652+
[]query{
653+
{
654+
"http://www.host.test",
655+
`cookie-1="quoted" cookie-2="quoted with spaces" cookie-3="quoted,with,commas" cookie-4=" ,"`,
656+
},
657+
},
658+
},
642659
}
643660

644661
func TestBasics(t *testing.T) {

src/net/http/request.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ func (r *Request) Cookie(name string) (*Cookie, error) {
464464
// AddCookie only sanitizes c's name and value, and does not sanitize
465465
// a Cookie header already present in the request.
466466
func (r *Request) AddCookie(c *Cookie) {
467-
s := fmt.Sprintf("%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value))
467+
s := fmt.Sprintf("%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value, c.Quoted))
468468
if c := r.Header.Get("Cookie"); c != "" {
469469
r.Header.Set("Cookie", c+"; "+s)
470470
} else {

0 commit comments

Comments
 (0)