Files
higress/plugins/wasm-go/extensions/waf/wasmplugin/plugin.go

344 lines
10 KiB
Go

package wasmplugin
import (
"errors"
"strconv"
"strings"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/corazawaf/coraza/v3"
"github.com/corazawaf/coraza/v3/debuglog"
ctypes "github.com/corazawaf/coraza/v3/types"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
func PluginStart() {
wrapper.SetCtx(
"waf-plugin-go",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
wrapper.ProcessStreamDoneBy(onHttpStreamDone),
)
}
type WafConfig struct {
waf coraza.WAF
//tx ctypes.Transaction
}
func parseConfig(json gjson.Result, config *WafConfig, log wrapper.Log) error {
var secRules []string
var value gjson.Result
value = json.Get("useCRS")
if value.Exists() {
if value.Bool() {
secRules = append(secRules, "Include @demo-conf")
secRules = append(secRules, "Include @crs-setup-demo-conf")
secRules = append(secRules, "Include @owasp_crs/*.conf")
secRules = append(secRules, "SecRuleEngine On")
}
}
value = json.Get("secRules")
if value.Exists() {
for _, item := range json.Get("secRules").Array() {
rule := item.String()
secRules = append(secRules, rule)
}
}
conf := coraza.NewWAFConfig().
WithErrorCallback(logError).
WithDebugLogger(debuglog.DefaultWithPrinterFactory(logPrinterFactory)).
WithRootFS(root)
// error: Failed to load Wasm module due to a missing import: wasi_snapshot_preview1.fd_filestat_get
// because without fs.go
waf, err := coraza.NewWAF(conf.WithDirectives(strings.Join(secRules, "\n")))
config.waf = waf
if err != nil {
log.Errorf("Failed to create waf conf: %v", err)
return errors.New("failed to create waf conf")
}
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config WafConfig, log wrapper.Log) types.Action {
ctx.SetContext("skipwaf", false)
if ignoreBody() {
ctx.DontReadRequestBody()
ctx.DontReadResponseBody()
ctx.SetContext("skipwaf", true)
return types.ActionContinue
}
ctx.SetContext("interruptionHandled", false)
ctx.SetContext("processedRequestBody", false)
ctx.SetContext("processedResponseBody", false)
ctx.SetContext("tx", config.waf.NewTransaction())
tx := ctx.GetContext("tx").(ctypes.Transaction)
protocol, err := proxywasm.GetProperty([]string{"request", "protocol"})
if err != nil {
// TODO(anuraaga): HTTP protocol is commonly required in WAF rules, we should probably
// fail fast here, but proxytest does not support properties yet.
protocol = []byte("HTTP/2.0")
}
ctx.SetContext("httpProtocol", string(protocol))
// Note the pseudo-header :path includes the query.
// See https://httpwg.org/specs/rfc9113.html#rfc.section.8.3.1
uri, err := proxywasm.GetHttpRequestHeader(":path")
if err != nil {
log.Error("Failed to get :path")
return types.ActionContinue
}
// This currently relies on Envoy's behavior of mapping all requests to HTTP/2 semantics
// and its request properties, but they may not be true of other proxies implementing
// proxy-wasm.
if tx.IsRuleEngineOff() {
return types.ActionContinue
}
// OnHttpRequestHeaders does not terminate if IP/Port retrieve goes wrong
srcIP, srcPort := retrieveAddressInfo(log, "source")
dstIP, dstPort := retrieveAddressInfo(log, "destination")
tx.ProcessConnection(srcIP, srcPort, dstIP, dstPort)
method, err := proxywasm.GetHttpRequestHeader(":method")
if err != nil {
log.Error("Failed to get :method")
return types.ActionContinue
}
tx.ProcessURI(uri, method, string(protocol))
hs, err := proxywasm.GetHttpRequestHeaders()
if err != nil {
log.Error("Failed to get request headers")
return types.ActionContinue
}
for _, h := range hs {
tx.AddRequestHeader(h[0], h[1])
}
// CRS rules tend to expect Host even with HTTP/2
authority, err := proxywasm.GetHttpRequestHeader(":authority")
if err == nil {
tx.AddRequestHeader("Host", authority)
tx.SetServerName(parseServerName(log, authority))
}
interruption := tx.ProcessRequestHeaders()
if interruption != nil {
return handleInterruption(ctx, "http_request_headers", interruption, log)
}
return types.ActionContinue
}
func onHttpRequestBody(ctx wrapper.HttpContext, config WafConfig, body []byte, log wrapper.Log) types.Action {
if ctx.GetContext("interruptionHandled").(bool) {
return types.ActionContinue
}
tx := ctx.GetContext("tx").(ctypes.Transaction)
if tx.IsRuleEngineOff() {
return types.ActionContinue
}
// Do not perform any action related to request body data if SecRequestBodyAccess is set to false
if !tx.IsRequestBodyAccessible() {
log.Info("Skipping request body inspection, SecRequestBodyAccess is off.")
// ProcessRequestBody is still performed for phase 2 rules, checking already populated variables
ctx.SetContext("processedRequestBody", true)
interruption, err := tx.ProcessRequestBody()
if err != nil {
log.Error("Failed to process request body")
return types.ActionContinue
}
if interruption != nil {
return handleInterruption(ctx, "http_request_body", interruption, log)
}
return types.ActionContinue
}
interruption, _, err := tx.WriteRequestBody(body)
if err != nil {
log.Error("Failed to write request body")
return types.ActionContinue
}
if interruption != nil {
return handleInterruption(ctx, "http_request_body", interruption, log)
}
ctx.SetContext("processedRequestBody", true)
interruption, err = tx.ProcessRequestBody()
if err != nil {
log.Error("Failed to process request body")
return types.ActionContinue
}
if interruption != nil {
return handleInterruption(ctx, "http_request_body", interruption, log)
}
return types.ActionContinue
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config WafConfig, log wrapper.Log) types.Action {
if ctx.GetContext("skipwaf").(bool) {
return types.ActionContinue
}
if ctx.GetContext("interruptionHandled").(bool) {
return types.ActionContinue
}
tx := ctx.GetContext("tx").(ctypes.Transaction)
if tx.IsRuleEngineOff() {
return types.ActionContinue
}
// Requests without body won't call OnHttpRequestBody, but there are rules in the request body
// phase that still need to be executed. If they haven't been executed yet, now is the time.
if !ctx.GetContext("processedRequestBody").(bool) {
ctx.SetContext("processedRequestBody", true)
interruption, err := tx.ProcessRequestBody()
if err != nil {
log.Error("Failed to process request body")
return types.ActionContinue
}
if interruption != nil {
return handleInterruption(ctx, "http_response_headers", interruption, log)
}
}
status, err := proxywasm.GetHttpResponseHeader(":status")
if err != nil {
log.Error("Failed to get :status")
return types.ActionContinue
}
code, err := strconv.Atoi(status)
if err != nil {
code = 0
}
hs, err := proxywasm.GetHttpResponseHeaders()
if err != nil {
log.Error("Failed to get response headers")
return types.ActionContinue
}
for _, h := range hs {
tx.AddResponseHeader(h[0], h[1])
}
interruption := tx.ProcessResponseHeaders(code, ctx.GetContext("httpProtocol").(string))
if interruption != nil {
return handleInterruption(ctx, "http_response_headers", interruption, log)
}
return types.ActionContinue
}
func onHttpResponseBody(ctx wrapper.HttpContext, config WafConfig, body []byte, log wrapper.Log) types.Action {
if ctx.GetContext("interruptionHandled").(bool) {
// At response body phase, proxy-wasm currently relies on emptying the response body as a way of
// interruption the response. See https://github.com/corazawaf/coraza-proxy-wasm/issues/26.
// If OnHttpResponseBody is called again and an interruption has already been raised, it means that
// we have to keep going with the sanitization of the response, emptying it.
// Sending the crafted HttpResponse with empty body, we don't expect to trigger OnHttpResponseBody
// log.Warn("Response body interruption already handled, keeping replacing the body")
// Interruption happened, we don't want to send response body data
return replaceResponseBodyWhenInterrupted(log, replaceResponseBody)
}
tx := ctx.GetContext("tx").(ctypes.Transaction)
if tx.IsRuleEngineOff() {
return types.ActionContinue
}
// Do not perform any action related to response body data if SecResponseBodyAccess is set to false
if !tx.IsResponseBodyAccessible() {
log.Debug("Skipping response body inspection, SecResponseBodyAccess is off.")
// ProcessResponseBody is performed for phase 4 rules, checking already populated variables
ctx.SetContext("processedResponseBody", true)
interruption, err := tx.ProcessResponseBody()
if err != nil {
log.Error("Failed to process response body")
return types.ActionContinue
}
if interruption != nil {
// Proxy-wasm can not anymore deny the response. The best interruption is emptying the body
// Coraza Multiphase evaluation will help here avoiding late interruptions
return handleInterruption(ctx, "http_response_body", interruption, log)
}
return types.ActionContinue
}
interruption, _, err := tx.WriteResponseBody(body)
if err != nil {
log.Error("Failed to write response body")
return types.ActionContinue
}
if interruption != nil {
return handleInterruption(ctx, "http_response_body", interruption, log)
}
// We have already sent response headers, an unauthorized response can not be sent anymore,
// but we can still drop the response to prevent leaking sensitive content.
// The error will also be logged by Coraza.
ctx.SetContext("processedResponseBody", true)
interruption, err = tx.ProcessResponseBody()
if err != nil {
log.Error("Failed to process response body")
return types.ActionContinue
}
if interruption != nil {
return handleInterruption(ctx, "http_response_body", interruption, log)
}
return types.ActionContinue
}
func onHttpStreamDone(ctx wrapper.HttpContext, config WafConfig, log wrapper.Log) {
if ctx.GetContext("skipwaf").(bool) {
return
}
tx := ctx.GetContext("tx").(ctypes.Transaction)
if !tx.IsRuleEngineOff() {
// Responses without body won't call OnHttpResponseBody, but there are rules in the response body
// phase that still need to be executed. If they haven't been executed yet, now is the time.
if !ctx.GetContext("processedResponseBody").(bool) {
ctx.SetContext("processedResponseBody", true)
_, err := tx.ProcessResponseBody()
if err != nil {
log.Error("Failed to process response body")
}
}
}
tx.ProcessLogging()
_ = tx.Close()
}