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 25 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
28 changes: 20 additions & 8 deletions contrib/google.golang.org/grpc/appsec.go
Expand Up @@ -12,27 +12,34 @@ import (
"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)
ip := httpsec.IPFromHeaders(md, "")
op := grpcsec.StartHandlerOperation(grpcsec.HandlerOperationArgs{Metadata: md, ClientIP: ip}, nil)
defer func() {
events := op.Finish(grpcsec.HandlerOperationRes{})
instrumentation.SetTags(span, op.Tags())
if len(events) == 0 {
return
}
setAppSecTags(ctx, span, events)
setAppSecTags(ctx, span, ip, events)
}()
if op.BlockedCode != nil {
op.AddTag("appsec.blocked", 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 +50,20 @@ 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)
ip := httpsec.IPFromHeaders(md, "")
op := grpcsec.StartHandlerOperation(grpcsec.HandlerOperationArgs{Metadata: md, ClientIP: ip}, nil)
defer func() {
events := op.Finish(grpcsec.HandlerOperationRes{})
instrumentation.SetTags(span, op.Tags())
if len(events) == 0 {
return
}
setAppSecTags(stream.Context(), span, events)
setAppSecTags(stream.Context(), span, ip, events)
}()
if op.BlockedCode != nil {
op.AddTag("appsec.blocked", true)
return status.Error(*op.BlockedCode, "Request blocked")
}
return handler(srv, appsecServerStream{ServerStream: stream, handlerOperation: op})
}
}
Expand All @@ -72,11 +84,11 @@ 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 setAppSecTags(ctx context.Context, span ddtrace.Span, clientIP instrumentation.NetaddrIP, events []json.RawMessage) {
md, _ := metadata.FromIncomingContext(ctx)
var addr net.Addr
var peerAddr net.Addr
if p, ok := peer.FromContext(ctx); ok {
addr = p.Addr
peerAddr = p.Addr
}
grpcsec.SetSecurityEventTags(span, events, addr, md)
grpcsec.SetSecurityEventTags(span, events, clientIP, peerAddr, 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)
})

}
19 changes: 15 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,9 +62,18 @@ 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 {
Expand Down Expand Up @@ -102,9 +111,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
69 changes: 69 additions & 0 deletions internal/appsec/dyngo/instrumentation/grpcsec/actions.go
@@ -0,0 +1,69 @@
// 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.

//go:build appsec
// +build appsec

package grpcsec

import (
"sync"

"google.golang.org/grpc/codes"
)

// Action is used to identify any action kind
type Action interface {
isAction()
}

// ActionsHandler handles WAF actions registration and execution
type ActionsHandler struct {
mu sync.RWMutex
actions map[string]Action
}

// NewActionsHandler returns an action handler holding the default ASM actions.
// Currently, only the default "block" action is supported
func NewActionsHandler() ActionsHandler {
// Register the default "block" action as specified in the blocking RFC
actions := map[string]Action{"block": &BlockRequestAction{Status: codes.Aborted}}

return ActionsHandler{
actions: actions,
}
}

// RegisterAction registers a specific action to the actions handler. If the action kind is unknown
// the action will have no effect
func (h *ActionsHandler) RegisterAction(id string, a Action) {
h.mu.Lock()
defer h.mu.Unlock()
h.actions[id] = a
}

// Apply executes the action identified by `id`
func (h *ActionsHandler) Apply(id string, op *HandlerOperation) bool {
h.mu.RLock()
a, ok := h.actions[id]
h.mu.RUnlock()
if !ok {
return false
}
// Currently, only the "block_request" type is supported, so we only need to check for blockRequestParams
if p, ok := a.(*BlockRequestAction); ok {
op.BlockedCode = &p.Status
return true
}
return false
}

// BlockRequestAction is the struct used to perform the request blocking action
type BlockRequestAction struct {
// Status is the return code to use when blocking the request
Status codes.Code
}

func (*BlockRequestAction) isAction() {}
4 changes: 4 additions & 0 deletions internal/appsec/dyngo/instrumentation/grpcsec/grpc.go
Expand Up @@ -15,6 +15,8 @@ import (

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

"google.golang.org/grpc/codes"
)

// Abstract gRPC server handler operation definitions. It is based on two
Expand All @@ -39,12 +41,14 @@ type (
dyngo.Operation
instrumentation.TagsHolder
instrumentation.SecurityEventsHolder
BlockedCode *codes.Code
}
// HandlerOperationArgs is the grpc handler arguments.
HandlerOperationArgs struct {
// Message received by the gRPC handler.
// Corresponds to the address `grpc.server.request.metadata`.
Metadata map[string][]string
ClientIP instrumentation.NetaddrIP
}
// HandlerOperationRes is the grpc handler results. Empty as of today.
HandlerOperationRes struct{}
Expand Down
23 changes: 14 additions & 9 deletions internal/appsec/dyngo/instrumentation/grpcsec/tags.go
Expand Up @@ -17,26 +17,31 @@ import (

// SetSecurityEventTags sets the AppSec-specific span tags when a security event
// occurred into the service entry span.
func SetSecurityEventTags(span ddtrace.Span, events []json.RawMessage, addr net.Addr, md map[string][]string) {
if err := setSecurityEventTags(span, events, addr, md); err != nil {
func SetSecurityEventTags(span ddtrace.Span, events []json.RawMessage, clientIP instrumentation.NetaddrIP, peerAddr net.Addr, md map[string][]string) {
if err := setSecurityEventTags(span, events, clientIP, peerAddr, md); err != nil {
log.Error("appsec: %v", err)
}
}

func setSecurityEventTags(span ddtrace.Span, events []json.RawMessage, addr net.Addr, md map[string][]string) error {
func setSecurityEventTags(span ddtrace.Span, events []json.RawMessage, clientIP instrumentation.NetaddrIP, peerAddr net.Addr, md map[string][]string) error {
if err := instrumentation.SetEventSpanTags(span, events); err != nil {
return err
}
var ip string
switch actual := addr.(type) {
var peerIP string
switch actual := peerAddr.(type) {
case *net.UDPAddr:
ip = actual.IP.String()
peerIP = actual.IP.String()
case *net.TCPAddr:
ip = actual.IP.String()
peerIP = actual.IP.String()
}
if ip != "" {
span.SetTag("network.client.ip", ip)
if peerIP != "" {
span.SetTag("network.client.ip", peerIP)
}

if clientIP.IsValid() {
span.SetTag("http.client_ip", clientIP.String())
}

for h, v := range httpsec.NormalizeHTTPHeaders(md) {
span.SetTag("grpc.metadata."+h, v)
}
Expand Down