Skip to content

Commit 6126fb6

Browse files
authored
appsec: support for SSRF Exploit Prevention (#2707)
Co-authored-by Eliott Bouhana <[email protected]> Co-authored-by: Julio Guerra <[email protected]>
1 parent 9298b8d commit 6126fb6

File tree

30 files changed

+553
-149
lines changed

30 files changed

+553
-149
lines changed

Diff for: appsec/events/block.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2022 Datadog, Inc.
5+
6+
// Package events provides security event types that appsec can return in function calls it monitors when blocking them.
7+
// It allows finer-grained integrations of appsec into your Go errors' management logic.
8+
package events
9+
10+
import "errors"
11+
12+
var _ error = (*BlockingSecurityEvent)(nil)
13+
14+
var securityError = &BlockingSecurityEvent{}
15+
16+
// BlockingSecurityEvent is the error type returned by function calls blocked by appsec.
17+
// Even though appsec takes care of responding automatically to the blocked requests, it
18+
// is your duty to abort the request handlers that are calling functions blocked by appsec.
19+
// For instance, if a gRPC handler performs a SQL query blocked by appsec, the SQL query
20+
// function call gets blocked and aborted by returning an error of type SecurityBlockingEvent.
21+
// This allows you to safely abort your request handlers, and to be able to leverage errors.As if
22+
// necessary in your Go error management logic to be able to tell if the error is a blocking security
23+
// event or not (eg. to avoid retrying an HTTP client request).
24+
type BlockingSecurityEvent struct{}
25+
26+
func (*BlockingSecurityEvent) Error() string {
27+
return "request blocked by WAF"
28+
}
29+
30+
// IsSecurityError returns true if the error is a security event.
31+
func IsSecurityError(err error) bool {
32+
return errors.Is(err, securityError)
33+
}

Diff for: contrib/google.golang.org/grpc/appsec.go

+11-8
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,13 @@ func appsecUnaryHandlerMiddleware(method string, span ddtrace.Span, handler grpc
6262
return nil, err
6363
}
6464
defer grpcsec.StartReceiveOperation(types.ReceiveOperationArgs{}, op).Finish(types.ReceiveOperationRes{Message: req})
65-
rv, err := handler(ctx, req)
66-
if e, ok := err.(*types.MonitoringError); ok {
67-
err = status.Error(codes.Code(e.GRPCStatus()), e.Error())
65+
66+
rv, downstreamErr := handler(ctx, req)
67+
if blocked {
68+
return nil, err
6869
}
69-
return rv, err
70+
71+
return rv, downstreamErr
7072
}
7173
}
7274

@@ -113,11 +115,12 @@ func appsecStreamHandlerMiddleware(method string, span ddtrace.Span, handler grp
113115
return err
114116
}
115117

116-
err = handler(srv, stream)
117-
if e, ok := err.(*types.MonitoringError); ok {
118-
err = status.Error(codes.Code(e.GRPCStatus()), e.Error())
118+
downstreamErr := handler(srv, stream)
119+
if blocked {
120+
return err
119121
}
120-
return err
122+
123+
return downstreamErr
121124
}
122125
}
123126

Diff for: contrib/labstack/echo.v4/appsec.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ package echo
88
import (
99
"net/http"
1010

11+
"gopkg.in/DataDog/dd-trace-go.v1/appsec/events"
1112
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
1213
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec"
13-
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec/types"
1414

1515
"github.com/labstack/echo/v4"
1616
)
@@ -27,7 +27,7 @@ func withAppSec(next echo.HandlerFunc, span tracer.Span) echo.HandlerFunc {
2727
err = next(c)
2828
// If the error is a monitoring one, it means appsec actions will take care of writing the response
2929
// and handling the error. Don't call the echo error handler in this case
30-
if _, ok := err.(*types.MonitoringError); !ok && err != nil {
30+
if _, ok := err.(*events.BlockingSecurityEvent); !ok && err != nil {
3131
c.Error(err)
3232
}
3333
})

Diff for: contrib/net/http/roundtripper.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package http
77

88
import (
99
"fmt"
10+
"gopkg.in/DataDog/dd-trace-go.v1/appsec/events"
1011
"math"
1112
"net/http"
1213
"os"
@@ -15,6 +16,8 @@ import (
1516
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
1617
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
1718
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
19+
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec"
20+
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec"
1821
)
1922

2023
type roundTripper struct {
@@ -57,7 +60,7 @@ func (rt *roundTripper) RoundTrip(req *http.Request) (res *http.Response, err er
5760
if rt.cfg.after != nil {
5861
rt.cfg.after(res, span)
5962
}
60-
if rt.cfg.errCheck == nil || rt.cfg.errCheck(err) {
63+
if !events.IsSecurityError(err) && (rt.cfg.errCheck == nil || rt.cfg.errCheck(err)) {
6164
span.Finish(tracer.WithError(err))
6265
} else {
6366
span.Finish()
@@ -75,7 +78,15 @@ func (rt *roundTripper) RoundTrip(req *http.Request) (res *http.Response, err er
7578
fmt.Fprintf(os.Stderr, "contrib/net/http.Roundtrip: failed to inject http headers: %v\n", err)
7679
}
7780
}
81+
82+
if appsec.RASPEnabled() {
83+
if err := httpsec.ProtectRoundTrip(ctx, r2.URL.String()); err != nil {
84+
return nil, err
85+
}
86+
}
87+
7888
res, err = rt.base.RoundTrip(r2)
89+
7990
if err != nil {
8091
span.SetTag("http.errors", err.Error())
8192
if rt.cfg.errCheck == nil || rt.cfg.errCheck(err) {

Diff for: contrib/net/http/roundtripper_test.go

+78
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ import (
1616
"testing"
1717
"time"
1818

19+
"gopkg.in/DataDog/dd-trace-go.v1/appsec/events"
1920
"gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/namingschematest"
2021
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
2122
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
2223
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer"
2324
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
25+
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec"
26+
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/httpsec"
2427
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
2528

2629
"github.com/stretchr/testify/assert"
@@ -619,3 +622,78 @@ func TestClientNamingSchema(t *testing.T) {
619622
t.Run("ServiceName", namingschematest.NewServiceNameTest(genSpans, wantServiceNameV0))
620623
t.Run("SpanName", namingschematest.NewSpanNameTest(genSpans, assertOpV0, assertOpV1))
621624
}
625+
626+
type emptyRoundTripper struct{}
627+
628+
func (rt *emptyRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) {
629+
recorder := httptest.NewRecorder()
630+
recorder.WriteHeader(200)
631+
return recorder.Result(), nil
632+
}
633+
634+
func TestAppsec(t *testing.T) {
635+
t.Setenv("DD_APPSEC_RULES", "../../../internal/appsec/testdata/rasp.json")
636+
637+
client := WrapRoundTripper(&emptyRoundTripper{})
638+
639+
for _, enabled := range []bool{true, false} {
640+
641+
t.Run(strconv.FormatBool(enabled), func(t *testing.T) {
642+
t.Setenv("DD_APPSEC_RASP_ENABLED", strconv.FormatBool(enabled))
643+
644+
mt := mocktracer.Start()
645+
defer mt.Stop()
646+
647+
appsec.Start()
648+
if !appsec.Enabled() {
649+
t.Skip("appsec not enabled")
650+
}
651+
652+
defer appsec.Stop()
653+
654+
w := httptest.NewRecorder()
655+
r, err := http.NewRequest("GET", "?value=169.254.169.254", nil)
656+
require.NoError(t, err)
657+
658+
TraceAndServe(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
659+
req, err := http.NewRequest("GET", "http://169.254.169.254", nil)
660+
require.NoError(t, err)
661+
662+
resp, err := client.RoundTrip(req.WithContext(r.Context()))
663+
664+
if enabled {
665+
require.ErrorIs(t, err, &events.BlockingSecurityEvent{})
666+
} else {
667+
require.NoError(t, err)
668+
}
669+
670+
if resp != nil {
671+
defer resp.Body.Close()
672+
}
673+
}), w, r, &ServeConfig{
674+
Service: "service",
675+
Resource: "resource",
676+
})
677+
678+
spans := mt.FinishedSpans()
679+
require.Len(t, spans, 2) // service entry serviceSpan & http request serviceSpan
680+
serviceSpan := spans[1]
681+
682+
if !enabled {
683+
require.NotContains(t, serviceSpan.Tags(), "_dd.appsec.json")
684+
require.NotContains(t, serviceSpan.Tags(), "_dd.stack")
685+
return
686+
}
687+
688+
require.Contains(t, serviceSpan.Tags(), "_dd.appsec.json")
689+
appsecJSON := serviceSpan.Tag("_dd.appsec.json")
690+
require.Contains(t, appsecJSON, httpsec.ServerIoNetURLAddr)
691+
692+
require.Contains(t, serviceSpan.Tags(), "_dd.stack")
693+
694+
// This is a nested event so it should contain the child span id in the service entry span
695+
// TODO(eliott.bouhana): uncomment this once we have the child span id in the service entry span
696+
// require.Contains(t, appsecJSON, `"span_id":`+strconv.FormatUint(requestSpan.SpanID(), 10))
697+
})
698+
}
699+
}

Diff for: go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.20
55
require (
66
cloud.google.com/go/pubsub v1.33.0
77
github.com/99designs/gqlgen v0.17.36
8-
github.com/DataDog/appsec-internal-go v1.5.0
8+
github.com/DataDog/appsec-internal-go v1.6.0
99
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0
1010
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1
1111
github.com/DataDog/datadog-go/v5 v5.3.0

Diff for: go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -624,8 +624,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9s
624624
github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1/go.mod h1:4qFor3D/HDsvBME35Xy9rwW9DecL+M2sNw1ybjPtwA0=
625625
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
626626
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
627-
github.com/DataDog/appsec-internal-go v1.5.0 h1:8kS5zSx5T49uZ8dZTdT19QVAvC/B8ByyZdhQKYQWHno=
628-
github.com/DataDog/appsec-internal-go v1.5.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U=
627+
github.com/DataDog/appsec-internal-go v1.6.0 h1:QHvPOv/O0s2fSI/BraZJNpRDAtdlrRm5APJFZNBxjAw=
628+
github.com/DataDog/appsec-internal-go v1.6.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U=
629629
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8=
630630
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo=
631631
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 h1:5nE6N3JSs2IG3xzMthNFhXfOaXlrsdgqmJ73lndFf8c=

Diff for: internal/apps/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require (
88
)
99

1010
require (
11-
github.com/DataDog/appsec-internal-go v1.5.0 // indirect
11+
github.com/DataDog/appsec-internal-go v1.6.0 // indirect
1212
github.com/DataDog/go-libddwaf/v3 v3.2.0 // indirect
1313
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
1414
github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect

Diff for: internal/apps/go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
github.com/DataDog/appsec-internal-go v1.5.0 h1:8kS5zSx5T49uZ8dZTdT19QVAvC/B8ByyZdhQKYQWHno=
2-
github.com/DataDog/appsec-internal-go v1.5.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U=
1+
github.com/DataDog/appsec-internal-go v1.6.0 h1:QHvPOv/O0s2fSI/BraZJNpRDAtdlrRm5APJFZNBxjAw=
2+
github.com/DataDog/appsec-internal-go v1.6.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U=
33
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8=
44
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo=
55
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 h1:5nE6N3JSs2IG3xzMthNFhXfOaXlrsdgqmJ73lndFf8c=

Diff for: internal/appsec/appsec.go

+8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ func Enabled() bool {
2626
return activeAppSec != nil && activeAppSec.started
2727
}
2828

29+
// RASPEnabled returns true when DD_APPSEC_RASP_ENABLED=true or is unset. Granted that AppSec is enabled.
30+
func RASPEnabled() bool {
31+
mu.RLock()
32+
defer mu.RUnlock()
33+
return activeAppSec != nil && activeAppSec.started && activeAppSec.cfg.RASP
34+
}
35+
2936
// Start AppSec when enabled is enabled by both using the appsec build tag and
3037
// setting the environment variable DD_APPSEC_ENABLED to true.
3138
func Start(opts ...config.StartOption) {
@@ -162,6 +169,7 @@ func (a *appsec) start(telemetry *appsecTelemetry) error {
162169
}
163170

164171
a.enableRCBlocking()
172+
a.enableRASP()
165173

166174
a.started = true
167175
log.Info("appsec: up and running")

Diff for: internal/appsec/config/config.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ type Config struct {
6666
// APISec configuration
6767
APISec internal.APISecConfig
6868
// RC is the remote configuration client used to receive product configuration updates. Nil if RC is disabled (default)
69-
RC *remoteconfig.ClientConfig
69+
RC *remoteconfig.ClientConfig
70+
RASP bool
7071
}
7172

7273
// WithRCConfig sets the AppSec remote config client configuration to the specified cfg
@@ -115,5 +116,6 @@ func NewConfig() (*Config, error) {
115116
TraceRateLimit: int64(internal.RateLimitFromEnv()),
116117
Obfuscator: internal.NewObfuscatorConfig(),
117118
APISec: internal.NewAPISecConfig(),
119+
RASP: internal.RASPEnabled(),
118120
}, nil
119121
}

Diff for: internal/appsec/emitter/grpcsec/types/types.go

-25
Original file line numberDiff line numberDiff line change
@@ -72,33 +72,8 @@ type (
7272
// Corresponds to the address `grpc.server.request.message`.
7373
Message interface{}
7474
}
75-
76-
// MonitoringError is used to vehicle a gRPC error that also embeds a request status code
77-
MonitoringError struct {
78-
msg string
79-
status uint32
80-
}
8175
)
8276

83-
// NewMonitoringError creates and returns a new gRPC monitoring error, wrapped under
84-
// sharedesec.MonitoringError
85-
func NewMonitoringError(msg string, code uint32) error {
86-
return &MonitoringError{
87-
msg: msg,
88-
status: code,
89-
}
90-
}
91-
92-
// GRPCStatus returns the gRPC status code embedded in the error
93-
func (e *MonitoringError) GRPCStatus() uint32 {
94-
return e.status
95-
}
96-
97-
// Error implements the error interface
98-
func (e *MonitoringError) Error() string {
99-
return e.msg
100-
}
101-
10277
// Finish the gRPC handler operation, along with the given results, and emit a
10378
// finish event up in the operation stack.
10479
func (op *HandlerOperation) Finish(res HandlerOperationRes) []any {

0 commit comments

Comments
 (0)