mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 04:28:50 +02:00
🐛 Do not consider body unreadable when it's http.NoBody (#352)
* Do not consider body unreadable when it's http.NoBody Fix #351 * 🐛 appsec: only drop unreadable bodies for methods that carry one Gate the crowdsecAppsecUnreadableBodyBlock drop on the request method, mirroring the reference lua-cs-bouncer METHODS_WITH_BODY list (POST/PUT/PATCH/DELETE). Browser GETs over HTTP/3 never carry a Content-Length (quic-go always wraps the stream in a non-nil body with ContentLength -1), so they were all wrongly dropped; they are now forwarded to appsec headers-only, while gRPC streams (always POST) stay droppable. Fix #351 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🍱 add test and remove useless comment * 🍱 add test for GET with unreadable body * 🍱 fix log * 🍱 fix test --------- Co-authored-by: mhx <mathieu@hanotaux.fr> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
This commit is contained in:
co-authored by
Claude Fable 5
mhx
maxlerebourg
parent
1c98c70f14
commit
31874263f2
+53
-11
@@ -7,6 +7,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
@@ -396,29 +397,27 @@ func (b blockingBody) Read(_ []byte) (int, error) {
|
||||
func (blockingBody) Close() error { return nil }
|
||||
|
||||
func Test_isBodyUnreadable(t *testing.T) {
|
||||
realBody := func() io.ReadCloser { return io.NopCloser(strings.NewReader("data")) }
|
||||
tests := []struct {
|
||||
name string
|
||||
protoMajor int
|
||||
contentLength int64
|
||||
hasBody bool
|
||||
body io.ReadCloser
|
||||
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},
|
||||
{name: "http2 grpc stream without content-length", protoMajor: 2, contentLength: -1, body: realBody(), want: true},
|
||||
{name: "http3 stream without content-length", protoMajor: 3, contentLength: -1, body: realBody(), want: true},
|
||||
{name: "http2 with content-length", protoMajor: 2, contentLength: 42, body: realBody(), want: false},
|
||||
{name: "http1.1 chunked without content-length", protoMajor: 1, contentLength: -1, body: realBody(), want: false},
|
||||
{name: "http2 without body", protoMajor: 2, contentLength: -1, body: nil, want: false},
|
||||
{name: "http2 with http.NoBody", protoMajor: 2, contentLength: -1, body: http.NoBody, 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
|
||||
}
|
||||
req.Body = tt.body
|
||||
if got := isBodyUnreadable(req); got != tt.want {
|
||||
t.Errorf("isBodyUnreadable() = %v, want %v", got, tt.want)
|
||||
}
|
||||
@@ -513,3 +512,46 @@ func Test_appsecQuery_dropUnreadableBody(t *testing.T) {
|
||||
t.Fatal("appsecQuery() blocked on a streaming request body (issue #323 regression)")
|
||||
}
|
||||
}
|
||||
|
||||
func newUnreadableGetRequest(done <-chan struct{}) *http.Request {
|
||||
req, _ := http.NewRequest(http.MethodGet, "http://localhost/", blockingBody{done: done})
|
||||
req.ProtoMajor = 3
|
||||
req.ContentLength = -1
|
||||
return req
|
||||
}
|
||||
|
||||
// Test_appsecQuery_unreadableBodyGetNotDropped is a regression test for issue #351
|
||||
func Test_appsecQuery_unreadableBodyGetNotDropped(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", newUnreadableGetRequest(done))
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-finished:
|
||||
if err != nil {
|
||||
t.Errorf("appsecQuery() on an HTTP/3 GET without content-length returned error: %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("appsecQuery() blocked on an HTTP/3 GET request body (issue #351 regression)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user