Skip to content

Commit

Permalink
appsec: SDK function for parsed http body instrumentation (#1178)
Browse files Browse the repository at this point in the history
- Add appsec root directory to expose AppSec SDK function
- Add new SDKBody operation to httpsec
- Keep track of ongoing operations in the context
- Update gin/echo code due to operation start/finish prototype changes
- Add CODEOWNERS entry for appsec/ directory
  • Loading branch information
Hellzy committed Mar 3, 2022
1 parent 585a18a commit 85593c8
Show file tree
Hide file tree
Showing 14 changed files with 463 additions and 66 deletions.
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
// 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

0 comments on commit 85593c8

Please sign in to comment.