Commit ed88076c authored by Volker Dobler's avatar Volker Dobler Committed by Nigel Tao

net/http: allow commas and spaces in cookie values

According to RFC 6265 a cookie value may contain neither
commas nor spaces but such values are very common in the
wild and browsers handle them very well so we'll allow
both commas and spaces.
Values starting or ending in a comma or a space are
sent in the quoted form to prevent missinterpetations.

RFC 6265 conforming values are handled as before and
semicolons, backslashes and double-quotes are still
disallowed.

Fixes #7243

LGTM=nigeltao
R=nigeltao
CC=bradfitz, golang-codereviews
https://golang.org/cl/86050045
parent f8f34c33
...@@ -76,11 +76,7 @@ func readSetCookies(h Header) []*Cookie { ...@@ -76,11 +76,7 @@ func readSetCookies(h Header) []*Cookie {
attr, val = attr[:j], attr[j+1:] attr, val = attr[:j], attr[j+1:]
} }
lowerAttr := strings.ToLower(attr) lowerAttr := strings.ToLower(attr)
parseCookieValueFn := parseCookieValue val, success = parseCookieValue(val)
if lowerAttr == "expires" {
parseCookieValueFn = parseCookieExpiresValue
}
val, success = parseCookieValueFn(val)
if !success { if !success {
c.Unparsed = append(c.Unparsed, parts[i]) c.Unparsed = append(c.Unparsed, parts[i])
continue continue
...@@ -298,12 +294,23 @@ func sanitizeCookieName(n string) string { ...@@ -298,12 +294,23 @@ func sanitizeCookieName(n string) string {
// ; US-ASCII characters excluding CTLs, // ; US-ASCII characters excluding CTLs,
// ; whitespace DQUOTE, comma, semicolon, // ; whitespace DQUOTE, comma, semicolon,
// ; and backslash // ; and backslash
// We loosen this as spaces and commas are common in cookie values
// but we produce a quoted cookie-value in when value starts or ends
// with a comma or space.
// See http://golang.org/issue/7243 for the discussion.
func sanitizeCookieValue(v string) string { func sanitizeCookieValue(v string) string {
return sanitizeOrWarn("Cookie.Value", validCookieValueByte, v) v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v)
if len(v) == 0 {
return v
}
if v[0] == ' ' || v[0] == ',' || v[len(v)-1] == ' ' || v[len(v)-1] == ',' {
return `"` + v + `"`
}
return v
} }
func validCookieValueByte(b byte) bool { func validCookieValueByte(b byte) bool {
return 0x20 < b && b < 0x7f && b != '"' && b != ',' && b != ';' && b != '\\' return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\'
} }
// path-av = "Path=" path-value // path-av = "Path=" path-value
...@@ -338,38 +345,13 @@ func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string { ...@@ -338,38 +345,13 @@ func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string {
return string(buf) return string(buf)
} }
func unquoteCookieValue(v string) string {
if len(v) > 1 && v[0] == '"' && v[len(v)-1] == '"' {
return v[1 : len(v)-1]
}
return v
}
func isCookieByte(c byte) bool {
switch {
case c == 0x21, 0x23 <= c && c <= 0x2b, 0x2d <= c && c <= 0x3a,
0x3c <= c && c <= 0x5b, 0x5d <= c && c <= 0x7e:
return true
}
return false
}
func isCookieExpiresByte(c byte) (ok bool) {
return isCookieByte(c) || c == ',' || c == ' '
}
func parseCookieValue(raw string) (string, bool) { func parseCookieValue(raw string) (string, bool) {
return parseCookieValueUsing(raw, isCookieByte) // Strip the quotes, if present.
} if len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' {
raw = raw[1 : len(raw)-1]
func parseCookieExpiresValue(raw string) (string, bool) { }
return parseCookieValueUsing(raw, isCookieExpiresByte)
}
func parseCookieValueUsing(raw string, validByte func(byte) bool) (string, bool) {
raw = unquoteCookieValue(raw)
for i := 0; i < len(raw); i++ { for i := 0; i < len(raw); i++ {
if !validByte(raw[i]) { if !validCookieValueByte(raw[i]) {
return "", false return "", false
} }
} }
......
...@@ -52,6 +52,44 @@ var writeSetCookiesTests = []struct { ...@@ -52,6 +52,44 @@ var writeSetCookiesTests = []struct {
&Cookie{Name: "cookie-8", Value: "eight", Domain: "::1"}, &Cookie{Name: "cookie-8", Value: "eight", Domain: "::1"},
"cookie-8=eight", "cookie-8=eight",
}, },
// The "special" cookies have values containing commas or spaces which
// are disallowed by RFC 6265 but are common in the wild.
{
&Cookie{Name: "special-1", Value: "a z"},
`special-1=a z`,
},
{
&Cookie{Name: "special-2", Value: " z"},
`special-2=" z"`,
},
{
&Cookie{Name: "special-3", Value: "a "},
`special-3="a "`,
},
{
&Cookie{Name: "special-4", Value: " "},
`special-4=" "`,
},
{
&Cookie{Name: "special-5", Value: "a,z"},
`special-5=a,z`,
},
{
&Cookie{Name: "special-6", Value: ",z"},
`special-6=",z"`,
},
{
&Cookie{Name: "special-7", Value: "a,"},
`special-7="a,"`,
},
{
&Cookie{Name: "special-8", Value: ","},
`special-8=","`,
},
{
&Cookie{Name: "empty-value", Value: ""},
`empty-value=`,
},
} }
func TestWriteSetCookies(t *testing.T) { func TestWriteSetCookies(t *testing.T) {
...@@ -178,6 +216,40 @@ var readSetCookiesTests = []struct { ...@@ -178,6 +216,40 @@ var readSetCookiesTests = []struct {
Raw: "ASP.NET_SessionId=foo; path=/; HttpOnly", Raw: "ASP.NET_SessionId=foo; path=/; HttpOnly",
}}, }},
}, },
// Make sure we can properly read back the Set-Cookie headers we create
// for values containing spaces or commas:
{
Header{"Set-Cookie": {`special-1=a z`}},
[]*Cookie{{Name: "special-1", Value: "a z", Raw: `special-1=a z`}},
},
{
Header{"Set-Cookie": {`special-2=" z"`}},
[]*Cookie{{Name: "special-2", Value: " z", Raw: `special-2=" z"`}},
},
{
Header{"Set-Cookie": {`special-3="a "`}},
[]*Cookie{{Name: "special-3", Value: "a ", Raw: `special-3="a "`}},
},
{
Header{"Set-Cookie": {`special-4=" "`}},
[]*Cookie{{Name: "special-4", Value: " ", Raw: `special-4=" "`}},
},
{
Header{"Set-Cookie": {`special-5=a,z`}},
[]*Cookie{{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}},
},
{
Header{"Set-Cookie": {`special-6=",z"`}},
[]*Cookie{{Name: "special-6", Value: ",z", Raw: `special-6=",z"`}},
},
{
Header{"Set-Cookie": {`special-7=a,`}},
[]*Cookie{{Name: "special-7", Value: "a,", Raw: `special-7=a,`}},
},
{
Header{"Set-Cookie": {`special-8=","`}},
[]*Cookie{{Name: "special-8", Value: ",", Raw: `special-8=","`}},
},
// TODO(bradfitz): users have reported seeing this in the // TODO(bradfitz): users have reported seeing this in the
// wild, but do browsers handle it? RFC 6265 just says "don't // wild, but do browsers handle it? RFC 6265 just says "don't
...@@ -264,9 +336,14 @@ func TestCookieSanitizeValue(t *testing.T) { ...@@ -264,9 +336,14 @@ func TestCookieSanitizeValue(t *testing.T) {
in, want string in, want string
}{ }{
{"foo", "foo"}, {"foo", "foo"},
{"foo bar", "foobar"}, {"foo;bar", "foobar"},
{"foo\\bar", "foobar"},
{"foo\"bar", "foobar"},
{"\x00\x7e\x7f\x80", "\x7e"}, {"\x00\x7e\x7f\x80", "\x7e"},
{`"withquotes"`, "withquotes"}, {`"withquotes"`, "withquotes"},
{"a z", "a z"},
{" z", `" z"`},
{"a ", `"a "`},
} }
for _, tt := range tests { for _, tt := range tests {
if got := sanitizeCookieValue(tt.in); got != tt.want { if got := sanitizeCookieValue(tt.in); got != tt.want {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment