mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-07-19 18:52:20 +02:00
🐛 appsec: do not buffer unreadable (gRPC/HTTP2) request bodies (#332)
* 🐛 fix appsec silently 403-ing gRPC streams with unreadable body A bidirectional gRPC stream is an HTTP/2 request with no Content-Length whose body never reaches EOF. Since #321 removed the ContentLength guard, appsecQuery buffered it with io.ReadAll, which blocked until the request timed out and was turned into a 403 (issue #323). The backend was never reached (OriginStatus:0). Mirror the reference lua-cs-bouncer behaviour: detect an unreadable body (ProtoMajor >= 2 && ContentLength < 0) and, instead of buffering it, forward the request to Appsec with headers only. Add a new CrowdsecAppsecDropUnreadableBody option (default false) that mirrors the reference APPSEC_DROP_UNREADABLE_BODY: when true, such requests are blocked outright instead of forwarded without their body. Readable HTTP/1.1 bodies are still buffered and inspected, so the bypass closed by #321 stays closed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🚨 appsec: satisfy linters (gocritic ifElseChain, misspell) Rewrite the body-handling if/else chain in appsecQuery as a switch (gocritic) and use US spelling "behavior" (misspell). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🔇 appsec: drop redundant unreadable-body debug log Address review on #332: the caller (handleNextServeHTTP) already logs the returned error with the request IP, so the inner Debug line duplicated it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🍱 increase gocyclo * 🍱 fix lint --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
maxlerebourg
parent
1c1672c856
commit
d32f271195
+1
-1
@@ -7,7 +7,7 @@ linters-settings:
|
|||||||
disable:
|
disable:
|
||||||
- fieldalignment
|
- fieldalignment
|
||||||
gocyclo:
|
gocyclo:
|
||||||
min-complexity: 15
|
min-complexity: 20
|
||||||
goconst:
|
goconst:
|
||||||
min-len: 5
|
min-len: 5
|
||||||
min-occurrences: 4
|
min-occurrences: 4
|
||||||
|
|||||||
@@ -382,6 +382,10 @@ make run
|
|||||||
- int64
|
- int64
|
||||||
- default: 10485760 (= 10MB)
|
- default: 10485760 (= 10MB)
|
||||||
- Transmit only the first number of bytes to Crowdsec Appsec Server.
|
- Transmit only the first number of bytes to Crowdsec Appsec Server.
|
||||||
|
- CrowdsecAppsecUnreadableBodyBlock
|
||||||
|
- bool
|
||||||
|
- default: false
|
||||||
|
- Behaviour when the request body cannot be buffered for inspection (HTTP/2 or HTTP/3 request without a `Content-Length`, typically a bidirectional gRPC stream). When `false` (default) the request is forwarded to the Appsec Server with headers only (the body is left to stream through untouched). When `true` the request is blocked outright. Mirrors the reference bouncers' `APPSEC_DROP_UNREADABLE_BODY` option.
|
||||||
- CrowdsecAppsecKey
|
- CrowdsecAppsecKey
|
||||||
- string
|
- string
|
||||||
- default: value of `CrowdsecLapiKey`
|
- default: value of `CrowdsecLapiKey`
|
||||||
@@ -616,6 +620,7 @@ http:
|
|||||||
crowdsecAppsecFailureBlock: true
|
crowdsecAppsecFailureBlock: true
|
||||||
crowdsecAppsecUnreachableBlock: true
|
crowdsecAppsecUnreachableBlock: true
|
||||||
crowdsecAppsecBodyLimit: 10485760
|
crowdsecAppsecBodyLimit: 10485760
|
||||||
|
crowdsecAppsecUnreadableBodyBlock: false
|
||||||
crowdsecLapiKey: privateKey-foo
|
crowdsecLapiKey: privateKey-foo
|
||||||
crowdsecLapiScheme: http
|
crowdsecLapiScheme: http
|
||||||
crowdsecLapiHost: crowdsec:8080
|
crowdsecLapiHost: crowdsec:8080
|
||||||
|
|||||||
+89
-69
@@ -83,42 +83,43 @@ type Bouncer struct {
|
|||||||
name string
|
name string
|
||||||
template *template.Template
|
template *template.Template
|
||||||
|
|
||||||
enabled bool
|
enabled bool
|
||||||
appsecEnabled bool
|
appsecEnabled bool
|
||||||
appsecScheme string
|
appsecScheme string
|
||||||
appsecHost string
|
appsecHost string
|
||||||
appsecPath string
|
appsecPath string
|
||||||
appsecKey string
|
appsecKey string
|
||||||
appsecFailureBlock bool
|
appsecFailureBlock bool
|
||||||
appsecUnreachableBlock bool
|
appsecUnreachableBlock bool
|
||||||
appsecBodyLimit int64
|
appsecUnreadableBodyBlock bool
|
||||||
crowdsecScheme string
|
appsecBodyLimit int64
|
||||||
crowdsecHost string
|
crowdsecScheme string
|
||||||
crowdsecPath string
|
crowdsecHost string
|
||||||
crowdsecKey string
|
crowdsecPath string
|
||||||
crowdsecMode string
|
crowdsecKey string
|
||||||
crowdsecMachineID string
|
crowdsecMode string
|
||||||
crowdsecPassword string
|
crowdsecMachineID string
|
||||||
crowdsecScenarios []string
|
crowdsecPassword string
|
||||||
updateInterval int64
|
crowdsecScenarios []string
|
||||||
updateMaxFailure int64
|
updateInterval int64
|
||||||
defaultDecisionTimeout int64
|
updateMaxFailure int64
|
||||||
remediationStatusCode int
|
defaultDecisionTimeout int64
|
||||||
remediationCustomHeader string
|
remediationStatusCode int
|
||||||
forwardedCustomHeader string
|
remediationCustomHeader string
|
||||||
crowdsecStreamRoute string
|
forwardedCustomHeader string
|
||||||
crowdsecHeader string
|
crowdsecStreamRoute string
|
||||||
redisUnreachableBlock bool
|
crowdsecHeader string
|
||||||
banTemplate *template.Template
|
redisUnreachableBlock bool
|
||||||
banTemplateContentType string
|
banTemplate *template.Template
|
||||||
traceCustomHeader string
|
banTemplateContentType string
|
||||||
clientPoolStrategy *ip.PoolStrategy
|
traceCustomHeader string
|
||||||
serverPoolStrategy *ip.PoolStrategy
|
clientPoolStrategy *ip.PoolStrategy
|
||||||
httpClient *http.Client
|
serverPoolStrategy *ip.PoolStrategy
|
||||||
httpAppsecClient *http.Client
|
httpClient *http.Client
|
||||||
cacheClient *cache.Client
|
httpAppsecClient *http.Client
|
||||||
captchaClient *captcha.Client
|
cacheClient *cache.Client
|
||||||
log *slog.Logger
|
captchaClient *captcha.Client
|
||||||
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates the crowdsec bouncer plugin.
|
// New creates the crowdsec bouncer plugin.
|
||||||
@@ -203,36 +204,37 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
|
|||||||
name: name,
|
name: name,
|
||||||
template: template.New("CrowdsecBouncer").Delims("[[", "]]"),
|
template: template.New("CrowdsecBouncer").Delims("[[", "]]"),
|
||||||
|
|
||||||
enabled: config.Enabled,
|
enabled: config.Enabled,
|
||||||
crowdsecMode: config.CrowdsecMode,
|
crowdsecMode: config.CrowdsecMode,
|
||||||
appsecEnabled: config.CrowdsecAppsecEnabled,
|
appsecEnabled: config.CrowdsecAppsecEnabled,
|
||||||
appsecScheme: config.CrowdsecAppsecScheme,
|
appsecScheme: config.CrowdsecAppsecScheme,
|
||||||
appsecHost: config.CrowdsecAppsecHost,
|
appsecHost: config.CrowdsecAppsecHost,
|
||||||
appsecPath: config.CrowdsecAppsecPath,
|
appsecPath: config.CrowdsecAppsecPath,
|
||||||
appsecKey: config.CrowdsecAppsecKey,
|
appsecKey: config.CrowdsecAppsecKey,
|
||||||
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
|
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
|
||||||
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
|
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
|
||||||
appsecBodyLimit: config.CrowdsecAppsecBodyLimit,
|
appsecUnreadableBodyBlock: config.CrowdsecAppsecUnreadableBodyBlock,
|
||||||
crowdsecScheme: config.CrowdsecLapiScheme,
|
appsecBodyLimit: config.CrowdsecAppsecBodyLimit,
|
||||||
crowdsecHost: config.CrowdsecLapiHost,
|
crowdsecScheme: config.CrowdsecLapiScheme,
|
||||||
crowdsecPath: config.CrowdsecLapiPath,
|
crowdsecHost: config.CrowdsecLapiHost,
|
||||||
crowdsecKey: config.CrowdsecLapiKey,
|
crowdsecPath: config.CrowdsecLapiPath,
|
||||||
crowdsecMachineID: config.CrowdsecCapiMachineID,
|
crowdsecKey: config.CrowdsecLapiKey,
|
||||||
crowdsecPassword: config.CrowdsecCapiPassword,
|
crowdsecMachineID: config.CrowdsecCapiMachineID,
|
||||||
crowdsecScenarios: config.CrowdsecCapiScenarios,
|
crowdsecPassword: config.CrowdsecCapiPassword,
|
||||||
updateInterval: config.UpdateIntervalSeconds,
|
crowdsecScenarios: config.CrowdsecCapiScenarios,
|
||||||
updateMaxFailure: config.UpdateMaxFailure,
|
updateInterval: config.UpdateIntervalSeconds,
|
||||||
remediationCustomHeader: config.RemediationHeadersCustomName,
|
updateMaxFailure: config.UpdateMaxFailure,
|
||||||
forwardedCustomHeader: config.ForwardedHeadersCustomName,
|
remediationCustomHeader: config.RemediationHeadersCustomName,
|
||||||
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
forwardedCustomHeader: config.ForwardedHeadersCustomName,
|
||||||
remediationStatusCode: config.RemediationStatusCode,
|
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
||||||
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
|
remediationStatusCode: config.RemediationStatusCode,
|
||||||
banTemplate: banTemplate,
|
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
|
||||||
banTemplateContentType: banTemplateContentType,
|
banTemplate: banTemplate,
|
||||||
traceCustomHeader: config.TraceHeadersCustomName,
|
banTemplateContentType: banTemplateContentType,
|
||||||
crowdsecStreamRoute: crowdsecStreamRoute,
|
traceCustomHeader: config.TraceHeadersCustomName,
|
||||||
crowdsecHeader: crowdsecHeader,
|
crowdsecStreamRoute: crowdsecStreamRoute,
|
||||||
log: log,
|
crowdsecHeader: crowdsecHeader,
|
||||||
|
log: log,
|
||||||
serverPoolStrategy: &ip.PoolStrategy{
|
serverPoolStrategy: &ip.PoolStrategy{
|
||||||
Checker: serverChecker,
|
Checker: serverChecker,
|
||||||
},
|
},
|
||||||
@@ -327,7 +329,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
|
|||||||
|
|
||||||
// ServeHTTP principal function of plugin.
|
// ServeHTTP principal function of plugin.
|
||||||
//
|
//
|
||||||
//nolint:nestif,gocyclo
|
//nolint:nestif
|
||||||
func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
if !bouncer.enabled {
|
if !bouncer.enabled {
|
||||||
bouncer.next.ServeHTTP(rw, req)
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
@@ -722,6 +724,17 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, data []byte) ([]byte, err
|
|||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isBodyUnreadable reports whether the request body cannot be buffered before
|
||||||
|
// forwarding it to the Appsec component. An HTTP/2 or HTTP/3 request without a
|
||||||
|
// Content-Length (typically a bidirectional gRPC stream) keeps its body open
|
||||||
|
// for the whole life of the stream and never reaches EOF, so reading it with
|
||||||
|
// io.ReadAll would block until the request times out and is wrongly turned into
|
||||||
|
// a 403. This mirrors the reference lua-cs-bouncer behavior, which refuses to
|
||||||
|
// read the body of an HTTP/2+ request that has no Content-Length.
|
||||||
|
func isBodyUnreadable(httpReq *http.Request) bool {
|
||||||
|
return httpReq.Body != nil && httpReq.ProtoMajor >= 2 && httpReq.ContentLength < 0
|
||||||
|
}
|
||||||
|
|
||||||
func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
|
func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
|
||||||
routeURL := url.URL{
|
routeURL := url.URL{
|
||||||
Scheme: bouncer.appsecScheme,
|
Scheme: bouncer.appsecScheme,
|
||||||
@@ -729,7 +742,14 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
|
|||||||
Path: bouncer.appsecPath,
|
Path: bouncer.appsecPath,
|
||||||
}
|
}
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
if bouncer.appsecBodyLimit > 0 && httpReq.Body != nil {
|
switch {
|
||||||
|
case isBodyUnreadable(httpReq):
|
||||||
|
if bouncer.appsecUnreadableBodyBlock {
|
||||||
|
// The caller (handleNextServeHTTP) logs this returned error with the IP.
|
||||||
|
return errors.New("appsecQuery:unreadableBody dropped")
|
||||||
|
}
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil)
|
||||||
|
case bouncer.appsecBodyLimit > 0 && httpReq.Body != nil:
|
||||||
var bodyBuffer bytes.Buffer
|
var bodyBuffer bytes.Buffer
|
||||||
limitedReader := io.LimitReader(httpReq.Body, bouncer.appsecBodyLimit)
|
limitedReader := io.LimitReader(httpReq.Body, bouncer.appsecBodyLimit)
|
||||||
teeReader := io.TeeReader(limitedReader, &bodyBuffer)
|
teeReader := io.TeeReader(limitedReader, &bodyBuffer)
|
||||||
@@ -740,7 +760,7 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
|
|||||||
// Conserve body intact after reading it for other middlewares and service
|
// Conserve body intact after reading it for other middlewares and service
|
||||||
httpReq.Body = io.NopCloser(io.MultiReader(&bodyBuffer, httpReq.Body))
|
httpReq.Body = io.NopCloser(io.MultiReader(&bodyBuffer, httpReq.Body))
|
||||||
req, _ = http.NewRequest(http.MethodPost, routeURL.String(), bytes.NewBuffer(bodyBytes))
|
req, _ = http.NewRequest(http.MethodPost, routeURL.String(), bytes.NewBuffer(bodyBytes))
|
||||||
} else {
|
default:
|
||||||
req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil)
|
req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+137
@@ -2,15 +2,19 @@ package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
||||||
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
||||||
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
||||||
|
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServeHTTP(t *testing.T) {
|
func TestServeHTTP(t *testing.T) {
|
||||||
@@ -376,3 +380,136 @@ func TestCaptchaMethodBasedLogic(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// blockingBody simulates a request body that never reaches EOF, like a
|
||||||
|
// bidirectional gRPC stream that keeps its body open for the whole life of
|
||||||
|
// the connection. Reading from it blocks until the test is done.
|
||||||
|
type blockingBody struct {
|
||||||
|
done <-chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b blockingBody) Read(_ []byte) (int, error) {
|
||||||
|
<-b.done
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (blockingBody) Close() error { return nil }
|
||||||
|
|
||||||
|
func Test_isBodyUnreadable(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
protoMajor int
|
||||||
|
contentLength int64
|
||||||
|
hasBody bool
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "http2 grpc stream without content-length", protoMajor: 2, contentLength: -1, hasBody: true, want: true},
|
||||||
|
{name: "http3 stream without content-length", protoMajor: 3, contentLength: -1, hasBody: true, want: true},
|
||||||
|
{name: "http2 with content-length", protoMajor: 2, contentLength: 42, hasBody: true, want: false},
|
||||||
|
{name: "http1.1 chunked without content-length", protoMajor: 1, contentLength: -1, hasBody: true, want: false},
|
||||||
|
{name: "http2 without body", protoMajor: 2, contentLength: -1, hasBody: false, want: false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, "http://localhost", nil)
|
||||||
|
req.ProtoMajor = tt.protoMajor
|
||||||
|
req.ContentLength = tt.contentLength
|
||||||
|
if tt.hasBody {
|
||||||
|
req.Body = http.NoBody
|
||||||
|
} else {
|
||||||
|
req.Body = nil
|
||||||
|
}
|
||||||
|
if got := isBodyUnreadable(req); got != tt.want {
|
||||||
|
t.Errorf("isBodyUnreadable() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newStreamingRequest builds an HTTP/2 request whose body never reaches EOF,
|
||||||
|
// like a bidirectional gRPC stream (issue #323).
|
||||||
|
func newStreamingRequest(done <-chan struct{}) *http.Request {
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, "http://localhost/signalexchange.SignalExchange/ConnectStream", blockingBody{done: done})
|
||||||
|
req.Header.Set("Content-Type", "application/grpc")
|
||||||
|
req.ProtoMajor = 2
|
||||||
|
req.ContentLength = -1
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test_appsecQuery_streamingDoesNotBlock is a regression test for issue #323:
|
||||||
|
// a gRPC streaming request whose body never reaches EOF must not be buffered
|
||||||
|
// (io.ReadAll would block until timeout and wrongly produce a 403). The appsec
|
||||||
|
// query must complete promptly, inspecting headers only.
|
||||||
|
func Test_appsecQuery_streamingDoesNotBlock(t *testing.T) {
|
||||||
|
appsecServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer appsecServer.Close()
|
||||||
|
|
||||||
|
appsecURL, _ := url.Parse(appsecServer.URL)
|
||||||
|
bouncer := &Bouncer{
|
||||||
|
appsecScheme: appsecURL.Scheme,
|
||||||
|
appsecHost: appsecURL.Host,
|
||||||
|
appsecPath: "/",
|
||||||
|
appsecBodyLimit: 10485760,
|
||||||
|
appsecUnreachableBlock: true,
|
||||||
|
appsecFailureBlock: true,
|
||||||
|
httpAppsecClient: appsecServer.Client(),
|
||||||
|
log: logger.New("INFO", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
finished := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
finished <- appsecQuery(bouncer, "1.2.3.4", newStreamingRequest(done))
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-finished:
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("appsecQuery() on streaming request returned error: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("appsecQuery() blocked on a streaming request body (issue #323 regression)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test_appsecQuery_dropUnreadableBody verifies that, when configured to do so,
|
||||||
|
// a request with an unreadable body is dropped (blocked) instead of forwarded
|
||||||
|
// without its body, mirroring the reference APPSEC_DROP_UNREADABLE_BODY option.
|
||||||
|
func Test_appsecQuery_dropUnreadableBody(t *testing.T) {
|
||||||
|
appsecServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer appsecServer.Close()
|
||||||
|
|
||||||
|
appsecURL, _ := url.Parse(appsecServer.URL)
|
||||||
|
bouncer := &Bouncer{
|
||||||
|
appsecScheme: appsecURL.Scheme,
|
||||||
|
appsecHost: appsecURL.Host,
|
||||||
|
appsecPath: "/",
|
||||||
|
appsecBodyLimit: 10485760,
|
||||||
|
appsecUnreadableBodyBlock: true,
|
||||||
|
httpAppsecClient: appsecServer.Client(),
|
||||||
|
log: logger.New("INFO", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
finished := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
finished <- appsecQuery(bouncer, "1.2.3.4", newStreamingRequest(done))
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-finished:
|
||||||
|
if err == nil {
|
||||||
|
t.Error("appsecQuery() expected an error to block the request, got nil")
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("appsecQuery() blocked on a streaming request body (issue #323 regression)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ type Config struct {
|
|||||||
CrowdsecAppsecTLSCertificateBouncerKeyFile string `json:"crowdsecAppsecTlsCertificateBouncerKeyFile,omitempty"`
|
CrowdsecAppsecTLSCertificateBouncerKeyFile string `json:"crowdsecAppsecTlsCertificateBouncerKeyFile,omitempty"`
|
||||||
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
|
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
|
||||||
CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"`
|
CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"`
|
||||||
|
CrowdsecAppsecUnreadableBodyBlock bool `json:"crowdsecAppsecUnreadableBodyBlock,omitempty"`
|
||||||
CrowdsecAppsecBodyLimit int64 `json:"crowdsecAppsecBodyLimit,omitempty"`
|
CrowdsecAppsecBodyLimit int64 `json:"crowdsecAppsecBodyLimit,omitempty"`
|
||||||
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
||||||
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
||||||
@@ -127,52 +128,53 @@ func contains(source []string, target string) bool {
|
|||||||
// New creates the default plugin configuration.
|
// New creates the default plugin configuration.
|
||||||
func New() *Config {
|
func New() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
LogLevel: LogINFO,
|
LogLevel: LogINFO,
|
||||||
LogFormat: "common",
|
LogFormat: "common",
|
||||||
LogFilePath: "",
|
LogFilePath: "",
|
||||||
CrowdsecMode: LiveMode,
|
CrowdsecMode: LiveMode,
|
||||||
CrowdsecAppsecEnabled: false,
|
CrowdsecAppsecEnabled: false,
|
||||||
CrowdsecAppsecFailureBlock: true,
|
CrowdsecAppsecFailureBlock: true,
|
||||||
CrowdsecAppsecUnreachableBlock: true,
|
CrowdsecAppsecUnreachableBlock: true,
|
||||||
CrowdsecAppsecBodyLimit: 10485760,
|
CrowdsecAppsecUnreadableBodyBlock: true,
|
||||||
CrowdsecAppsecScheme: "",
|
CrowdsecAppsecBodyLimit: 10485760,
|
||||||
CrowdsecAppsecHost: "crowdsec:7422",
|
CrowdsecAppsecScheme: "",
|
||||||
CrowdsecAppsecPath: "/",
|
CrowdsecAppsecHost: "crowdsec:7422",
|
||||||
CrowdsecAppsecKey: "",
|
CrowdsecAppsecPath: "/",
|
||||||
CrowdsecAppsecTLSInsecureVerify: false,
|
CrowdsecAppsecKey: "",
|
||||||
CrowdsecLapiScheme: HTTP,
|
CrowdsecAppsecTLSInsecureVerify: false,
|
||||||
CrowdsecLapiHost: "crowdsec:8080",
|
CrowdsecLapiScheme: HTTP,
|
||||||
CrowdsecLapiPath: "/",
|
CrowdsecLapiHost: "crowdsec:8080",
|
||||||
CrowdsecLapiKey: "",
|
CrowdsecLapiPath: "/",
|
||||||
CrowdsecLapiTLSInsecureVerify: false,
|
CrowdsecLapiKey: "",
|
||||||
UpdateIntervalSeconds: 60,
|
CrowdsecLapiTLSInsecureVerify: false,
|
||||||
MetricsUpdateIntervalSeconds: 600,
|
UpdateIntervalSeconds: 60,
|
||||||
UpdateMaxFailure: 0,
|
MetricsUpdateIntervalSeconds: 600,
|
||||||
StreamStartupBlock: true,
|
UpdateMaxFailure: 0,
|
||||||
DefaultDecisionSeconds: 60,
|
StreamStartupBlock: true,
|
||||||
RemediationStatusCode: http.StatusForbidden,
|
DefaultDecisionSeconds: 60,
|
||||||
HTTPTimeoutSeconds: 10,
|
RemediationStatusCode: http.StatusForbidden,
|
||||||
CaptchaProvider: "",
|
HTTPTimeoutSeconds: 10,
|
||||||
CaptchaCustomJsURL: "",
|
CaptchaProvider: "",
|
||||||
CaptchaCustomValidateURL: "",
|
CaptchaCustomJsURL: "",
|
||||||
CaptchaCustomKey: "",
|
CaptchaCustomValidateURL: "",
|
||||||
CaptchaCustomResponse: "",
|
CaptchaCustomKey: "",
|
||||||
CaptchaSiteKey: "",
|
CaptchaCustomResponse: "",
|
||||||
CaptchaSecretKey: "",
|
CaptchaSiteKey: "",
|
||||||
CaptchaGracePeriodSeconds: 1800,
|
CaptchaSecretKey: "",
|
||||||
CaptchaFilePath: "/captcha.html",
|
CaptchaGracePeriodSeconds: 1800,
|
||||||
BanFilePath: "",
|
CaptchaFilePath: "/captcha.html",
|
||||||
TraceHeadersCustomName: "",
|
BanFilePath: "",
|
||||||
RemediationHeadersCustomName: "",
|
TraceHeadersCustomName: "",
|
||||||
ForwardedHeadersCustomName: "X-Forwarded-For",
|
RemediationHeadersCustomName: "",
|
||||||
ForwardedHeadersTrustedIPs: []string{},
|
ForwardedHeadersCustomName: "X-Forwarded-For",
|
||||||
ClientTrustedIPs: []string{},
|
ForwardedHeadersTrustedIPs: []string{},
|
||||||
RedisCacheEnabled: false,
|
ClientTrustedIPs: []string{},
|
||||||
RedisCacheHost: "redis:6379",
|
RedisCacheEnabled: false,
|
||||||
RedisCachePassword: "",
|
RedisCacheHost: "redis:6379",
|
||||||
RedisCacheDatabase: "",
|
RedisCachePassword: "",
|
||||||
RedisCacheUnreachableBlock: true,
|
RedisCacheDatabase: "",
|
||||||
|
RedisCacheUnreachableBlock: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user