Skip to content

Commit

Permalink
update coraza from upstream
Browse files Browse the repository at this point in the history
  • Loading branch information
blotus committed Jan 8, 2024
2 parents e66cc5f + b887a58 commit a62b8d8
Show file tree
Hide file tree
Showing 14 changed files with 536 additions and 30 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ jobs:
uses: actions/checkout@v4

- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: go

- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ require (
github.com/corazawaf/libinjection-go v0.1.2
github.com/foxcpp/go-mockdns v1.0.0
github.com/magefile/mage v1.15.0
github.com/mccutchen/go-httpbin/v2 v2.12.0
github.com/mccutchen/go-httpbin/v2 v2.13.1
github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e
github.com/tidwall/gjson v1.17.0
golang.org/x/net v0.19.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6
github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mccutchen/go-httpbin/v2 v2.12.0 h1:MPrFw/Avug0E83SN/j5SYDuD9By0GDAJ9hNTR4RwjyU=
github.com/mccutchen/go-httpbin/v2 v2.12.0/go.mod h1:f4DUXYlU6yH0V81O4lJIwqpmYdTXXmYwzxMnYEimFPk=
github.com/mccutchen/go-httpbin/v2 v2.13.1 h1:mDTz2RTD3tugs1BKZM7o6YJsXODYWNvjKZko30B/aWk=
github.com/mccutchen/go-httpbin/v2 v2.13.1/go.mod h1:f4DUXYlU6yH0V81O4lJIwqpmYdTXXmYwzxMnYEimFPk=
github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
Expand Down
36 changes: 36 additions & 0 deletions internal/cookies/cookies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cookies

import (
"net/textproto"
"strings"
)

// ParseCookies parses cookies and splits in name, value pairs. Won't check for valid names nor values.
// If there are multiple cookies with the same name, it will append to the list with the same name key.
// Loosely based in the stdlib src/net/http/cookie.go
func ParseCookies(rawCookies string) map[string][]string {
cookies := make(map[string][]string)

rawCookies = textproto.TrimString(rawCookies)

if rawCookies == "" {
return cookies
}

var part string
for len(rawCookies) > 0 { // continue since we have rest
part, rawCookies, _ = strings.Cut(rawCookies, ";")
part = textproto.TrimString(part)
if part == "" {
continue
}
name, val, _ := strings.Cut(part, "=")
name = textproto.TrimString(name)
// if name is empty (eg: "Cookie: =foo;") skip it
if name == "" {
continue
}
cookies[name] = append(cookies[name], val)
}
return cookies
}
97 changes: 97 additions & 0 deletions internal/cookies/cookies_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package cookies

import (
"testing"
)

func equalMaps(map1 map[string][]string, map2 map[string][]string) bool {
if len(map1) != len(map2) {
return false
}

// Iterate through the key-value pairs of the first map
for key, slice1 := range map1 {
// Check if the key exists in the second map
slice2, ok := map2[key]
if !ok {
return false
}

// Compare the values of the corresponding keys
for i, val1 := range slice1 {
val2 := slice2[i]

// Compare the elements
if val1 != val2 {
return false
}
}
}

return true
}

func TestParseCookies(t *testing.T) {
type args struct {
rawCookies string
}
tests := []struct {
name string
args args
want map[string][]string
}{
{
name: "EmptyString",
args: args{rawCookies: " "},
want: map[string][]string{},
},
{
name: "SimpleCookie",
args: args{rawCookies: "test=test_value"},
want: map[string][]string{"test": {"test_value"}},
},
{
name: "MultipleCookies",
args: args{rawCookies: "test1=test_value1; test2=test_value2"},
want: map[string][]string{"test1": {"test_value1"}, "test2": {"test_value2"}},
},
{
name: "SpacesInCookieName",
args: args{rawCookies: " test1 =test_value1; test2 =test_value2"},
want: map[string][]string{"test1": {"test_value1"}, "test2": {"test_value2"}},
},
{
name: "SpacesInCookieValue",
args: args{rawCookies: "test1=test _value1; test2 =test_value2"},
want: map[string][]string{"test1": {"test _value1"}, "test2": {"test_value2"}},
},
{
name: "EmptyCookie",
args: args{rawCookies: ";;foo=bar"},
want: map[string][]string{"foo": {"bar"}},
},
{
name: "EmptyName",
args: args{rawCookies: "=bar;"},
want: map[string][]string{},
},
{
name: "MultipleEqualsInValues",
args: args{rawCookies: "test1=val==ue1;test2=value2"},
want: map[string][]string{"test1": {"val==ue1"}, "test2": {"value2"}},
},
{
name: "RepeatedCookieNameShouldGiveList",
args: args{rawCookies: "test1=value1;test1=value2"},
want: map[string][]string{"test1": {"value1", "value2"}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ParseCookies(tt.args.rawCookies)
if !equalMaps(got, tt.want) {
t.Errorf("ParseCookies() = %v, want %v", got, tt.want)
}
})
}
}
19 changes: 16 additions & 3 deletions internal/corazawaf/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/crowdsecurity/coraza/v3/internal/auditlog"
"github.com/crowdsecurity/coraza/v3/internal/bodyprocessors"
"github.com/crowdsecurity/coraza/v3/internal/collections"
"github.com/crowdsecurity/coraza/v3/internal/cookies"
"github.com/crowdsecurity/coraza/v3/internal/corazarules"
"github.com/crowdsecurity/coraza/v3/internal/corazatypes"
stringsutil "github.com/crowdsecurity/coraza/v3/internal/strings"
Expand Down Expand Up @@ -320,9 +321,21 @@ func (tx *Transaction) AddRequestHeader(key string, value string) {
tx.variables.reqbodyProcessor.Set("MULTIPART")
}
case "cookie":
// Cookies use the same syntax as GET params but with semicolon (;) separator
// WithoutUnescape is used to avoid implicitly performing an URL decode on the cookies
values := urlutil.ParseQueryWithoutUnescape(value, ';')
// 4.2. Cookie
//
// 4.2.1. Syntax
//
// The user agent sends stored cookies to the origin server in the
// Cookie header. If the server conforms to the requirements in
// Section 4.1 (and the user agent conforms to the requirements in
// Section 5), the user agent will send a Cookie header that conforms to
// the following grammar:
//
// cookie-header = "Cookie:" OWS cookie-string OWS
// cookie-string = cookie-pair *( ";" SP cookie-pair )
//
// There is no URL Decode performed no the cookies
values := cookies.ParseCookies(value)
for k, vr := range values {
for _, v := range vr {
tx.variables.requestCookies.Add(k, v)
Expand Down
22 changes: 22 additions & 0 deletions internal/corazawaf/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,28 @@ func TestCookiesNotUrldecoded(t *testing.T) {
}
}

func TestMultipleCookiesWithSpaceBetweenThem(t *testing.T) {
waf := NewWAF()
tx := waf.NewTransaction()
multipleCookies := "cookie1=value1; cookie2=value2; cookie1=value2"
tx.AddRequestHeader("cookie", multipleCookies)
v11 := tx.variables.requestCookies.Get("cookie1")[0]
if v11 != "value1" {
t.Errorf("failed to set cookie, got %q", v11)
}
v12 := tx.variables.requestCookies.Get("cookie1")[1]
if v12 != "value2" {
t.Errorf("failed to set cookie, got %q", v12)
}
v2 := tx.variables.requestCookies.Get("cookie2")[0]
if v2 != "value2" {
t.Errorf("failed to set cookie, got %q", v2)
}
if err := tx.Close(); err != nil {
t.Error(err)
}
}

func collectionValues(t *testing.T, col collection.Collection) []string {
t.Helper()
var values []string
Expand Down
82 changes: 73 additions & 9 deletions internal/transformations/base64decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,82 @@

package transformations

import (
"encoding/base64"
import "strings"

stringsutil "github.com/crowdsecurity/coraza/v3/internal/strings"
)
var base64DecMap = []byte{
127, 127, 127, 127, 127, 127, 127, 127, 127, 127,
127, 127, 127, 127, 127, 127, 127, 127, 127, 127,
127, 127, 127, 127, 127, 127, 127, 127, 127, 127,
127, 127, 127, 127, 127, 127, 127, 127, 127, 127,
127, 127, 127, 62, 127, 127, 127, 63, 52, 53,
54, 55, 56, 57, 58, 59, 60, 61, 127, 127,
127, 64, 127, 127, 127, 0, 1, 2, 3, 4,
5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
25, 127, 127, 127, 127, 127, 127, 26, 27, 28,
29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
49, 50, 51, 127, 127, 127, 127, 127,
}

// base64decode decodes a Base64-encoded string.
// Padding is optional.
// Partial decoding is returned up to the first invalid character (if any).
// New line characters (\r and \n) are ignored.
// Note: a custom base64 decoder is used in order to return partial decoding when an error arises. It
// would be possible to use the standard library only relying on undocumented behaviors of the decoder.
// For more context, see https://github.com/corazawaf/coraza/pull/940
func base64decode(data string) (string, bool, error) {
dec, err := base64.StdEncoding.DecodeString(data)
if err != nil {
// Forgiving implementation, which ignores invalid characters
return data, false, nil
res := doBase64decode(data)
return res, true, nil
}

func doBase64decode(src string) string {
slen := len(src)
if slen == 0 {
return src
}

var n, x int
var dst strings.Builder
dst.Grow(slen)

for i := 0; i < slen; i++ {
currChar := src[i]
// new line characters are ignored.
if currChar == '\r' || currChar == '\n' {
continue
}
// If invalid character or padding reached, we stop decoding
if currChar == '=' || currChar == ' ' || currChar > 127 {
break
}
decodedChar := base64DecMap[currChar]
// Another condition of invalid character
if decodedChar == 127 {
break
}

x = (x << 6) | int(decodedChar&0x3F)
n++
if n == 4 {
dst.WriteByte(byte(x >> 16))
dst.WriteByte(byte(x >> 8))
dst.WriteByte(byte(x))
n = 0
x = 0
}
}
return stringsutil.WrapUnsafe(dec), true, nil

// Handle any remaining characters
if n == 2 {
x <<= 12
dst.WriteByte(byte(x >> 16))
} else if n == 3 {
x <<= 6
dst.WriteByte(byte(x >> 16))
dst.WriteByte(byte(x >> 8))
}

return dst.String()
}
Loading

0 comments on commit a62b8d8

Please sign in to comment.