Skip to content
6 changes: 6 additions & 0 deletions experimental/plugins/plugintypes/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,11 @@ type TransactionVariables interface {
ArgsNames() collection.Keyed
ArgsGetNames() collection.Keyed
ArgsPostNames() collection.Keyed
ArgsRaw() collection.Keyed
ArgsGetRaw() collection.Map
ArgsPostRaw() collection.Map
ArgsGetNamesRaw() collection.Keyed
ArgsPostNamesRaw() collection.Keyed
ArgsNamesRaw() collection.Keyed
MultipartStrictError() collection.Single
}
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,6 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down
3 changes: 3 additions & 0 deletions internal/bodyprocessors/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@ func (js *jsonBodyProcessor) ProcessRequest(reader io.Reader, v plugintypes.Tran
ss := s.String()
// Process with recursion limit
col := v.ArgsPost()
colRaw := v.ArgsPostRaw()
data, err := readJSON(ss, bpo.RequestBodyRecursionLimit)
if err != nil {
return err
}
for key, value := range data {
col.SetIndex(key, 0, value)
// JSON values are not URL-encoded, so raw == cooked
colRaw.SetIndex(key, 0, value)
}

// Store the raw JSON in the TX variable for validateSchema
Expand Down
3 changes: 3 additions & 0 deletions internal/bodyprocessors/multipart.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func (mbp *multipartBodyProcessor) ProcessRequest(reader io.Reader, v plugintype
filesTmpNamesCol := v.FilesTmpNames()
fileSizesCol := v.FilesSizes()
postCol := v.ArgsPost()
postRawCol := v.ArgsPostRaw()
filesCombinedSizeCol := v.FilesCombinedSize()
filesNamesCol := v.FilesNames()
headersNames := v.MultipartPartHeaders()
Expand Down Expand Up @@ -107,6 +108,8 @@ func (mbp *multipartBodyProcessor) ProcessRequest(reader io.Reader, v plugintype
}
totalSize += int64(len(data))
postCol.Add(p.FormName(), string(data))
// Multipart fields are not URL-encoded, so raw == cooked
postRawCol.Add(p.FormName(), string(data))
filesCombinedSizeCol.(*collections.Single).Set(fmt.Sprintf("%d", totalSize))
if errors.Is(err, io.ErrUnexpectedEOF) {
break
Expand Down
40 changes: 40 additions & 0 deletions internal/bodyprocessors/multipart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,43 @@ text defa`)
t.Fatalf("expected ArgsPost 'text' to be 'text defa', got %q", textValues[0])
}
}

func TestMultipartArgsPostRaw(t *testing.T) {
// For multipart, field values are not URL-encoded, so ArgsPostRaw should equal ArgsPost.
payload := strings.TrimSpace(`
-----------------------------9051914041544843365972754266
Content-Disposition: form-data; name="username"

john_doe
-----------------------------9051914041544843365972754266
Content-Disposition: form-data; name="comment"

hello world
-----------------------------9051914041544843365972754266--
`)

mp := multipartProcessor(t)
v := corazawaf.NewTransactionVariables()
if err := mp.ProcessRequest(strings.NewReader(payload), v, plugintypes.BodyProcessorOptions{
Mime: "multipart/form-data; boundary=---------------------------9051914041544843365972754266",
}); err != nil {
t.Fatal(err)
}

// ArgsPost and ArgsPostRaw should both contain the field values (raw == cooked for multipart).
argsPost := v.ArgsPost()
argsPostRaw := v.ArgsPostRaw()

fields := map[string]string{
"username": "john_doe",
"comment": "hello world",
}
for field, want := range fields {
if got := argsPost.Get(field); len(got) == 0 || got[0] != want {
t.Errorf("ArgsPost[%q]: got %v, want %q", field, got, want)
}
if got := argsPostRaw.Get(field); len(got) == 0 || got[0] != want {
t.Errorf("ArgsPostRaw[%q]: got %v, want %q", field, got, want)
}
}
}
9 changes: 8 additions & 1 deletion internal/bodyprocessors/urlencoded.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,18 @@ func (*urlencodedBodyProcessor) ProcessRequest(reader io.Reader, v plugintypes.T
}

b := buf.String()
values := urlutil.ParseQuery(b, '&')

// Parse both decoded and raw values in a single pass
values, rawValues := urlutil.ParseQueryBoth(b, '&')
argsCol := v.ArgsPost()
for k, vs := range values {
argsCol.Set(k, vs)
}
argsRawCol := v.ArgsPostRaw()
for k, vs := range rawValues {
argsRawCol.Set(k, vs)
}

v.RequestBody().(*collections.Single).Set(b)
v.RequestBodyLength().(*collections.Single).Set(strconv.Itoa(len(b)))
return nil
Expand Down
60 changes: 60 additions & 0 deletions internal/bodyprocessors/urlencoded_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,66 @@
"github.com/corazawaf/coraza/v3/internal/corazawaf"
)

func TestURLEncodeRawValues(t *testing.T) {
bp, err := bodyprocessors.GetBodyProcessor("urlencoded")
if err != nil {
t.Fatal(err)
}

tests := []struct {
name string

Check failure on line 23 in internal/bodyprocessors/urlencoded_test.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gofmt)
body string
wantCooked map[string]string // decoded value in ArgsPost
wantRaw map[string]string // raw value in ArgsPostRaw
}{
{
name: "percent-encoded value",
body: "key=%3Cscript%3E",
wantCooked: map[string]string{"key": "<script>"},
wantRaw: map[string]string{"key": "%3Cscript%3E"},
},
{
name: "double-encoded value",
body: "password=Secret%2500",
wantCooked: map[string]string{"password": "Secret%00"},
wantRaw: map[string]string{"password": "Secret%2500"},
},
{
name: "plus sign preserved in raw",
body: "q=hello+world",
wantCooked: map[string]string{"q": "hello world"},
wantRaw: map[string]string{"q": "hello+world"},
},
{
name: "plain value identical in both",
body: "plain=hello",
wantCooked: map[string]string{"plain": "hello"},
wantRaw: map[string]string{"plain": "hello"},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
v := corazawaf.NewTransactionVariables()
if err := bp.ProcessRequest(strings.NewReader(tc.body), v, plugintypes.BodyProcessorOptions{}); err != nil {
t.Fatal(err)
}
for k, want := range tc.wantCooked {
got := v.ArgsPost().Get(k)
if len(got) == 0 || got[0] != want {
t.Errorf("ArgsPost[%q]: got %v, want %q", k, got, want)
}
}
for k, want := range tc.wantRaw {
got := v.ArgsPostRaw().Get(k)
if len(got) == 0 || got[0] != want {
t.Errorf("ArgsPostRaw[%q]: got %v, want %q", k, got, want)
}
}
})
}
}

func TestURLEncode(t *testing.T) {
bp, err := bodyprocessors.GetBodyProcessor("urlencoded")
if err != nil {
Expand Down
93 changes: 91 additions & 2 deletions internal/corazawaf/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,18 @@ func (tx *Transaction) Collection(idx variables.RuleVariable) collection.Collect
return tx.variables.argsGetNames
case variables.ArgsPostNames:
return tx.variables.argsPostNames
case variables.ArgsRaw:
return tx.variables.argsRaw
case variables.ArgsGetRaw:
return tx.variables.argsGetRaw
case variables.ArgsPostRaw:
return tx.variables.argsPostRaw
case variables.ArgsNamesRaw:
return tx.variables.argsNamesRaw
case variables.ArgsGetNamesRaw:
return tx.variables.argsGetNamesRaw
case variables.ArgsPostNamesRaw:
return tx.variables.argsPostNamesRaw
case variables.ResBodyProcessor:
return tx.variables.resBodyProcessor
case variables.TX:
Expand Down Expand Up @@ -748,14 +760,19 @@ func (tx *Transaction) ProcessConnection(client string, cPort int, server string
tx.variables.serverPort.Set(p2)
}

// ExtractGetArguments transforms an url encoded string to a map and creates ARGS_GET
// ExtractGetArguments transforms an url encoded string to a map and creates ARGS_GET and ARGS_GET_RAW
func (tx *Transaction) ExtractGetArguments(uri string) {
data := urlutil.ParseQuery(uri, '&')
data, rawData := urlutil.ParseQueryBoth(uri, '&')
for k, vs := range data {
for _, v := range vs {
tx.AddGetRequestArgument(k, v)
}
}
for k, vs := range rawData {
for _, v := range vs {
tx.addGetRequestArgumentRaw(k, v)
}
}
}

// AddGetRequestArgument
Expand Down Expand Up @@ -785,6 +802,14 @@ func (tx *Transaction) AddPathRequestArgument(key string, value string) {
tx.variables.argsPath.Add(key, value)
}

func (tx *Transaction) addGetRequestArgumentRaw(key string, value string) {
if tx.checkArgumentLimit(tx.variables.argsGetRaw) {
tx.debugLogger.Warn().Msg("skipping raw get request argument, over limit")
return
}
tx.variables.argsGetRaw.Add(key, value)
}
Comment thread
fzipi marked this conversation as resolved.

func (tx *Transaction) checkArgumentLimit(c *collections.NamedCollection) bool {
return c.Len() >= tx.WAF.ArgumentLimit
}
Expand Down Expand Up @@ -1724,10 +1749,16 @@ type TransactionVariables struct {
argsCombinedSize *collections.SizeCollection
argsGet *collections.NamedCollection
argsGetNames collection.Keyed
argsGetNamesRaw collection.Keyed
argsGetRaw *collections.NamedCollection
argsNames *collections.ConcatKeyed
argsNamesRaw *collections.ConcatKeyed
argsPath *collections.NamedCollection
argsPost *collections.NamedCollection
argsPostNames collection.Keyed
argsPostNamesRaw collection.Keyed
argsPostRaw *collections.NamedCollection
argsRaw *collections.ConcatKeyed
duration *collections.Single
env *collections.Map
files *collections.Map
Expand Down Expand Up @@ -1894,10 +1925,14 @@ func NewTransactionVariables() *TransactionVariables {
v.argsGet = collections.NewCaseSensitiveNamedCollection(variables.ArgsGet)
v.argsPost = collections.NewCaseSensitiveNamedCollection(variables.ArgsPost)
v.argsPath = collections.NewCaseSensitiveNamedCollection(variables.ArgsPath)
v.argsGetRaw = collections.NewCaseSensitiveNamedCollection(variables.ArgsGetRaw)
v.argsPostRaw = collections.NewCaseSensitiveNamedCollection(variables.ArgsPostRaw)
} else {
v.argsGet = collections.NewNamedCollection(variables.ArgsGet)
v.argsPost = collections.NewNamedCollection(variables.ArgsPost)
v.argsPath = collections.NewNamedCollection(variables.ArgsPath)
v.argsGetRaw = collections.NewNamedCollection(variables.ArgsGetRaw)
v.argsPostRaw = collections.NewNamedCollection(variables.ArgsPostRaw)
}

v.argsGetNames = v.argsGet.Names(variables.ArgsGetNames)
Expand All @@ -1916,6 +1951,18 @@ func NewTransactionVariables() *TransactionVariables {
// Only used in a concatenating collection so variable name doesn't matter.
v.argsPath.Names(variables.Unknown),
)
v.argsGetNamesRaw = v.argsGetRaw.Names(variables.ArgsGetNamesRaw)
v.argsPostNamesRaw = v.argsPostRaw.Names(variables.ArgsPostNamesRaw)
v.argsRaw = collections.NewConcatKeyed(
variables.ArgsRaw,
v.argsGetRaw,
v.argsPostRaw,
)
v.argsNamesRaw = collections.NewConcatKeyed(
variables.ArgsNamesRaw,
v.argsGetNamesRaw,
v.argsPostNamesRaw,
)
return v
}

Expand Down Expand Up @@ -2203,6 +2250,30 @@ func (v *TransactionVariables) ArgsPostNames() collection.Keyed {
return v.argsPostNames
}

func (v *TransactionVariables) ArgsRaw() collection.Keyed {
return v.argsRaw
}

func (v *TransactionVariables) ArgsGetRaw() collection.Map {
return v.argsGetRaw
}

func (v *TransactionVariables) ArgsPostRaw() collection.Map {
return v.argsPostRaw
}

func (v *TransactionVariables) ArgsGetNamesRaw() collection.Keyed {
return v.argsGetNamesRaw
}

func (v *TransactionVariables) ArgsPostNamesRaw() collection.Keyed {
return v.argsPostNamesRaw
}

func (v *TransactionVariables) ArgsNamesRaw() collection.Keyed {
return v.argsNamesRaw
}

func (v *TransactionVariables) ResBodyError() collection.Single {
return v.resBodyError
}
Expand Down Expand Up @@ -2252,6 +2323,24 @@ func (v *TransactionVariables) All(f func(v variables.RuleVariable, col collecti
if !f(variables.ArgsPostNames, v.argsPostNames) {
return
}
if !f(variables.ArgsPostRaw, v.argsPostRaw) {
return
}
if !f(variables.ArgsRaw, v.argsRaw) {
return
}
if !f(variables.ArgsGetRaw, v.argsGetRaw) {
return
}
if !f(variables.ArgsNamesRaw, v.argsNamesRaw) {
return
}
if !f(variables.ArgsGetNamesRaw, v.argsGetNamesRaw) {
return
}
if !f(variables.ArgsPostNamesRaw, v.argsPostNamesRaw) {
return
}
if !f(variables.Duration, v.duration) {
return
}
Expand Down
Loading
Loading