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: support for SSRF Exploit Prevention #2707

Merged
merged 22 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8a6bac3
appsec: support for SSRF Exploit Prevention
eliottness Mar 15, 2024
95f8872
remove spanID and nested event span_id
eliottness Mar 25, 2024
ab7887e
TEMP!: hardcode RASP SSRF rule & span tag for staging
eliottness Mar 25, 2024
81aabd8
listeners: remove timeout arg for roundtriper
Hellzy May 23, 2024
314c6e2
waf_test: used sharedsec.HTTPClientIPAddre
Hellzy May 23, 2024
55e8fec
waf.go: remove hardcoded rasp ssrf rule
Hellzy May 24, 2024
c63c77e
http: add stacktraces to span
Hellzy May 24, 2024
171c9a6
Register RC capa and check RASP enablement
Hellzy May 24, 2024
e8a34d3
imports police
Hellzy May 24, 2024
b93e0dc
net/http: remove rasp test tag
Hellzy May 27, 2024
709b466
emitter: simplify to 1 generated stacktrace only
Hellzy May 27, 2024
f045f29
Don't add nil stacktraces to spans
Hellzy May 27, 2024
5619fa4
internal/appsec: refactor roundtripper and add new events.SecurityBlo…
eliottness May 28, 2024
cd8164a
rename stack_trace action in generate_stack as per libddwaf upgrading…
eliottness May 29, 2024
985b0aa
fix multiple issues:
eliottness May 31, 2024
a24d23a
rename appsec/events.SecurityBlockingEvent -> BlockingSecurityEvent
eliottness May 31, 2024
b2f4edf
misc suggestions from @Julio-Guerra
eliottness May 31, 2024
463d661
appsec: grpc: remove MonitoringError and fix user blocking
eliottness Jun 3, 2024
dc164df
run go mod tidy
eliottness Jun 3, 2024
8d6f944
internal/appsec: add appsec.RASPEnabled() and use it to disable RASP …
eliottness Jun 3, 2024
c835a08
Update internal/appsec/emitter/httpsec/roundtripper.go
eliottness Jun 3, 2024
aef04d2
order imports
eliottness Jun 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions appsec/events/block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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 events provides security event types that appsec can return in function calls it monitors when blocking them.
// It allows finer-grained integrations of appsec into your Go errors' management logic.
package events

import "errors"

var _ error = (*BlockingSecurityEvent)(nil)

var securityError = &BlockingSecurityEvent{}

// BlockingSecurityEvent is the error type returned by function calls blocked by appsec.
// Even though appsec takes care of responding automatically to the blocked requests, it
// is your duty to abort the request handlers that are calling functions blocked by appsec.
// For instance, if a gRPC handler performs a SQL query blocked by appsec, the SQL query
// function call gets blocked and aborted by returning an error of type SecurityBlockingEvent.
// This allows you to safely abort your request handlers, and to be able to leverage errors.As if
// necessary in your Go error management logic to be able to tell if the error is a blocking security
// event or not (eg. to avoid retrying an HTTP client request).
type BlockingSecurityEvent struct{}

func (*BlockingSecurityEvent) Error() string {
return "request blocked by WAF"
}

// IsSecurityError returns true if the error is a security event.
func IsSecurityError(err error) bool {
return errors.Is(err, securityError)
}
19 changes: 11 additions & 8 deletions contrib/google.golang.org/grpc/appsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@ func appsecUnaryHandlerMiddleware(method string, span ddtrace.Span, handler grpc
return nil, err
}
defer grpcsec.StartReceiveOperation(types.ReceiveOperationArgs{}, op).Finish(types.ReceiveOperationRes{Message: req})
rv, err := handler(ctx, req)
if e, ok := err.(*types.MonitoringError); ok {
err = status.Error(codes.Code(e.GRPCStatus()), e.Error())

rv, downstreamErr := handler(ctx, req)
if blocked {
return nil, err
}
return rv, err

return rv, downstreamErr
}
}

Expand Down Expand Up @@ -113,11 +115,12 @@ func appsecStreamHandlerMiddleware(method string, span ddtrace.Span, handler grp
return err
}

err = handler(srv, stream)
if e, ok := err.(*types.MonitoringError); ok {
err = status.Error(codes.Code(e.GRPCStatus()), e.Error())
downstreamErr := handler(srv, stream)
if blocked {
return err
}
return err

return downstreamErr
}
}

Expand Down
4 changes: 2 additions & 2 deletions contrib/labstack/echo.v4/appsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ package echo
import (
"net/http"

"gopkg.in/DataDog/dd-trace-go.v1/appsec/events"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec/types"

"github.com/labstack/echo/v4"
)
Expand All @@ -27,7 +27,7 @@ func withAppSec(next echo.HandlerFunc, span tracer.Span) echo.HandlerFunc {
err = next(c)
// If the error is a monitoring one, it means appsec actions will take care of writing the response
// and handling the error. Don't call the echo error handler in this case
if _, ok := err.(*types.MonitoringError); !ok && err != nil {
if _, ok := err.(*events.BlockingSecurityEvent); !ok && err != nil {
c.Error(err)
}
})
Expand Down
13 changes: 12 additions & 1 deletion contrib/net/http/roundtripper.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package http

import (
"fmt"
"gopkg.in/DataDog/dd-trace-go.v1/appsec/events"
"math"
"net/http"
"os"
Expand All @@ -15,6 +16,8 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec"
)

type roundTripper struct {
Expand Down Expand Up @@ -57,7 +60,7 @@ func (rt *roundTripper) RoundTrip(req *http.Request) (res *http.Response, err er
if rt.cfg.after != nil {
rt.cfg.after(res, span)
}
if rt.cfg.errCheck == nil || rt.cfg.errCheck(err) {
if !events.IsSecurityError(err) && (rt.cfg.errCheck == nil || rt.cfg.errCheck(err)) {
span.Finish(tracer.WithError(err))
} else {
span.Finish()
Expand All @@ -75,7 +78,15 @@ func (rt *roundTripper) RoundTrip(req *http.Request) (res *http.Response, err er
fmt.Fprintf(os.Stderr, "contrib/net/http.Roundtrip: failed to inject http headers: %v\n", err)
}
}

if appsec.RASPEnabled() {
if err := httpsec.ProtectRoundTrip(ctx, r2.URL.String()); err != nil {
return nil, err
}
}

res, err = rt.base.RoundTrip(r2)

if err != nil {
span.SetTag("http.errors", err.Error())
if rt.cfg.errCheck == nil || rt.cfg.errCheck(err) {
Expand Down
78 changes: 78 additions & 0 deletions contrib/net/http/roundtripper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ import (
"testing"
"time"

"gopkg.in/DataDog/dd-trace-go.v1/appsec/events"
"gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/namingschematest"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"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"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/httpsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -619,3 +622,78 @@ func TestClientNamingSchema(t *testing.T) {
t.Run("ServiceName", namingschematest.NewServiceNameTest(genSpans, wantServiceNameV0))
t.Run("SpanName", namingschematest.NewSpanNameTest(genSpans, assertOpV0, assertOpV1))
}

type emptyRoundTripper struct{}

func (rt *emptyRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) {
recorder := httptest.NewRecorder()
recorder.WriteHeader(200)
return recorder.Result(), nil
}

func TestAppsec(t *testing.T) {
t.Setenv("DD_APPSEC_RULES", "../../../internal/appsec/testdata/rasp.json")

client := WrapRoundTripper(&emptyRoundTripper{})

for _, enabled := range []bool{true, false} {

t.Run(strconv.FormatBool(enabled), func(t *testing.T) {
t.Setenv("DD_APPSEC_RASP_ENABLED", strconv.FormatBool(enabled))

mt := mocktracer.Start()
defer mt.Stop()

appsec.Start()
if !appsec.Enabled() {
t.Skip("appsec not enabled")
}

defer appsec.Stop()

w := httptest.NewRecorder()
r, err := http.NewRequest("GET", "?value=169.254.169.254", nil)
require.NoError(t, err)

TraceAndServe(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
req, err := http.NewRequest("GET", "http://169.254.169.254", nil)
require.NoError(t, err)

resp, err := client.RoundTrip(req.WithContext(r.Context()))

if enabled {
require.ErrorIs(t, err, &events.BlockingSecurityEvent{})
} else {
require.NoError(t, err)
}

if resp != nil {
defer resp.Body.Close()
}
}), w, r, &ServeConfig{
Service: "service",
Resource: "resource",
})

spans := mt.FinishedSpans()
require.Len(t, spans, 2) // service entry serviceSpan & http request serviceSpan
serviceSpan := spans[1]

if !enabled {
require.NotContains(t, serviceSpan.Tags(), "_dd.appsec.json")
require.NotContains(t, serviceSpan.Tags(), "_dd.stack")
return
}

require.Contains(t, serviceSpan.Tags(), "_dd.appsec.json")
appsecJSON := serviceSpan.Tag("_dd.appsec.json")
require.Contains(t, appsecJSON, httpsec.ServerIoNetURLAddr)

require.Contains(t, serviceSpan.Tags(), "_dd.stack")

// This is a nested event so it should contain the child span id in the service entry span
// TODO(eliott.bouhana): uncomment this once we have the child span id in the service entry span
// require.Contains(t, appsecJSON, `"span_id":`+strconv.FormatUint(requestSpan.SpanID(), 10))
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.20
require (
cloud.google.com/go/pubsub v1.33.0
github.com/99designs/gqlgen v0.17.36
github.com/DataDog/appsec-internal-go v1.5.0
github.com/DataDog/appsec-internal-go v1.6.0
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1
github.com/DataDog/datadog-go/v5 v5.3.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -624,8 +624,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9s
github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1/go.mod h1:4qFor3D/HDsvBME35Xy9rwW9DecL+M2sNw1ybjPtwA0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/appsec-internal-go v1.5.0 h1:8kS5zSx5T49uZ8dZTdT19QVAvC/B8ByyZdhQKYQWHno=
github.com/DataDog/appsec-internal-go v1.5.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U=
github.com/DataDog/appsec-internal-go v1.6.0 h1:QHvPOv/O0s2fSI/BraZJNpRDAtdlrRm5APJFZNBxjAw=
github.com/DataDog/appsec-internal-go v1.6.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 h1:5nE6N3JSs2IG3xzMthNFhXfOaXlrsdgqmJ73lndFf8c=
Expand Down
2 changes: 1 addition & 1 deletion internal/apps/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
)

require (
github.com/DataDog/appsec-internal-go v1.5.0 // indirect
github.com/DataDog/appsec-internal-go v1.6.0 // indirect
github.com/DataDog/go-libddwaf/v3 v3.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect
Expand Down
4 changes: 2 additions & 2 deletions internal/apps/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/DataDog/appsec-internal-go v1.5.0 h1:8kS5zSx5T49uZ8dZTdT19QVAvC/B8ByyZdhQKYQWHno=
github.com/DataDog/appsec-internal-go v1.5.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U=
github.com/DataDog/appsec-internal-go v1.6.0 h1:QHvPOv/O0s2fSI/BraZJNpRDAtdlrRm5APJFZNBxjAw=
github.com/DataDog/appsec-internal-go v1.6.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 h1:5nE6N3JSs2IG3xzMthNFhXfOaXlrsdgqmJ73lndFf8c=
Expand Down
8 changes: 8 additions & 0 deletions internal/appsec/appsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ func Enabled() bool {
return activeAppSec != nil && activeAppSec.started
}

// RASPEnabled returns true when DD_APPSEC_RASP_ENABLED=true or is unset. Granted that AppSec is enabled.
func RASPEnabled() bool {
mu.RLock()
defer mu.RUnlock()
return activeAppSec != nil && activeAppSec.started && activeAppSec.cfg.RASP
}

// Start AppSec when enabled is enabled by both using the appsec build tag and
// setting the environment variable DD_APPSEC_ENABLED to true.
func Start(opts ...config.StartOption) {
Expand Down Expand Up @@ -162,6 +169,7 @@ func (a *appsec) start(telemetry *appsecTelemetry) error {
}

a.enableRCBlocking()
a.enableRASP()

a.started = true
log.Info("appsec: up and running")
Expand Down
4 changes: 3 additions & 1 deletion internal/appsec/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ type Config struct {
// APISec configuration
APISec internal.APISecConfig
// RC is the remote configuration client used to receive product configuration updates. Nil if RC is disabled (default)
RC *remoteconfig.ClientConfig
RC *remoteconfig.ClientConfig
RASP bool
}

// WithRCConfig sets the AppSec remote config client configuration to the specified cfg
Expand Down Expand Up @@ -115,5 +116,6 @@ func NewConfig() (*Config, error) {
TraceRateLimit: int64(internal.RateLimitFromEnv()),
Obfuscator: internal.NewObfuscatorConfig(),
APISec: internal.NewAPISecConfig(),
RASP: internal.RASPEnabled(),
}, nil
}
25 changes: 0 additions & 25 deletions internal/appsec/emitter/grpcsec/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,33 +72,8 @@ type (
// Corresponds to the address `grpc.server.request.message`.
Message interface{}
}

// MonitoringError is used to vehicle a gRPC error that also embeds a request status code
MonitoringError struct {
msg string
status uint32
}
)

// NewMonitoringError creates and returns a new gRPC monitoring error, wrapped under
// sharedesec.MonitoringError
func NewMonitoringError(msg string, code uint32) error {
return &MonitoringError{
msg: msg,
status: code,
}
}

// GRPCStatus returns the gRPC status code embedded in the error
func (e *MonitoringError) GRPCStatus() uint32 {
return e.status
}

// Error implements the error interface
func (e *MonitoringError) Error() string {
return e.msg
}

// Finish the gRPC handler operation, along with the given results, and emit a
// finish event up in the operation stack.
func (op *HandlerOperation) Finish(res HandlerOperationRes) []any {
Expand Down
Loading
Loading