Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

appsec: SDK function for parsed http body instrumentation #1178

Merged
merged 8 commits into from Mar 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -13,5 +13,6 @@
/internal/traceprof @DataDog/profiling-go

# appsec
/appsec @DataDog/appsec-go
/internal/appsec @DataDog/appsec-go
/contrib/**/appsec.go @DataDog/appsec-go
32 changes: 32 additions & 0 deletions appsec/appsec.go
@@ -0,0 +1,32 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2022 Datadog, Inc.

// Package appsec provides application security features in the form of SDK
// functions that can be manually called to monitor specific code paths and data.
// Application Security is currently transparently integrated into the APM tracer
// and cannot be used nor started alone at the moment.
// You can read more on how to enable and start Application Security for Go at
// https://docs.datadoghq.com/security_platform/application_security/getting_started/go
package appsec

import (
"golang.org/x/net/context"

"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/httpsec"
)

// MonitorParsedHTTPBody runs the security monitoring rules on the given *parsed*
// HTTP request body. The given context must be the HTTP request context as returned
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
// by the Context() method of an HTTP request. Calls to this function are ignored if
// AppSec is disabled or the given context is incorrect.
// Note that passing the raw bytes of the HTTP request body is not expected and would
// result in inaccurate attack detection.
func MonitorParsedHTTPBody(ctx context.Context, body interface{}) {
if appsec.Enabled() {
httpsec.MonitorParsedBody(ctx, body)
}
// bonus: use sync.Once to log a debug message once if AppSec is disabled
}
62 changes: 62 additions & 0 deletions appsec/example_test.go
@@ -0,0 +1,62 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2022 Datadog, Inc.

package appsec_test

import (
"encoding/json"
"io"
"net/http"

"gopkg.in/DataDog/dd-trace-go.v1/appsec"
echotrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/labstack/echo.v4"
httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http"

"github.com/labstack/echo/v4"
)

type parsedBodyType struct {
Value string `json:"value"`
}

func customBodyParser(body io.ReadCloser) (*parsedBodyType, error) {
var parsedBody parsedBodyType
err := json.NewDecoder(body).Decode(&parsedBody)
return &parsedBody, err
}

// Monitor HTTP request parsed body
func ExampleMonitorParsedHTTPBody() {
mux := httptrace.NewServeMux()
mux.HandleFunc("/body", func(w http.ResponseWriter, r *http.Request) {
// Use the SDK to monitor the request's parsed body
body, err := customBodyParser(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
appsec.MonitorParsedHTTPBody(r.Context(), body)
w.Write([]byte("Body monitored using AppSec SDK\n"))
})
http.ListenAndServe(":8080", mux)
}

// Monitor HTTP request parsed body with a framework customized context type
func ExampleMonitorParsedHTTPBody_CustomContext() {
r := echo.New()
r.Use(echotrace.Middleware())
r.POST("/body", func(c echo.Context) (e error) {
req := c.Request()
body, err := customBodyParser(req.Body)
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
// Use the SDK to monitor the request's parsed body
appsec.MonitorParsedHTTPBody(c.Request().Context(), body)
return c.String(http.StatusOK, "Body monitored using AppSec SDK")
})

r.Start(":8080")
}
3 changes: 2 additions & 1 deletion contrib/gin-gonic/gin/appsec.go
Expand Up @@ -27,7 +27,8 @@ func useAppSec(c *gin.Context, span tracer.Span) func() {
}
}
args := httpsec.MakeHandlerOperationArgs(req, params)
op := httpsec.StartOperation(args, nil)
ctx, op := httpsec.StartOperation(req.Context(), args)
c.Request = req.WithContext(ctx)
return func() {
events := op.Finish(httpsec.HandlerOperationRes{Status: c.Writer.Status()})
if len(events) > 0 {
Expand Down
30 changes: 30 additions & 0 deletions contrib/gin-gonic/gin/gintrace_test.go
Expand Up @@ -15,6 +15,7 @@ import (
"strings"
"testing"

pappsec "gopkg.in/DataDog/dd-trace-go.v1/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
Expand Down Expand Up @@ -554,6 +555,10 @@ func TestAppSec(t *testing.T) {
r.Any("/path0.0/:myPathParam0/path0.1/:myPathParam1/path0.2/:myPathParam2/path0.3/*param3", func(c *gin.Context) {
c.String(200, "Hello Params!\n")
})
r.Any("/body", func(c *gin.Context) {
pappsec.MonitorParsedHTTPBody(c.Request.Context(), "$globals")
c.String(200, "Hello Body!\n")
})

srv := httptest.NewServer(r)
defer srv.Close()
Expand Down Expand Up @@ -630,4 +635,29 @@ func TestAppSec(t *testing.T) {
require.True(t, strings.Contains(event, "nfd-000-001"))

})

// Test a PHP injection attack via request parsed body
t.Run("SDK-body", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

req, err := http.NewRequest("POST", srv.URL+"/body", nil)
if err != nil {
panic(err)
}
res, err := srv.Client().Do(req)
require.NoError(t, err)

// Check that the handler was properly called
b, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "Hello Body!\n", string(b))

finished := mt.FinishedSpans()
require.Len(t, finished, 1)

event := finished[0].Tag("_dd.appsec.json")
require.NotNil(t, event)
require.True(t, strings.Contains(event.(string), "crs-933-130"))
})
}
31 changes: 31 additions & 0 deletions contrib/go-chi/chi.v4/chi_test.go
Expand Up @@ -14,6 +14,7 @@ import (
"strings"
"testing"

pappsec "gopkg.in/DataDog/dd-trace-go.v1/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
Expand Down Expand Up @@ -323,6 +324,11 @@ func TestAppSec(t *testing.T) {
_, err := w.Write([]byte("Hello World!\n"))
require.NoError(t, err)
})
router.HandleFunc("/body", func(w http.ResponseWriter, r *http.Request) {
pappsec.MonitorParsedHTTPBody(r.Context(), "$globals")
_, err := w.Write([]byte("Hello Body!\n"))
require.NoError(t, err)
})

srv := httptest.NewServer(router)
defer srv.Close()
Expand Down Expand Up @@ -379,4 +385,29 @@ func TestAppSec(t *testing.T) {
require.True(t, strings.Contains(event, "myPathParam2"))
require.True(t, strings.Contains(event, "server.request.path_params"))
})

// Test a PHP injection attack via request parsed body
t.Run("SDK-body", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

req, err := http.NewRequest("POST", srv.URL+"/body", nil)
if err != nil {
panic(err)
}
res, err := srv.Client().Do(req)
require.NoError(t, err)

// Check that the handler was properly called
b, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "Hello Body!\n", string(b))

finished := mt.FinishedSpans()
require.Len(t, finished, 1)

event := finished[0].Tag("_dd.appsec.json")
require.NotNil(t, event)
require.True(t, strings.Contains(event.(string), "crs-933-130"))
})
}
31 changes: 31 additions & 0 deletions contrib/go-chi/chi.v5/chi_test.go
Expand Up @@ -14,6 +14,7 @@ import (
"strings"
"testing"

pappsec "gopkg.in/DataDog/dd-trace-go.v1/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
Expand Down Expand Up @@ -323,6 +324,11 @@ func TestAppSec(t *testing.T) {
_, err := w.Write([]byte("Hello World!\n"))
require.NoError(t, err)
})
router.HandleFunc("/body", func(w http.ResponseWriter, r *http.Request) {
pappsec.MonitorParsedHTTPBody(r.Context(), "$globals")
_, err := w.Write([]byte("Hello Body!\n"))
require.NoError(t, err)
})

srv := httptest.NewServer(router)
defer srv.Close()
Expand Down Expand Up @@ -379,4 +385,29 @@ func TestAppSec(t *testing.T) {
require.True(t, strings.Contains(event, "myPathParam2"))
require.True(t, strings.Contains(event, "server.request.path_params"))
})

// Test a PHP injection attack via request parsed body
t.Run("SDK-body", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

req, err := http.NewRequest("POST", srv.URL+"/body", nil)
if err != nil {
panic(err)
}
res, err := srv.Client().Do(req)
require.NoError(t, err)

// Check that the handler was properly called
b, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "Hello Body!\n", string(b))

finished := mt.FinishedSpans()
require.Len(t, finished, 1)

event := finished[0].Tag("_dd.appsec.json")
require.NotNil(t, event)
require.True(t, strings.Contains(event.(string), "crs-933-130"))
})
}
31 changes: 31 additions & 0 deletions contrib/go-chi/chi/chi_test.go
Expand Up @@ -14,6 +14,7 @@ import (
"strings"
"testing"

pappsec "gopkg.in/DataDog/dd-trace-go.v1/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
Expand Down Expand Up @@ -323,6 +324,11 @@ func TestAppSec(t *testing.T) {
_, err := w.Write([]byte("Hello World!\n"))
require.NoError(t, err)
})
router.HandleFunc("/body", func(w http.ResponseWriter, r *http.Request) {
pappsec.MonitorParsedHTTPBody(r.Context(), "$globals")
_, err := w.Write([]byte("Hello Body!\n"))
require.NoError(t, err)
})

srv := httptest.NewServer(router)
defer srv.Close()
Expand Down Expand Up @@ -379,4 +385,29 @@ func TestAppSec(t *testing.T) {
require.True(t, strings.Contains(event, "myPathParam2"))
require.True(t, strings.Contains(event, "server.request.path_params"))
})

// Test a PHP injection attack via request parsed body
t.Run("SDK-body", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

req, err := http.NewRequest("POST", srv.URL+"/body", nil)
if err != nil {
panic(err)
}
res, err := srv.Client().Do(req)
require.NoError(t, err)

// Check that the handler was properly called
b, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "Hello Body!\n", string(b))

finished := mt.FinishedSpans()
require.Len(t, finished, 1)

event := finished[0].Tag("_dd.appsec.json")
require.NotNil(t, event)
require.True(t, strings.Contains(event.(string), "crs-933-130"))
})
}
29 changes: 29 additions & 0 deletions contrib/gorilla/mux/mux_test.go
Expand Up @@ -14,6 +14,7 @@ import (
"strings"
"testing"

pappsec "gopkg.in/DataDog/dd-trace-go.v1/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
Expand Down Expand Up @@ -326,6 +327,11 @@ func TestAppSec(t *testing.T) {
_, err := w.Write([]byte("Hello World!\n"))
require.NoError(t, err)
})
router.HandleFunc("/body", func(w http.ResponseWriter, r *http.Request) {
pappsec.MonitorParsedHTTPBody(r.Context(), "$globals")
_, err := w.Write([]byte("Hello Body!\n"))
require.NoError(t, err)
})

srv := httptest.NewServer(router)
defer srv.Close()
Expand Down Expand Up @@ -404,4 +410,27 @@ func TestAppSec(t *testing.T) {
require.True(t, strings.Contains(event, "server.response.status"))
require.True(t, strings.Contains(event, "nfd-000-001"))
})

// Test a PHP injection attack via request parsed body
t.Run("SDK-body", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

req, err := http.NewRequest("POST", srv.URL+"/body", nil)
if err != nil {
panic(err)
}
res, err := srv.Client().Do(req)
require.NoError(t, err)
// Check that the handler was properly called
b, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "Hello Body!\n", string(b))

finished := mt.FinishedSpans()
require.Len(t, finished, 1)
event := finished[0].Tag("_dd.appsec.json")
require.NotNil(t, event)
require.True(t, strings.Contains(event.(string), "crs-933-130"))
})
}
3 changes: 2 additions & 1 deletion contrib/labstack/echo.v4/appsec.go
Expand Up @@ -22,7 +22,8 @@ func useAppSec(c echo.Context, span tracer.Span) func() {
params[n] = c.Param(n)
}
args := httpsec.MakeHandlerOperationArgs(req, params)
op := httpsec.StartOperation(args, nil)
ctx, op := httpsec.StartOperation(req.Context(), args)
c.SetRequest(req.WithContext(ctx))
return func() {
events := op.Finish(httpsec.HandlerOperationRes{Status: c.Response().Status})
if len(events) > 0 {
Expand Down