diff --git a/examples/http-server/.gitignore b/examples/http-server/.gitignore new file mode 100644 index 000000000..d8085cf9b --- /dev/null +++ b/examples/http-server/.gitignore @@ -0,0 +1 @@ +http-server diff --git a/examples/http-server/README.md b/examples/http-server/README.md new file mode 100644 index 000000000..c9307076a --- /dev/null +++ b/examples/http-server/README.md @@ -0,0 +1,57 @@ +# HTTP-Server with Coraza + +This example is intended to provide a straightforward way to spin up Coraza and grasp its behaviour. + +## Run the example + +```bash +go run . +``` + +The server will be reachable at `http://localhost:8090`. + +```bash +# True positive request (403 Forbidden) +curl -i 'localhost:8090/hello?id=0' +# True negative request (200 OK) +curl -i 'localhost:8090/hello' +``` + +You can customise the rules to be used by using the `DIRECTIVES_FILE` environment variable to load a directives file: + +```bash +DIRECTIVES_FILE=my_directives.conf go run . +``` + +You can also customise response body and response headers by using `RESPONSE_HEADERS` and `RESPONSE_BODY` environment variables respectively: + +```bash +RESPONSE_BODY=creditcard go run . +``` + +And then + +```bash +# True positive request (403 Forbidden) due to matching response body +curl -i 'localhost:8090/hello' +``` + +## Customize WAF rules + +The configuration of the WAF relies on [default.conf](https://github.com/corazawaf/coraza/blob/main/examples/http-server/default.conf). Feel free to play with it. + +## Customize server behaviour + +The following snippet shows an example of code that may be added to the [exampleHandler](https://github.com/corazawaf/coraza/blob/main/examples/http-server/main.go#L17) in order to make the example capable of echoing the body request. It comes in handy for testing rules that match the response body. + +```go +func exampleHandler(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "text/plain") + var buf bytes.Buffer + _, err := io.Copy(&buf, req.Body) + if err != nil { + log.Fatalf("handler can not read request body: %v", err) + } + w.Write(buf.Bytes()) +} +``` diff --git a/examples/http-server/default.conf b/examples/http-server/default.conf new file mode 100644 index 000000000..7f3edc52e --- /dev/null +++ b/examples/http-server/default.conf @@ -0,0 +1,7 @@ +SecDebugLogLevel 9 +SecDebugLog /dev/stdout + +SecRule ARGS:id "@eq 0" "id:1, phase:1,deny, status:403,msg:'Invalid id',log,auditlog" + +SecRequestBodyAccess On +SecRule REQUEST_BODY "@contains password" "id:100, phase:2,deny, status:403,msg:'Invalid request body',log,auditlog" diff --git a/examples/http-server/go.mod b/examples/http-server/go.mod new file mode 100644 index 000000000..5423bbdf3 --- /dev/null +++ b/examples/http-server/go.mod @@ -0,0 +1,12 @@ +module github.com/corazawaf/coraza/v3/examples/http-server + +go 1.19 + +require github.com/corazawaf/coraza/v3 v3.0.0-20220914101451-05d352c89b24 + +require ( + github.com/magefile/mage v1.15.0 // indirect + github.com/tidwall/gjson v1.17.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect +) diff --git a/examples/http-server/go.sum b/examples/http-server/go.sum new file mode 100644 index 000000000..e91936f74 --- /dev/null +++ b/examples/http-server/go.sum @@ -0,0 +1,11 @@ +github.com/corazawaf/coraza/v3 v3.0.0-20220914101451-05d352c89b24 h1:dy3992o5ue40g1QWKupjsBwZTRWagsuiGcOsbV0b4xs= +github.com/corazawaf/coraza/v3 v3.0.0-20220914101451-05d352c89b24/go.mod h1:xhc7feR6FUfYgmBmRw3UObvLiyzT3XPQtlJD+huy+Mc= +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/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= diff --git a/examples/http-server/main.go b/examples/http-server/main.go new file mode 100644 index 000000000..6f18e085d --- /dev/null +++ b/examples/http-server/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "strings" + + "github.com/crowdsecurity/coraza/v3" + txhttp "github.com/crowdsecurity/coraza/v3/http" + "github.com/crowdsecurity/coraza/v3/types" +) + +func exampleHandler(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "text/plain") + resBody := "Hello world, transaction not disrupted." + + if body := os.Getenv("RESPONSE_BODY"); body != "" { + resBody = body + } + + if h := os.Getenv("RESPONSE_HEADERS"); h != "" { + key, val, _ := strings.Cut(h, ":") + w.Header().Set(key, val) + } + + // The server generates the response + w.Write([]byte(resBody)) +} + +func main() { + waf := createWAF() + + http.Handle("/", txhttp.WrapHandler(waf, http.HandlerFunc(exampleHandler))) + + fmt.Println("Server is running. Listening port: 8090") + + log.Fatal(http.ListenAndServe(":8090", nil)) +} + +func createWAF() coraza.WAF { + directivesFile := "./default.conf" + if s := os.Getenv("DIRECTIVES_FILE"); s != "" { + directivesFile = s + } + + waf, err := coraza.NewWAF( + coraza.NewWAFConfig(). + WithErrorCallback(logError). + WithDirectivesFromFile(directivesFile), + ) + if err != nil { + log.Fatal(err) + } + return waf +} + +func logError(error types.MatchedRule) { + msg := error.ErrorLog() + fmt.Printf("[logError][%s] %s\n", error.Rule().Severity(), msg) +} diff --git a/examples/http-server/main_test.go b/examples/http-server/main_test.go new file mode 100644 index 000000000..9440bdeb9 --- /dev/null +++ b/examples/http-server/main_test.go @@ -0,0 +1,115 @@ +package main + +import ( + "bytes" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + + txhttp "github.com/crowdsecurity/coraza/v3/http" +) + +func setupTestServer(t *testing.T) *httptest.Server { + t.Helper() + waf := createWAF() + return httptest.NewServer(txhttp.WrapHandler(waf, http.HandlerFunc(exampleHandler))) +} + +func doGetRequest(t *testing.T, getPath string) int { + t.Helper() + resp, err := http.Get(getPath) + if err != nil { + log.Fatalln(err) + } + resp.Body.Close() + return resp.StatusCode +} + +func doPostRequest(t *testing.T, postPath string, data []byte) int { + t.Helper() + resp, err := http.Post(postPath, "application/x-www-form-urlencoded", bytes.NewBuffer(data)) + if err != nil { + log.Fatalln(err) + } + resp.Body.Close() + return resp.StatusCode +} + +func TestHttpServer(t *testing.T) { + tests := []struct { + name string + path string + expStatus int + envVars map[string]string + body []byte // if body is populated, POST request is sent + }{ + {"negative", "/", 200, nil, nil}, + {"positive for query parameter", "/?id=0", 403, nil, nil}, + { + "positive for response body", + "/", + 403, + map[string]string{ + "DIRECTIVES_FILE": "./testdata/response-body.conf", + "RESPONSE_BODY": "creditcard", + }, + nil, + }, + { + "positive for response header", + "/", + 403, + map[string]string{ + "DIRECTIVES_FILE": "./testdata/response-headers.conf", + "RESPONSE_HEADERS": "foo:bar", + }, + nil, + }, + { + "negative for request body process partial (payload beyond processed body)", + "/", + 200, + map[string]string{ + "DIRECTIVES_FILE": "./testdata/request-body-limits-processpartial.conf", + }, + []byte("beyond the limit script"), + }, + { + "positive for response body limit reject", + "/", + 413, + map[string]string{ + "DIRECTIVES_FILE": "./testdata/response-body-limits-reject.conf", + "RESPONSE_BODY": "response body beyond the limit", + }, + nil, + }, + } + // Perform tests + for _, tc := range tests { + tt := tc + var statusCode int + t.Run(tt.name, func(t *testing.T) { + if len(tt.envVars) > 0 { + for k, v := range tt.envVars { + os.Setenv(k, v) + defer os.Unsetenv(k) + } + } + + // Spin up the test server + testServer := setupTestServer(t) + defer testServer.Close() + if tt.body == nil { + statusCode = doGetRequest(t, testServer.URL+tt.path) + } else { + statusCode = doPostRequest(t, testServer.URL+tt.path, tt.body) + } + if want, have := tt.expStatus, statusCode; want != have { + t.Errorf("Unexpected status code, want: %d, have: %d", want, have) + } + }) + } +} diff --git a/examples/http-server/testdata/request-body-limits-processpartial.conf b/examples/http-server/testdata/request-body-limits-processpartial.conf new file mode 100644 index 000000000..b359eb933 --- /dev/null +++ b/examples/http-server/testdata/request-body-limits-processpartial.conf @@ -0,0 +1,7 @@ +SecDebugLogLevel 9 +SecDebugLog /dev/stdout +SecRequestBodyAccess On +SecRequestBodyInMemoryLimit 5 +SecRequestBodyLimit 6 +SecRequestBodyLimitAction ProcessPartial +SecRule REQUEST_BODY "@contains script" "id:200, phase:2, deny, status:403, msg:'Invalid request body',log,auditlog" diff --git a/examples/http-server/testdata/response-body-limits-reject.conf b/examples/http-server/testdata/response-body-limits-reject.conf new file mode 100644 index 000000000..506017d66 --- /dev/null +++ b/examples/http-server/testdata/response-body-limits-reject.conf @@ -0,0 +1,6 @@ +SecDebugLogLevel 9 +SecDebugLog /dev/stdout +SecResponseBodyAccess On +SecResponseBodyMimeType text/plain +SecResponseBodyLimit 6 +SecResponseBodyLimitAction Reject diff --git a/examples/http-server/testdata/response-body.conf b/examples/http-server/testdata/response-body.conf new file mode 100644 index 000000000..073bcec7a --- /dev/null +++ b/examples/http-server/testdata/response-body.conf @@ -0,0 +1,5 @@ +SecDebugLogLevel 9 +SecDebugLog /dev/stdout +SecResponseBodyAccess On +SecResponseBodyMimeType text/plain +SecRule RESPONSE_BODY "@contains creditcard" "id:200, phase:4,deny, status:403,msg:'Invalid response body',log,auditlog" diff --git a/examples/http-server/testdata/response-headers.conf b/examples/http-server/testdata/response-headers.conf new file mode 100644 index 000000000..b894845d3 --- /dev/null +++ b/examples/http-server/testdata/response-headers.conf @@ -0,0 +1,3 @@ +SecDebugLogLevel 9 +SecDebugLog /dev/stdout +SecRule RESPONSE_HEADERS:Foo "@pm bar" "id:199,phase:3,deny,t:lowercase,deny, status:403,msg:'Invalid response header',log,auditlog"