Skip to content

Commit

Permalink
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 3, 2022
1 parent 6cd57d7 commit e859163
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 21 deletions.
8 changes: 5 additions & 3 deletions internal/appsec/dyngo/instrumentation/common.go
Expand Up @@ -102,9 +102,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
91 changes: 91 additions & 0 deletions internal/appsec/dyngo/instrumentation/httpsec/actions.go
@@ -0,0 +1,91 @@
// 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"
)

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 *Operation)
}

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: 403,
Template: "html",
},
}
// 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 *Operation) {
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 {
payload := blockedPayload(&p)
op.handler = http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Write(payload)
writer.WriteHeader(p.Status)
})
}
}

// ActionParam is used to identify an action parameters data type
type ActionParam interface{}

// 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 int
// Template is the payload template to use to write the response (html or json)
Template string
}

func blockedPayload(params *BlockRequestParams) []byte {
payload := BlockedTemplateJSON
if params.Template == "html" {
payload = BlockedTemplateHTML
}
return payload
}
31 changes: 30 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 @@ -81,6 +82,10 @@ func WrapHandler(handler http.Handler, span ddtrace.Span, pathParams map[string]
args := MakeHandlerOperationArgs(r, pathParams)
ctx, op := StartOperation(r.Context(), args)
r = r.WithContext(ctx)
if op.handler != nil {
op.handler.ServeHTTP(w, r)
return
}
defer func() {
var status int
if mw, ok := w.(interface{ Status() int }); ok {
Expand All @@ -99,7 +104,6 @@ func WrapHandler(handler http.Handler, span ddtrace.Span, pathParams map[string]
}
SetSecurityEventTags(span, events, remoteIP, args.Headers, w.Header())
}()

handler.ServeHTTP(w, r)
})
}
Expand Down Expand Up @@ -152,6 +156,8 @@ type (
dyngo.Operation
instrumentation.TagsHolder
instrumentation.SecurityEventsHolder
// handler is the alternate HTTP handler that must be called for a given operation, if not nil
handler http.Handler
}

// SDKBodyOperation type representing an SDK body. It must be created with
Expand Down Expand Up @@ -294,3 +300,26 @@ func IPFromRequest(r *http.Request) netaddrIP {

return netaddrIP{}
}

var (
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 = []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
}
}

}
}
43 changes: 26 additions & 17 deletions internal/appsec/waf.go
Expand Up @@ -95,9 +95,29 @@ func registerWAF(rules []byte, timeout time.Duration, limiter Limiter, obfCfg *O
// newWAFEventListener returns the WAF event listener to register in order to enable it.
func newHTTPWAFEventListener(handle *waf.Handle, addresses []string, timeout time.Duration, limiter Limiter) dyngo.EventListener {
var monitorRulesOnce sync.Once // per instantiation
actionHandler := httpsec.NewActionsHandler()

return httpsec.OnHandlerOperationStart(func(op *httpsec.Operation, args httpsec.HandlerOperationArgs) {
var body interface{}
wafCtx := waf.NewContext(handle)
if wafCtx == nil {
// The WAF event listener got concurrently released
return
}

values := map[string]interface{}{}
for _, addr := range addresses {
if addr == httpClientIP && args.ClientIP.IsValid() {
values[httpClientIP] = args.ClientIP.String()
}
}

matches, actionIds := runWAF(wafCtx, values, timeout)
if len(matches) > 0 {
for _, id := range actionIds {
actionHandler.Exec(id, op)
}
}

op.On(httpsec.OnSDKBodyOperationStart(func(op *httpsec.SDKBodyOperation, args httpsec.SDKBodyOperationArgs) {
body = args.Body
Expand All @@ -106,13 +126,7 @@ func newHTTPWAFEventListener(handle *waf.Handle, addresses []string, timeout tim
// At the moment, AppSec doesn't block the requests, and so we can use the fact we are in monitoring-only mode
// to call the WAF only once at the end of the handler operation.
op.On(httpsec.OnHandlerOperationFinish(func(op *httpsec.Operation, res httpsec.HandlerOperationRes) {
wafCtx := waf.NewContext(handle)
if wafCtx == nil {
// The WAF event listener got concurrently released
return
}
defer wafCtx.Close()

// Run the WAF on the rule addresses available in the request args
values := make(map[string]interface{}, len(addresses))
for _, addr := range addresses {
Expand Down Expand Up @@ -141,14 +155,9 @@ func newHTTPWAFEventListener(handle *waf.Handle, addresses []string, timeout tim
}
case serverResponseStatusAddr:
values[serverResponseStatusAddr] = res.Status

case httpClientIP:
if args.ClientIP.IsValid() {
values[httpClientIP] = args.ClientIP.String()
}
}
}
matches := runWAF(wafCtx, values, timeout)
matches, _ := runWAF(wafCtx, values, timeout)

// Add WAF metrics.
rInfo := handle.RulesetInfo()
Expand Down Expand Up @@ -222,7 +231,7 @@ func newGRPCWAFEventListener(handle *waf.Handle, _ []string, timeout time.Durati
if md := handlerArgs.Metadata; len(md) > 0 {
values[grpcServerRequestMetadata] = md
}
event := runWAF(wafCtx, values, timeout)
event, _ := runWAF(wafCtx, values, timeout)

// WAF run durations are WAF context bound. As of now we need to keep track of those externally since
// we use a new WAF context for each callback. When we are able to re-use the same WAF context across
Expand Down Expand Up @@ -260,17 +269,17 @@ func newGRPCWAFEventListener(handle *waf.Handle, _ []string, timeout time.Durati
})
}

func runWAF(wafCtx *waf.Context, values map[string]interface{}, timeout time.Duration) []byte {
matches, _, err := wafCtx.Run(values, timeout)
func runWAF(wafCtx *waf.Context, values map[string]interface{}, timeout time.Duration) ([]byte, []string) {
matches, actions, err := wafCtx.Run(values, timeout)
if err != nil {
if err == waf.ErrTimeout {
log.Debug("appsec: waf timeout value of %s reached", timeout)
} else {
log.Error("appsec: unexpected waf error: %v", err)
return nil
return nil, nil
}
}
return matches
return matches, actions
}

// HTTP rule addresses currently supported by the WAF
Expand Down

0 comments on commit e859163

Please sign in to comment.