From e29ce50912f165ac4908cb9e1628e417e1da0415 Mon Sep 17 00:00:00 2001 From: Eliott Bouhana Date: Thu, 2 Jul 2026 17:15:59 +0200 Subject: [PATCH] feat(golang): add /rasp/cmdi endpoint + enable CMDi RASP tests on net-http-orchestrion Add the CMDI shared handler with array-capable request parsing (query/JSON/XML/urlencoded) and appsec.MonitorParsedHTTPBody, register /rasp/cmdi in the Go RASP weblogs (net-http-orchestrion, net-http, chi, gin, echo), and enable the Test_Cmdi_* classes for net-http-orchestrion in manifests/golang.yml. Exec runs via an instrumented os/exec path so the WAF evaluates server.sys.exec.cmd; no shell is used. --- manifests/golang.yml | 55 ++++++++++++---- .../docker/golang/app/_shared/rasp/rasp.go | 64 +++++++++++++++++++ utils/build/docker/golang/app/chi/main.go | 1 + utils/build/docker/golang/app/echo/main.go | 1 + utils/build/docker/golang/app/gin/main.go | 1 + .../golang/app/net-http-orchestrion/main.go | 1 + .../build/docker/golang/app/net-http/main.go | 1 + 7 files changed, 113 insertions(+), 11 deletions(-) diff --git a/manifests/golang.yml b/manifests/golang.yml index c544083463f..6f9e0b88459 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -222,18 +222,51 @@ manifest: tests/appsec/rasp/test_api10.py::Test_API10_response_status: v2.4.0 tests/appsec/rasp/test_api10.py::Test_API10_without_downstream_body_analysis_using_max: v2.4.0 tests/appsec/rasp/test_api10.py::Test_API10_without_downstream_body_analysis_using_sample_rate: v2.7.0-dev - tests/appsec/rasp/test_cmdi.py::Test_Cmdi_BodyJson: missing_feature - tests/appsec/rasp/test_cmdi.py::Test_Cmdi_BodyUrlEncoded: missing_feature - tests/appsec/rasp/test_cmdi.py::Test_Cmdi_BodyXml: missing_feature - tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Capability: missing_feature - tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Mandatory_SpanTags: missing_feature - tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Optional_SpanTags: missing_feature + tests/appsec/rasp/test_cmdi.py::Test_Cmdi_BodyJson: + - weblog_declaration: + "*": irrelevant (CMDi detection requires orchestrion) + net-http-orchestrion: v2.11.0-dev + tests/appsec/rasp/test_cmdi.py::Test_Cmdi_BodyUrlEncoded: + - weblog_declaration: + "*": irrelevant (CMDi detection requires orchestrion) + net-http-orchestrion: v2.11.0-dev + tests/appsec/rasp/test_cmdi.py::Test_Cmdi_BodyXml: + - weblog_declaration: + "*": irrelevant (CMDi detection requires orchestrion) + net-http-orchestrion: v2.11.0-dev + tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Capability: + - weblog_declaration: + "*": irrelevant (CMDi detection requires orchestrion) + net-http-orchestrion: v2.11.0-dev + tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Mandatory_SpanTags: + - weblog_declaration: + "*": irrelevant (CMDi detection requires orchestrion) + net-http-orchestrion: v2.11.0-dev + tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Optional_SpanTags: + - weblog_declaration: + "*": irrelevant (CMDi detection requires orchestrion) + net-http-orchestrion: v2.11.0-dev tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Rules_Version: v2.3.0-dev # Possibly earlier - tests/appsec/rasp/test_cmdi.py::Test_Cmdi_StackTrace: missing_feature - tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Telemetry: missing_feature - tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Telemetry_V2: missing_feature - tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Telemetry_Variant_Tag: missing_feature - tests/appsec/rasp/test_cmdi.py::Test_Cmdi_UrlQuery: missing_feature + tests/appsec/rasp/test_cmdi.py::Test_Cmdi_StackTrace: + - weblog_declaration: + "*": irrelevant (CMDi detection requires orchestrion) + net-http-orchestrion: v2.11.0-dev + tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Telemetry: + - weblog_declaration: + "*": irrelevant (CMDi detection requires orchestrion) + net-http-orchestrion: v2.11.0-dev + tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Telemetry_V2: + - weblog_declaration: + "*": irrelevant (CMDi detection requires orchestrion) + net-http-orchestrion: v2.11.0-dev + tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Telemetry_Variant_Tag: + - weblog_declaration: + "*": irrelevant (CMDi detection requires orchestrion) + net-http-orchestrion: v2.11.0-dev + tests/appsec/rasp/test_cmdi.py::Test_Cmdi_UrlQuery: + - weblog_declaration: + "*": irrelevant (CMDi detection requires orchestrion) + net-http-orchestrion: v2.11.0-dev tests/appsec/rasp/test_cmdi.py::Test_Cmdi_Waf_Version: v2.3.0-dev # Possibly earlier tests/appsec/rasp/test_lfi.py::Test_Lfi_BodyJson: - weblog_declaration: diff --git a/utils/build/docker/golang/app/_shared/rasp/rasp.go b/utils/build/docker/golang/app/_shared/rasp/rasp.go index 443a4a7b081..71ab6961847 100644 --- a/utils/build/docker/golang/app/_shared/rasp/rasp.go +++ b/utils/build/docker/golang/app/_shared/rasp/rasp.go @@ -7,6 +7,7 @@ import ( "log" "net/http" "os" + "os/exec" sqltrace "github.com/DataDog/dd-trace-go/contrib/database/sql/v2" httptrace "github.com/DataDog/dd-trace-go/contrib/net/http/v2" @@ -151,3 +152,66 @@ func SQLi(w http.ResponseWriter, r *http.Request) { log.Println("unknown error during sql call: ", err.Error()) } } + +func parseCommandRASPRequest(r *http.Request) []string { + switch r.Method { + case http.MethodGet: + return []string{r.URL.Query().Get("command")} + case http.MethodPost: + switch r.Header.Get("Content-Type") { + case "application/json": + var body struct { + Command []string `json:"command"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + log.Fatalf("failed to parse body: %v\n", err) + } + if err := appsec.MonitorParsedHTTPBody(r.Context(), map[string]any{"command": body.Command}); err != nil { + log.Fatalf("Body Monitoring should not block the request: %v\n", err) + } + return body.Command + case "application/xml": + var body struct { + Command []string `xml:"cmd"` + } + if err := xml.NewDecoder(r.Body).Decode(&body); err != nil { + log.Fatalf("failed to parse body: %v\n", err) + } + if err := appsec.MonitorParsedHTTPBody(r.Context(), map[string]any{"command": body.Command}); err != nil { + log.Fatalf("Body Monitoring should not block the request: %v\n", err) + } + return body.Command + case "application/x-www-form-urlencoded": + if err := r.ParseForm(); err != nil { + log.Fatalf("failed to parse body: %v\n", err) + } + command := r.Form.Get("command") + if err := appsec.MonitorParsedHTTPBody(r.Context(), map[string]string{"command": command}); err != nil { + log.Fatalf("Body Monitoring should not block the request: %v\n", err) + } + return []string{command} + default: + log.Fatalln("unsupported content type") + } + default: + log.Fatalln("method not allowed") + } + + return nil +} + +func CMDI(w http.ResponseWriter, r *http.Request) { + command := parseCommandRASPRequest(r) + if len(command) == 0 { + return + } + + err := (&exec.Cmd{Path: command[0], Args: command}).Run() + if events.IsSecurityError(err) { + return + } + + if err != nil { + log.Println("unknown error during command execution: ", err.Error()) + } +} diff --git a/utils/build/docker/golang/app/chi/main.go b/utils/build/docker/golang/app/chi/main.go index d372e89694a..8754f10c3b8 100644 --- a/utils/build/docker/golang/app/chi/main.go +++ b/utils/build/docker/golang/app/chi/main.go @@ -367,6 +367,7 @@ func main() { mux.HandleFunc("/rasp/multiple", rasp.LFIMultiple) mux.HandleFunc("/rasp/ssrf", rasp.SSRF) mux.HandleFunc("/rasp/sqli", rasp.SQLi) + mux.HandleFunc("/rasp/cmdi", rasp.CMDI) mux.HandleFunc("/external_request", rasp.ExternalRequest) mux.HandleFunc("GET /external_request/redirect", rasp.ExternalRedirectRequest) diff --git a/utils/build/docker/golang/app/echo/main.go b/utils/build/docker/golang/app/echo/main.go index 17fc3b88fae..5afd1670775 100644 --- a/utils/build/docker/golang/app/echo/main.go +++ b/utils/build/docker/golang/app/echo/main.go @@ -376,6 +376,7 @@ func main() { r.Any("/rasp/multiple", echoHandleFunc(rasp.LFIMultiple)) r.Any("/rasp/ssrf", echoHandleFunc(rasp.SSRF)) r.Any("/rasp/sqli", echoHandleFunc(rasp.SQLi)) + r.Any("/rasp/cmdi", echoHandleFunc(rasp.CMDI)) r.Any("/external_request", echoHandleFunc(rasp.ExternalRequest)) r.GET("/external_request/redirect", echoHandleFunc(rasp.ExternalRedirectRequest)) diff --git a/utils/build/docker/golang/app/gin/main.go b/utils/build/docker/golang/app/gin/main.go index 978e22e7f25..b90c4d7bc31 100644 --- a/utils/build/docker/golang/app/gin/main.go +++ b/utils/build/docker/golang/app/gin/main.go @@ -362,6 +362,7 @@ func main() { r.Any("/rasp/multiple", ginHandleFunc(rasp.LFIMultiple)) r.Any("/rasp/ssrf", ginHandleFunc(rasp.SSRF)) r.Any("/rasp/sqli", ginHandleFunc(rasp.SQLi)) + r.Any("/rasp/cmdi", ginHandleFunc(rasp.CMDI)) r.Any("/external_request", ginHandleFunc(rasp.ExternalRequest)) r.GET("/external_request/redirect", ginHandleFunc(rasp.ExternalRedirectRequest)) diff --git a/utils/build/docker/golang/app/net-http-orchestrion/main.go b/utils/build/docker/golang/app/net-http-orchestrion/main.go index 8e3693e47df..b1720526840 100644 --- a/utils/build/docker/golang/app/net-http-orchestrion/main.go +++ b/utils/build/docker/golang/app/net-http-orchestrion/main.go @@ -612,6 +612,7 @@ func main() { mux.HandleFunc("/rasp/multiple", rasp.LFIMultiple) mux.HandleFunc("/rasp/ssrf", rasp.SSRF) mux.HandleFunc("/rasp/sqli", rasp.SQLi) + mux.HandleFunc("/rasp/cmdi", rasp.CMDI) mux.HandleFunc("/external_request", rasp.ExternalRequest) mux.HandleFunc("GET /external_request/redirect", rasp.ExternalRedirectRequest) diff --git a/utils/build/docker/golang/app/net-http/main.go b/utils/build/docker/golang/app/net-http/main.go index aeb1c5ab8c9..917b27a900b 100644 --- a/utils/build/docker/golang/app/net-http/main.go +++ b/utils/build/docker/golang/app/net-http/main.go @@ -752,6 +752,7 @@ func main() { mux.HandleFunc("/rasp/multiple", rasp.LFIMultiple) mux.HandleFunc("/rasp/ssrf", rasp.SSRF) mux.HandleFunc("/rasp/sqli", rasp.SQLi) + mux.HandleFunc("/rasp/cmdi", rasp.CMDI) mux.HandleFunc("/add_event", func(w http.ResponseWriter, r *http.Request) { span, ok := tracer.SpanFromContext(r.Context())