Skip to content

Commit f1cfd13

Browse files
authored
fix: parse multiple cookies with spaces (#943)
* fix: parse multiple cookies with spaces --------- Signed-off-by: Felipe Zipitria <[email protected]>
1 parent 3b532a6 commit f1cfd13

File tree

6 files changed

+171
-10
lines changed

6 files changed

+171
-10
lines changed

Diff for: go.sum

-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6
66
github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4=
77
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
88
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
9-
github.com/mccutchen/go-httpbin/v2 v2.12.0 h1:MPrFw/Avug0E83SN/j5SYDuD9By0GDAJ9hNTR4RwjyU=
10-
github.com/mccutchen/go-httpbin/v2 v2.12.0/go.mod h1:f4DUXYlU6yH0V81O4lJIwqpmYdTXXmYwzxMnYEimFPk=
119
github.com/mccutchen/go-httpbin/v2 v2.13.1 h1:mDTz2RTD3tugs1BKZM7o6YJsXODYWNvjKZko30B/aWk=
1210
github.com/mccutchen/go-httpbin/v2 v2.13.1/go.mod h1:f4DUXYlU6yH0V81O4lJIwqpmYdTXXmYwzxMnYEimFPk=
1311
github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=

Diff for: internal/cookies/cookies.go

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package cookies
2+
3+
import (
4+
"net/textproto"
5+
"strings"
6+
)
7+
8+
// ParseCookies parses cookies and splits in name, value pairs. Won't check for valid names nor values.
9+
// If there are multiple cookies with the same name, it will append to the list with the same name key.
10+
// Loosely based in the stdlib src/net/http/cookie.go
11+
func ParseCookies(rawCookies string) map[string][]string {
12+
cookies := make(map[string][]string)
13+
14+
rawCookies = textproto.TrimString(rawCookies)
15+
16+
if rawCookies == "" {
17+
return cookies
18+
}
19+
20+
var part string
21+
for len(rawCookies) > 0 { // continue since we have rest
22+
part, rawCookies, _ = strings.Cut(rawCookies, ";")
23+
part = textproto.TrimString(part)
24+
if part == "" {
25+
continue
26+
}
27+
name, val, _ := strings.Cut(part, "=")
28+
name = textproto.TrimString(name)
29+
// if name is empty (eg: "Cookie: =foo;") skip it
30+
if name == "" {
31+
continue
32+
}
33+
cookies[name] = append(cookies[name], val)
34+
}
35+
return cookies
36+
}

Diff for: internal/cookies/cookies_test.go

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package cookies
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func equalMaps(map1 map[string][]string, map2 map[string][]string) bool {
8+
if len(map1) != len(map2) {
9+
return false
10+
}
11+
12+
// Iterate through the key-value pairs of the first map
13+
for key, slice1 := range map1 {
14+
// Check if the key exists in the second map
15+
slice2, ok := map2[key]
16+
if !ok {
17+
return false
18+
}
19+
20+
// Compare the values of the corresponding keys
21+
for i, val1 := range slice1 {
22+
val2 := slice2[i]
23+
24+
// Compare the elements
25+
if val1 != val2 {
26+
return false
27+
}
28+
}
29+
}
30+
31+
return true
32+
}
33+
34+
func TestParseCookies(t *testing.T) {
35+
type args struct {
36+
rawCookies string
37+
}
38+
tests := []struct {
39+
name string
40+
args args
41+
want map[string][]string
42+
}{
43+
{
44+
name: "EmptyString",
45+
args: args{rawCookies: " "},
46+
want: map[string][]string{},
47+
},
48+
{
49+
name: "SimpleCookie",
50+
args: args{rawCookies: "test=test_value"},
51+
want: map[string][]string{"test": {"test_value"}},
52+
},
53+
{
54+
name: "MultipleCookies",
55+
args: args{rawCookies: "test1=test_value1; test2=test_value2"},
56+
want: map[string][]string{"test1": {"test_value1"}, "test2": {"test_value2"}},
57+
},
58+
{
59+
name: "SpacesInCookieName",
60+
args: args{rawCookies: " test1 =test_value1; test2 =test_value2"},
61+
want: map[string][]string{"test1": {"test_value1"}, "test2": {"test_value2"}},
62+
},
63+
{
64+
name: "SpacesInCookieValue",
65+
args: args{rawCookies: "test1=test _value1; test2 =test_value2"},
66+
want: map[string][]string{"test1": {"test _value1"}, "test2": {"test_value2"}},
67+
},
68+
{
69+
name: "EmptyCookie",
70+
args: args{rawCookies: ";;foo=bar"},
71+
want: map[string][]string{"foo": {"bar"}},
72+
},
73+
{
74+
name: "EmptyName",
75+
args: args{rawCookies: "=bar;"},
76+
want: map[string][]string{},
77+
},
78+
{
79+
name: "MultipleEqualsInValues",
80+
args: args{rawCookies: "test1=val==ue1;test2=value2"},
81+
want: map[string][]string{"test1": {"val==ue1"}, "test2": {"value2"}},
82+
},
83+
{
84+
name: "RepeatedCookieNameShouldGiveList",
85+
args: args{rawCookies: "test1=value1;test1=value2"},
86+
want: map[string][]string{"test1": {"value1", "value2"}},
87+
},
88+
}
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
got := ParseCookies(tt.args.rawCookies)
92+
if !equalMaps(got, tt.want) {
93+
t.Errorf("ParseCookies() = %v, want %v", got, tt.want)
94+
}
95+
})
96+
}
97+
}

Diff for: internal/corazawaf/transaction.go

+16-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/corazawaf/coraza/v3/internal/auditlog"
2323
"github.com/corazawaf/coraza/v3/internal/bodyprocessors"
2424
"github.com/corazawaf/coraza/v3/internal/collections"
25+
"github.com/corazawaf/coraza/v3/internal/cookies"
2526
"github.com/corazawaf/coraza/v3/internal/corazarules"
2627
"github.com/corazawaf/coraza/v3/internal/corazatypes"
2728
stringsutil "github.com/corazawaf/coraza/v3/internal/strings"
@@ -320,9 +321,21 @@ func (tx *Transaction) AddRequestHeader(key string, value string) {
320321
tx.variables.reqbodyProcessor.Set("MULTIPART")
321322
}
322323
case "cookie":
323-
// Cookies use the same syntax as GET params but with semicolon (;) separator
324-
// WithoutUnescape is used to avoid implicitly performing an URL decode on the cookies
325-
values := urlutil.ParseQueryWithoutUnescape(value, ';')
324+
// 4.2. Cookie
325+
//
326+
// 4.2.1. Syntax
327+
//
328+
// The user agent sends stored cookies to the origin server in the
329+
// Cookie header. If the server conforms to the requirements in
330+
// Section 4.1 (and the user agent conforms to the requirements in
331+
// Section 5), the user agent will send a Cookie header that conforms to
332+
// the following grammar:
333+
//
334+
// cookie-header = "Cookie:" OWS cookie-string OWS
335+
// cookie-string = cookie-pair *( ";" SP cookie-pair )
336+
//
337+
// There is no URL Decode performed no the cookies
338+
values := cookies.ParseCookies(value)
326339
for k, vr := range values {
327340
for _, v := range vr {
328341
tx.variables.requestCookies.Add(k, v)

Diff for: internal/corazawaf/transaction_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,28 @@ func TestCookiesNotUrldecoded(t *testing.T) {
835835
}
836836
}
837837

838+
func TestMultipleCookiesWithSpaceBetweenThem(t *testing.T) {
839+
waf := NewWAF()
840+
tx := waf.NewTransaction()
841+
multipleCookies := "cookie1=value1; cookie2=value2; cookie1=value2"
842+
tx.AddRequestHeader("cookie", multipleCookies)
843+
v11 := tx.variables.requestCookies.Get("cookie1")[0]
844+
if v11 != "value1" {
845+
t.Errorf("failed to set cookie, got %q", v11)
846+
}
847+
v12 := tx.variables.requestCookies.Get("cookie1")[1]
848+
if v12 != "value2" {
849+
t.Errorf("failed to set cookie, got %q", v12)
850+
}
851+
v2 := tx.variables.requestCookies.Get("cookie2")[0]
852+
if v2 != "value2" {
853+
t.Errorf("failed to set cookie, got %q", v2)
854+
}
855+
if err := tx.Close(); err != nil {
856+
t.Error(err)
857+
}
858+
}
859+
838860
func collectionValues(t *testing.T, col collection.Collection) []string {
839861
t.Helper()
840862
var values []string

Diff for: internal/url/url.go

-5
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ func ParseQuery(query string, separator byte) map[string][]string {
1313
return doParseQuery(query, separator, true)
1414
}
1515

16-
// ParseQueryWithoutUnescape is a sibling of ParseQuery, but without performing URL unescape of keys and values.
17-
func ParseQueryWithoutUnescape(query string, separator byte) map[string][]string {
18-
return doParseQuery(query, separator, false)
19-
}
20-
2116
func doParseQuery(query string, separator byte, urlUnescape bool) map[string][]string {
2217
m := make(map[string][]string)
2318
for query != "" {

0 commit comments

Comments
 (0)