From 24af0c8cf4f10bab558740b595712be3b85493ec Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosso Date: Mon, 6 Dec 2021 18:49:14 -0300 Subject: [PATCH] fix: multipart rewrite --- bodyprocessors/multipart.go | 115 ++++++++++++++++++++----------- bodyprocessors/multipart_test.go | 60 ++++++++++++++++ transaction_test.go | 2 +- types/variables/variables.go | 4 +- 4 files changed, 138 insertions(+), 43 deletions(-) create mode 100644 bodyprocessors/multipart_test.go diff --git a/bodyprocessors/multipart.go b/bodyprocessors/multipart.go index a305a7180..c249c81b7 100644 --- a/bodyprocessors/multipart.go +++ b/bodyprocessors/multipart.go @@ -17,7 +17,11 @@ package bodyprocessors import ( "fmt" "io" - "net/http" + "log" + "mime" + "mime/multipart" + "os" + "strings" "github.com/jptosso/coraza-waf/v2/types/variables" ) @@ -26,53 +30,84 @@ type multipartBodyProcessor struct { collections *collectionsMap } -func (mbp *multipartBodyProcessor) Read(reader io.Reader, mime string, storagePath string) error { - req, _ := http.NewRequest("GET", "/", reader) - req.Header.Set("Content-Type", mime) - err := req.ParseMultipartForm(1000000000) +func (mbp *multipartBodyProcessor) Read(reader io.Reader, mimeType string, storagePath string) error { + mediaType, params, err := mime.ParseMediaType(mimeType) if err != nil { - return err + log.Fatal(err) } - totalSize := int64(0) - fn := map[string][]string{ - "": {}, - } - fl := map[string][]string{ - "": {}, + if !strings.HasPrefix(mediaType, "multipart/") { + return fmt.Errorf("not a multipart body") } - fs := map[string][]string{ - "": {}, - } - for field, fheaders := range req.MultipartForm.File { - // TODO add them to temporal storage - // or maybe not, according to http.MultipartForm, it does exactly that - // the main issue is how do I get this path? - fn[""] = append(fn[""], field) - for _, header := range fheaders { - fl[""] = append(fl[""], header.Filename) - totalSize += header.Size - fs[""] = append(fs[""], fmt.Sprintf("%d", header.Size)) + mr := multipart.NewReader(reader, params["boundary"]) + totalSize := int64(0) + filesNames := []string{} + filesArgNames := []string{} + fileList := []string{} + fileSizes := []string{} + postNames := []string{} + postFields := map[string][]string{} + for { + p, err := mr.NextPart() + if err == io.EOF { + break + } + if err != nil { + return err + } + // we create a temp file + + // if is a file + if p.FileName() != "" { + temp, err := os.CreateTemp(storagePath, "crzmp*") + if err != nil { + return err + } + sz, err := io.Copy(temp, p) + if err != nil { + return err + } + totalSize += sz + filesNames = append(filesNames, p.FileName()) + fileList = append(fileList, temp.Name()) + fileSizes = append(fileSizes, fmt.Sprintf("%d", sz)) + filesArgNames = append(filesArgNames, p.FormName()) + } else { + fmt.Println("VARIABLE", p.FormName()) + // if is a field + data, err := io.ReadAll(p) + if err != nil { + return err + } + totalSize += int64(len(data)) + postNames = append(postNames, p.FormName()) + if _, ok := postFields[p.FormName()]; !ok { + postFields[p.FormName()] = []string{} + } + postFields[p.FormName()] = append(postFields[p.FormName()], string(data)) + } - } - m := map[string][]string{} - names := []string{} - for k, vs := range req.MultipartForm.Value { - m[k] = vs - names = append(names, k) - } - fcs := map[string][]string{ - "": {fmt.Sprintf("%d", totalSize)}, } mbp.collections = &collectionsMap{ - variables.FilesNames: fn, - variables.Files: fl, - variables.FilesSizes: fs, - variables.FilesCombinedSize: fcs, - variables.ArgsPost: m, + variables.FilesNames: map[string][]string{ + "": filesArgNames, + }, + variables.FilesTmpNames: map[string][]string{ + "": fileList, + }, + variables.Files: map[string][]string{ + "": filesNames, + }, + variables.FilesSizes: map[string][]string{ + "": fileSizes, + }, variables.ArgsPostNames: map[string][]string{ - "": names, + "": postNames, + }, + variables.ArgsPost: postFields, + variables.Args: postFields, + variables.FilesCombinedSize: map[string][]string{ + "": {fmt.Sprintf("%d", totalSize)}, }, - variables.Args: m, } return nil diff --git a/bodyprocessors/multipart_test.go b/bodyprocessors/multipart_test.go new file mode 100644 index 000000000..176206ee5 --- /dev/null +++ b/bodyprocessors/multipart_test.go @@ -0,0 +1,60 @@ +package bodyprocessors + +import ( + "os" + "strings" + "testing" + + "github.com/jptosso/coraza-waf/v2/types/variables" +) + +func TestMultipartProcessor(t *testing.T) { + payload := `-----------------------------9051914041544843365972754266 +Content-Disposition: form-data; name="text" + +text default +-----------------------------9051914041544843365972754266 +Content-Disposition: form-data; name="file1"; filename="a.txt" +Content-Type: text/plain + +Content of a.txt. + +-----------------------------9051914041544843365972754266 +Content-Disposition: form-data; name="file2"; filename="a.html" +Content-Type: text/html + +Content of a.html. + +-----------------------------9051914041544843365972754266--` + payload = strings.ReplaceAll(payload, "\n", "\r\n") + + p := multipartBodyProcessor{} + if err := p.Read(strings.NewReader(payload), "multipart/form-data; boundary=---------------------------9051914041544843365972754266", "/tmp"); err != nil { + t.Error(err) + } + + res := p.Collections() + if len(res[variables.FilesNames][""]) != 2 { + t.Errorf("Expected 2 files, got %d", len(res[variables.FilesNames])) + } + if len(res[variables.ArgsPostNames]) != 1 { + t.Errorf("Expected 1 args, got %d", len(res[variables.ArgsPostNames])) + } + if len(res[variables.ArgsPost]["text"]) == 0 || res[variables.ArgsPost]["text"][0] != "text default" { + t.Errorf("Expected text3 to be 'some super text content 3', got %v", res[variables.ArgsPost]) + } + if len(res[variables.FilesTmpNames][""]) != 2 { + t.Errorf("Expected 2 files, got %d", len(res[variables.FilesTmpNames])) + } + if len(res[variables.FilesTmpNames]) > 0 { + if len(res[variables.FilesTmpNames][""]) == 0 { + t.Errorf("Expected files, got %d", len(res[variables.FilesTmpNames][""])) + } else { + fname := res[variables.FilesTmpNames][""][0] + if _, err := os.Stat(fname); err != nil { + t.Errorf("Expected file %s to exist", fname) + } + + } + } +} diff --git a/transaction_test.go b/transaction_test.go index d61feed56..74aee6310 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -91,7 +91,7 @@ func TestTxMultipart(t *testing.T) { } exp := map[string]string{ "%{args_post.text}": "test-value", - "%{files_combined_size}": "50", + "%{files_combined_size}": "60", "%{files}": "a.html", "%{files_names}": "file1", } diff --git a/types/variables/variables.go b/types/variables/variables.go index 6664d9fbe..a1b5d92dc 100644 --- a/types/variables/variables.go +++ b/types/variables/variables.go @@ -141,7 +141,7 @@ const ( // Geo contains the location information of the client Geo RuleVariable = iota RequestCookiesNames RuleVariable = iota - FilesTmpnames RuleVariable = iota + FilesTmpNames RuleVariable = iota // ArgsNames contains the names of the arguments (POST and GET) ArgsNames RuleVariable = iota // ArgsGetNames contains the names of the GET arguments @@ -243,7 +243,7 @@ var rulemap = map[RuleVariable]string{ ResponseHeaders: "RESPONSE_HEADERS", Geo: "GEO", RequestCookiesNames: "REQUEST_COOKIES_NAMES", - FilesTmpnames: "FILES_TMPNAMES", + FilesTmpNames: "FILES_TMPNAMES", ArgsNames: "ARGS_NAMES", ArgsGetNames: "ARGS_GET_NAMES", ArgsPostNames: "ARGS_POST_NAMES",