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

internal/appsec: implement WAF actions for http/grpc #1533

Merged
merged 34 commits into from Dec 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2b83f16
internal/appsec: implement IPFromRequest
Hellzy Oct 20, 2022
f224b27
internal/appsec: pass http.clientIP through operation args
Hellzy Oct 20, 2022
c1c1364
internal/appsec: pass htpt.client_ip address to the WAF
Hellzy Oct 20, 2022
566a6c5
internal/appsec: internal/appsec: add support for WAF actions
Hellzy Nov 3, 2022
dea3413
internal/appsec/dyngo: write response header before body in blocking …
Hellzy Nov 16, 2022
e6f2902
internal/appsec/dyngo: clear actions/events before second waf run
Hellzy Nov 16, 2022
b7e1d8a
internal/appsec/httpsec: remove appsec build tag constraint for actions
Hellzy Nov 21, 2022
733a143
Apply suggestions from code review
Hellzy Nov 23, 2022
9f24219
internal/appsec/dyngo: simplify BlockRequestAction struct
Hellzy Nov 23, 2022
36db486
internal/appsec: actionHandler.Apply returns true if blocking
Hellzy Nov 23, 2022
17010e7
internal/appsec: add mutex for Operation actions
Hellzy Nov 23, 2022
aad08f8
internal/appsec: add mutex for action handler
Hellzy Nov 23, 2022
8a48b47
internal/appsec: add RLock call to security events holder
Hellzy Nov 23, 2022
9f6411e
appsec: grpc: remove unary/stream handlers and use status code
Hellzy Nov 23, 2022
4a75723
internal/appsec: go lint
Hellzy Nov 24, 2022
49a7c27
internal/appsec: move applyActions() to a dedicated function
Hellzy Nov 24, 2022
4320cd0
appsec: grpc: retrieve http.client_ip and use action handler
Hellzy Nov 24, 2022
9663fac
internal/appsec: dyngo: use go embed for blocked response default tem…
Hellzy Dec 5, 2022
6c03b89
Merge branch 'main' into francois.mazeau/ip-blocking
Hellzy Dec 6, 2022
11a4cfb
internal/appsec: defer wafCtx.Close()
Hellzy Dec 6, 2022
62e93b0
internal/appsec: add request blocking test case
Hellzy Dec 6, 2022
ca98bff
internal/appsec: golint
Hellzy Dec 6, 2022
e4379ba
appsec: test grpc IP blocking
Hellzy Dec 8, 2022
2286b11
internal/appsec: fix content type when blocking
Hellzy Dec 8, 2022
9272678
grpcsec: report the http.client_ip span tag
Julio-Guerra Dec 8, 2022
c7377af
grpcsec: report the http.client_ip span tag
Julio-Guerra Dec 8, 2022
cf3ac2f
Merge branch 'main' into francois.mazeau/ip-blocking
Julio-Guerra Dec 9, 2022
7f24120
internal/appsec: close waf ctx when blocking
Hellzy Dec 9, 2022
71c6c73
internal/appsec: add actions_test.go
Hellzy Dec 9, 2022
7281ec4
internal/appsec: report private IP if no global IP found
Hellzy Dec 9, 2022
fde2717
appsec: retrieve remote addr for grpc
Hellzy Dec 9, 2022
a266362
github: update codeowners file to better cover appsec files in contrib
Julio-Guerra Dec 11, 2022
3d9d7d4
appsec: fix client ip tags with peer ip addresses
Julio-Guerra Dec 12, 2022
e7a01b3
Merge branch 'main' into francois.mazeau/ip-blocking
Julio-Guerra Dec 12, 2022
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
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -15,7 +15,7 @@
# appsec
/appsec @DataDog/appsec-go
/internal/appsec @DataDog/appsec-go
/contrib/**/appsec.go @DataDog/appsec-go
/contrib/**/*appsec*.go @DataDog/appsec-go
/.github/workflows/appsec.yml @DataDog/appsec-go

# telemetry
Expand Down
20 changes: 10 additions & 10 deletions contrib/gin-gonic/gin/appsec.go
Expand Up @@ -6,8 +6,6 @@
package gin

import (
"net"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/httpsec"
Expand All @@ -18,27 +16,29 @@ import (
// useAppSec executes the AppSec logic related to the operation start and
// returns the function to be executed upon finishing the operation
func useAppSec(c *gin.Context, span tracer.Span) func() {
req := c.Request
instrumentation.SetAppSecEnabledTags(span)

var params map[string]string
if l := len(c.Params); l > 0 {
params = make(map[string]string, l)
for _, p := range c.Params {
params[p.Key] = p.Value
}
}
args := httpsec.MakeHandlerOperationArgs(req, params)

req := c.Request
ipTags, clientIP := httpsec.ClientIPTags(req.Header, req.RemoteAddr)
instrumentation.SetStringTags(span, ipTags)

args := httpsec.MakeHandlerOperationArgs(req, clientIP, params)
ctx, op := httpsec.StartOperation(req.Context(), args)
c.Request = req.WithContext(ctx)

return func() {
events := op.Finish(httpsec.HandlerOperationRes{Status: c.Writer.Status()})
instrumentation.SetTags(span, op.Tags())
if len(events) > 0 {
remoteIP, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
remoteIP = req.RemoteAddr
}
httpsec.SetSecurityEventTags(span, events, remoteIP, args.Headers, c.Writer.Header())
httpsec.SetSecurityEventTags(span, events, args.Headers, c.Writer.Header())
}
instrumentation.SetTags(span, op.Tags())
}
}
45 changes: 34 additions & 11 deletions contrib/google.golang.org/grpc/appsec.go
Expand Up @@ -7,32 +7,46 @@ package grpc

import (
"encoding/json"
"net"

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

"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
)

// UnaryHandler wrapper to use when AppSec is enabled to monitor its execution.
func appsecUnaryHandlerMiddleware(span ddtrace.Span, handler grpc.UnaryHandler) grpc.UnaryHandler {
instrumentation.SetAppSecEnabledTags(span)
return func(ctx context.Context, req interface{}) (interface{}, error) {
md, _ := metadata.FromIncomingContext(ctx)
op := grpcsec.StartHandlerOperation(grpcsec.HandlerOperationArgs{Metadata: md}, nil)
var remoteAddr string
if p, ok := peer.FromContext(ctx); ok {
remoteAddr = p.Addr.String()
}
ipTags, clientIP := httpsec.ClientIPTags(md, remoteAddr)
instrumentation.SetStringTags(span, ipTags)

op := grpcsec.StartHandlerOperation(grpcsec.HandlerOperationArgs{Metadata: md, ClientIP: clientIP}, nil)
defer func() {
events := op.Finish(grpcsec.HandlerOperationRes{})
instrumentation.SetTags(span, op.Tags())
if len(events) == 0 {
return
}
setAppSecTags(ctx, span, events)
setAppSecEventsTags(ctx, span, events)
}()

if op.BlockedCode != nil {
op.AddTag(httpsec.BlockedRequestTag, true)
return nil, status.Errorf(*op.BlockedCode, "Request blocked")
}

defer grpcsec.StartReceiveOperation(grpcsec.ReceiveOperationArgs{}, op).Finish(grpcsec.ReceiveOperationRes{Message: req})
return handler(ctx, req)
}
Expand All @@ -43,15 +57,28 @@ func appsecStreamHandlerMiddleware(span ddtrace.Span, handler grpc.StreamHandler
instrumentation.SetAppSecEnabledTags(span)
return func(srv interface{}, stream grpc.ServerStream) error {
md, _ := metadata.FromIncomingContext(stream.Context())
op := grpcsec.StartHandlerOperation(grpcsec.HandlerOperationArgs{Metadata: md}, nil)
var remoteAddr string
if p, ok := peer.FromContext(stream.Context()); ok {
remoteAddr = p.Addr.String()
}
ipTags, clientIP := httpsec.ClientIPTags(md, remoteAddr)
instrumentation.SetStringTags(span, ipTags)

op := grpcsec.StartHandlerOperation(grpcsec.HandlerOperationArgs{Metadata: md, ClientIP: clientIP}, nil)
defer func() {
events := op.Finish(grpcsec.HandlerOperationRes{})
instrumentation.SetTags(span, op.Tags())
if len(events) == 0 {
return
}
setAppSecTags(stream.Context(), span, events)
setAppSecEventsTags(stream.Context(), span, events)
}()

if op.BlockedCode != nil {
op.AddTag(httpsec.BlockedRequestTag, true)
return status.Error(*op.BlockedCode, "Request blocked")
}

return handler(srv, appsecServerStream{ServerStream: stream, handlerOperation: op})
}
}
Expand All @@ -72,11 +99,7 @@ func (ss appsecServerStream) RecvMsg(m interface{}) error {
}

// Set the AppSec tags when security events were found.
func setAppSecTags(ctx context.Context, span ddtrace.Span, events []json.RawMessage) {
func setAppSecEventsTags(ctx context.Context, span ddtrace.Span, events []json.RawMessage) {
md, _ := metadata.FromIncomingContext(ctx)
var addr net.Addr
if p, ok := peer.FromContext(ctx); ok {
addr = p.Addr
}
grpcsec.SetSecurityEventTags(span, events, addr, md)
grpcsec.SetSecurityEventTags(span, events, md)
}
89 changes: 89 additions & 0 deletions contrib/google.golang.org/grpc/appsec_test.go
Expand Up @@ -14,7 +14,9 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec"

"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)

func TestAppSec(t *testing.T) {
Expand Down Expand Up @@ -94,3 +96,90 @@ func TestAppSec(t *testing.T) {
require.True(t, strings.Contains(event, "ua0-600-55x")) // canary rule attack attempt
})
}

// Test that http blocking works by using custom rules/rules data
func TestBlocking(t *testing.T) {
t.Setenv("DD_APPSEC_RULES", "../../../internal/appsec/testdata/blocking.json")
appsec.Start()
defer appsec.Stop()
if !appsec.Enabled() {
t.Skip("appsec disabled")
}

rig, err := newRig(false)
require.NoError(t, err)
defer rig.Close()

client := rig.client

t.Run("unary-block", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

// Send a XSS attack in the payload along with the canary value in the RPC metadata
ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("dd-canary", "dd-test-scanner-log", "x-client-ip", "1.2.3.4"))
reply, err := client.Ping(ctx, &FixtureRequest{Name: "<script>alert('xss');</script>"})

require.Nil(t, reply)
require.Equal(t, codes.Aborted, status.Code(err))

finished := mt.FinishedSpans()
require.Len(t, finished, 1)
// The request should have the attack attempts
event, _ := finished[0].Tag("_dd.appsec.json").(string)
require.NotNil(t, event)
require.True(t, strings.Contains(event, "blk-001-001"))
})

t.Run("unary-no-block", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

// Send a XSS attack in the payload along with the canary value in the RPC metadata
ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("dd-canary", "dd-test-scanner-log", "x-client-ip", "1.2.3.5"))
reply, err := client.Ping(ctx, &FixtureRequest{Name: "<script>alert('xss');</script>"})

require.Equal(t, "passed", reply.Message)
require.Equal(t, codes.OK, status.Code(err))
})

t.Run("stream-block", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("dd-canary", "dd-test-scanner-log", "x-client-ip", "1.2.3.4"))
stream, err := client.StreamPing(ctx)
require.NoError(t, err)
reply, err := stream.Recv()

require.Equal(t, codes.Aborted, status.Code(err))
require.Nil(t, reply)

finished := mt.FinishedSpans()
require.Len(t, finished, 1)
// The request should have the attack attempts
event, _ := finished[0].Tag("_dd.appsec.json").(string)
require.NotNil(t, event)
require.True(t, strings.Contains(event, "blk-001-001"))
})

t.Run("stream-no-block", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("dd-canary", "dd-test-scanner-log", "x-client-ip", "1.2.3.5"))
stream, err := client.StreamPing(ctx)
require.NoError(t, err)

// Send a XSS attack
err = stream.Send(&FixtureRequest{Name: "<script>alert('xss');</script>"})
require.NoError(t, err)
reply, err := stream.Recv()
require.Equal(t, codes.OK, status.Code(err))
require.Equal(t, "passed", reply.Message)

err = stream.CloseSend()
require.NoError(t, err)
})

}
17 changes: 8 additions & 9 deletions contrib/labstack/echo.v4/appsec.go
Expand Up @@ -6,8 +6,6 @@
package echo

import (
"net"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/httpsec"
Expand All @@ -16,23 +14,24 @@ import (
)

func useAppSec(c echo.Context, span tracer.Span) func() {
req := c.Request()
instrumentation.SetAppSecEnabledTags(span)

params := make(map[string]string)
for _, n := range c.ParamNames() {
params[n] = c.Param(n)
}
args := httpsec.MakeHandlerOperationArgs(req, params)

req := c.Request()
ipTags, clientIP := httpsec.ClientIPTags(req.Header, req.RemoteAddr)
instrumentation.SetStringTags(span, ipTags)

args := httpsec.MakeHandlerOperationArgs(req, clientIP, params)
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 {
remoteIP, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
remoteIP = req.RemoteAddr
}
httpsec.SetSecurityEventTags(span, events, remoteIP, args.Headers, c.Response().Writer.Header())
httpsec.SetSecurityEventTags(span, events, args.Headers, c.Response().Writer.Header())
}
instrumentation.SetTags(span, op.Tags())
}
Expand Down
27 changes: 23 additions & 4 deletions internal/appsec/dyngo/instrumentation/common.go
Expand Up @@ -49,7 +49,7 @@ func (m *TagsHolder) Tags() map[string]interface{} {
// See httpsec/http.go and grpcsec/grpc.go.
type SecurityEventsHolder struct {
events []json.RawMessage
mu sync.Mutex
mu sync.RWMutex
}

// AddSecurityEvents adds the security events to the collected events list.
Expand All @@ -62,16 +62,33 @@ func (s *SecurityEventsHolder) AddSecurityEvents(events ...json.RawMessage) {

// Events returns the list of stored events.
func (s *SecurityEventsHolder) Events() []json.RawMessage {
s.mu.RLock()
defer s.mu.RUnlock()
return s.events
}

// ClearEvents clears the list of stored events
func (s *SecurityEventsHolder) ClearEvents() {
s.mu.Lock()
defer s.mu.Unlock()
s.events = s.events[0:0]
}

// SetTags fills the span tags using the key/value pairs found in `tags`
func SetTags(span TagSetter, tags map[string]interface{}) {
for k, v := range tags {
span.SetTag(k, v)
}
}

// SetStringTags fills the span tags using the key/value pairs of strings found
// in `tags`
func SetStringTags(span TagSetter, tags map[string]string) {
for k, v := range tags {
span.SetTag(k, v)
}
}

// SetAppSecEnabledTags sets the AppSec-specific span tags that are expected to be in
// the web service entry span (span of type `web`) when AppSec is enabled.
func SetAppSecEnabledTags(span TagSetter) {
Expand Down Expand Up @@ -102,9 +119,11 @@ func SetEventSpanTags(span TagSetter, events []json.RawMessage) error {

// Create the value of the security event tag.
// TODO(Julio-Guerra): a future libddwaf version should return something
// avoiding us the following events concatenation logic which currently
// involves unserializing the top-level JSON arrays to concatenate them
// together.
//
// avoiding us the following events concatenation logic which currently
// involves unserializing the top-level JSON arrays to concatenate them
// together.
//
// TODO(Julio-Guerra): avoid serializing the json in the request hot path
func makeEventTagValue(events []json.RawMessage) (json.RawMessage, error) {
var v interface{}
Expand Down