Skip to content

Commit

Permalink
internal/appsec: internal/appsec: add support for WAF actions
Browse files Browse the repository at this point in the history
ASM now instanciates an action handler that will perform various
actions commanded by the WAF after a match is performed. The
"block_request" action type is the only kind of action currently
supported, allowing to block an HTTP request.
  • Loading branch information
Hellzy committed Nov 10, 2022
1 parent c1c1364 commit 566a6c5
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 18 deletions.
6 changes: 6 additions & 0 deletions contrib/google.golang.org/grpc/appsec.go
Expand Up @@ -25,6 +25,9 @@ func appsecUnaryHandlerMiddleware(span ddtrace.Span, handler grpc.UnaryHandler)
return func(ctx context.Context, req interface{}) (interface{}, error) {
md, _ := metadata.FromIncomingContext(ctx)
op := grpcsec.StartHandlerOperation(grpcsec.HandlerOperationArgs{Metadata: md}, nil)
if op.UnaryHandler != nil {
return op.UnaryHandler(ctx, req)
}
defer func() {
events := op.Finish(grpcsec.HandlerOperationRes{})
instrumentation.SetTags(span, op.Tags())
Expand All @@ -44,6 +47,9 @@ func appsecStreamHandlerMiddleware(span ddtrace.Span, handler grpc.StreamHandler
return func(srv interface{}, stream grpc.ServerStream) error {
md, _ := metadata.FromIncomingContext(stream.Context())
op := grpcsec.StartHandlerOperation(grpcsec.HandlerOperationArgs{Metadata: md}, nil)
if op.StreamHandler != nil {
return op.StreamHandler(srv, stream)
}
defer func() {
events := op.Finish(grpcsec.HandlerOperationRes{})
instrumentation.SetTags(span, op.Tags())
Expand Down
85 changes: 85 additions & 0 deletions internal/appsec/dyngo/instrumentation/grpcsec/actions.go
@@ -0,0 +1,85 @@
// 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 (
"context"

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

type ActionParam interface{}

type action struct {
id string
params ActionParam
}

// ActionHandler handles WAF actions registration and execution
type ActionHandler interface {
RegisterAction(id string, params ActionParam)
Exec(id string, op *HandlerOperation)
}

type actionsHandler struct {
actions map[string]action
}

// NewActionsHandler returns an action handler holding the default ASM actions.
// Currently, only the default "block" action is supported
func NewActionsHandler() ActionHandler {
defaultBlockAction := action{
id: "block",
params: BlockRequestParams{
Status: codes.Aborted,
},
}
// Register the default "block" action as specified in the RFC for HTTP blocking
actions := map[string]action{defaultBlockAction.id: defaultBlockAction}

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, params ActionParam) {
h.actions[id] = action{
id: id,
params: params,
}
}

// Exec executes the action identified by `id`
func (h *actionsHandler) Exec(id string, op *HandlerOperation) {
a, ok := h.actions[id]
if !ok {
return
}
// Currently, only the "block_request" type is supported, so we only need to check for blockRequestParams
if p, ok := a.params.(BlockRequestParams); ok {
err := status.Error(p.Status, "Request blocked")
op.UnaryHandler = func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, err
}
op.StreamHandler = func(srv interface{}, stream grpc.ServerStream) error {
return err
}
}
}

// BlockRequestParams is the parameter struct used to perform actions of kind ActionBlockRequest
type BlockRequestParams struct {
ActionParam
// Status is the return code to use when blocking the request
Status codes.Code
}
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"
)

// Abstract gRPC server handler operation definitions. It is based on two
Expand All @@ -39,6 +41,8 @@ type (
dyngo.Operation
instrumentation.TagsHolder
instrumentation.SecurityEventsHolder
UnaryHandler grpc.UnaryHandler
StreamHandler grpc.StreamHandler
}
// HandlerOperationArgs is the grpc handler arguments.
HandlerOperationArgs struct {
Expand Down
84 changes: 84 additions & 0 deletions internal/appsec/dyngo/instrumentation/httpsec/actions.go
@@ -0,0 +1,84 @@
// 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 httpsec

import (
"net/http"
)

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

// BlockRequestAction is the parameter struct used to perform actions of kind ActionBlockRequest
type BlockRequestAction struct {
Action
// Status is the return code to use when blocking the request
Status int
// Template is the payload template to use to write the response (html or json)
Template string
// handler is the http handler be used to block the request (see wrap())
handler http.Handler
}

func (*BlockRequestAction) isAction() {}

func blockedPayload(a *BlockRequestAction) []byte {
payload := BlockedTemplateJSON
if a.Template == "html" {
payload = BlockedTemplateHTML
}
return payload
}

// ActionsHandler handles actions registration and their application to operations
type ActionsHandler struct {
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 {
handler := ActionsHandler{
actions: map[string]Action{},
}
// Register the default "block" action as specified in the RFC for HTTP blocking
handler.RegisterAction("block", &BlockRequestAction{
Status: 403,
Template: "html",
})

return &handler
}

// RegisterAction registers a specific action to the handler. If the action kind is unknown
// the action will not be registered
func (h *ActionsHandler) RegisterAction(id string, action Action) {
switch a := action.(type) {
case *BlockRequestAction:
payload := blockedPayload(a)
a.handler = http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Write(payload)
writer.WriteHeader(a.Status)
})
h.actions[id] = a
default:
break
}
}

// Apply applies the action identified by `id` for the given operation
func (h *ActionsHandler) Apply(id string, op *Operation) {
a, ok := h.actions[id]
if !ok {
return
}
op.actions = append(op.actions, a)
}
51 changes: 50 additions & 1 deletion internal/appsec/dyngo/instrumentation/httpsec/http.go
Expand Up @@ -15,6 +15,7 @@ import (
"encoding/json"
"net"
"net/http"
"os"
"reflect"
"strings"

Expand Down Expand Up @@ -74,25 +75,42 @@ func MonitorParsedBody(ctx context.Context, body interface{}) {
// HandlerOperationRes.
func WrapHandler(handler http.Handler, span ddtrace.Span, pathParams map[string]string) http.Handler {
instrumentation.SetAppSecEnabledTags(span)
applyActions := func(op *Operation) {
for _, action := range op.Actions() {
switch a := action.(type) {
case *BlockRequestAction:
handler = a.handler
op.AddTag("appsec.blocked", true)
default:
log.Warn("appsec: unsupported action %v. Ignoring.", a)
}
}
}

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
SetIPTags(span, r)

args := MakeHandlerOperationArgs(r, pathParams)
ctx, op := StartOperation(r.Context(), args)
r = r.WithContext(ctx)

events := op.Events()
if len(events) > 0 {
applyActions(op)
}
defer func() {
var status int
if mw, ok := w.(interface{ Status() int }); ok {
status = mw.Status()
}

events := op.Finish(HandlerOperationRes{Status: status})
events = append(events, op.Finish(HandlerOperationRes{Status: status})...)
instrumentation.SetTags(span, op.Tags())
if len(events) == 0 {
return
}

applyActions(op)
remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
remoteIP = r.RemoteAddr
Expand All @@ -101,6 +119,7 @@ func WrapHandler(handler http.Handler, span ddtrace.Span, pathParams map[string]
}()

handler.ServeHTTP(w, r)

})
}

Expand Down Expand Up @@ -152,6 +171,7 @@ type (
dyngo.Operation
instrumentation.TagsHolder
instrumentation.SecurityEventsHolder
actions []Action
}

// SDKBodyOperation type representing an SDK body. It must be created with
Expand Down Expand Up @@ -190,6 +210,10 @@ func (op *Operation) Finish(res HandlerOperationRes) []json.RawMessage {
return op.Events()
}

func (op *Operation) Actions() []Action {
return op.actions
}

// StartSDKBodyOperation starts the SDKBody operation and emits a start event
func StartSDKBodyOperation(parent *Operation, args SDKBodyOperationArgs) *SDKBodyOperation {
op := &SDKBodyOperation{Operation: dyngo.NewOperation(parent)}
Expand Down Expand Up @@ -294,3 +318,28 @@ func IPFromRequest(r *http.Request) netaddrIP {

return netaddrIP{}
}

var (
// BlockedTemplateJSON is the default JSON template used to write responses for blocked requests
BlockedTemplateJSON = []byte(`{"errors": [{"title": "You've been blocked", "detail": "Sorry, you cannot access this page. Please contact the customer service team. Security provided by Datadog."}]}`)
// BlockedTemplateHTML is the default HTML template used to write responses for blocked requests
BlockedTemplateHTML = []byte(`<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>You've been blocked</title> <style>a, body, div, html, span{margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline}body{background: -webkit-radial-gradient(26% 19%, circle, #fff, #f4f7f9); background: radial-gradient(circle at 26% 19%, #fff, #f4f7f9); display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -ms-flex-line-pack: center; align-content: center; width: 100%; min-height: 100vh; line-height: 1; flex-direction: column}p{display: block}main{text-align: center; flex: 1; display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -ms-flex-line-pack: center; align-content: center; flex-direction: column}p{font-size: 18px; line-height: normal; color: #646464; font-family: sans-serif; font-weight: 400}a{color: #4842b7}footer{width: 100%; text-align: center}footer p{font-size: 16px}</style></head><body> <main> <p>Sorry, you cannot access this page. Please contact the customer service team.</p></main> <footer> <p>Security provided by <a href="https://www.datadoghq.com/product/security-platform/application-security-monitoring/" target="_blank">Datadog</a></p></footer></body></html>`)
)

const (
envBlockedTemplateHTML = "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML"
envBlockedTemplateJSON = "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON"
)

func init() {
for env, template := range map[string]*[]byte{envBlockedTemplateJSON: &BlockedTemplateJSON, envBlockedTemplateHTML: &BlockedTemplateHTML} {
if path, ok := os.LookupEnv(env); ok {
if t, err := os.ReadFile(path); err != nil {
log.Warn("Could not read template at %s: %v", path, err)
} else {
*template = t
}
}

}
}

0 comments on commit 566a6c5

Please sign in to comment.