Skip to content

🐛 appsec: do not buffer unreadable (gRPC/HTTP2) request bodies#332

Open
mathieuHa wants to merge 3 commits into
mainfrom
fix/323-grpc-appsec-unreadable-body
Open

🐛 appsec: do not buffer unreadable (gRPC/HTTP2) request bodies#332
mathieuHa wants to merge 3 commits into
mainfrom
fix/323-grpc-appsec-unreadable-body

Conversation

@mathieuHa
Copy link
Copy Markdown
Collaborator

Problem

Fixes #323.

After #321, AppSec silently returns 403 on long-lived gRPC streams (e.g. NetBird ConnectStream) when AppSec is enabled.

A bidirectional gRPC stream is an HTTP/2 request with no Content-Length whose body never reaches EOF. #321 removed the httpReq.ContentLength > 0 guard in appsecQuery, so the AppSec path now buffers every body with io.ReadAll. For an open stream that read blocks until the request times out, producing a 403 before the backend is reached (OriginStatus:0). Short unary gRPC calls succeed; only streams hang.

Fix

Mirror the reference lua-cs-bouncer, which refuses to read the body of an HTTP/2+ request with no Content-Length and then either forwards headers-only or drops it (APPSEC_DROP_UNREADABLE_BODY).

  • isBodyUnreadable(req)req.Body != nil && req.ProtoMajor >= 2 && req.ContentLength < 0 (the Go equivalent of the lua http_version() >= 2 and http_content_length == nil check).
  • When the body is unreadable, appsecQuery no longer buffers it:
    • default → forward to AppSec with headers only (IP / URI / verb / headers still inspected), request proceeds;
    • new CrowdsecAppsecDropUnreadableBody option (default false, mirrors APPSEC_DROP_UNREADABLE_BODY) → block the request outright.
  • Readable HTTP/1.1 bodies are still buffered and inspected → the bypass closed by 🐛 fix appsec bypass with invalid content-length #321 stays closed.

Tests

  • Test_isBodyUnreadable — HTTP/2 & HTTP/3 without Content-Length (true), HTTP/2 with Content-Length (false), HTTP/1.1 chunked (false), no body (false).
  • Test_appsecQuery_streamingDoesNotBlock — regression for [BUG] Silently returns 403 on gRPC streaming connections #323: a never-EOF gRPC body returns within 2s (hangs ~forever / 2s timeout without the fix).
  • Test_appsecQuery_dropUnreadableBody — verifies drop mode blocks.

go test ./..., go vet ./... and yaegi test -v . (Traefik plugin runtime) all pass.

Docs

CrowdsecAppsecDropUnreadableBody documented in the variables list and the full dynamic-configuration example.

Comment thread bouncer.go Outdated
switch {
case isBodyUnreadable(httpReq):
if bouncer.appsecDropUnreadableBody {
bouncer.log.Debug(fmt.Sprintf("appsecQuery:unreadableBody ip:%s dropped:true", ip))
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed, it's logged at the caller lever

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3ca63bc — removed the redundant log.Debug; the caller (handleNextServeHTTP) already logs the returned error with the IP.

mathieuHa and others added 2 commits June 6, 2026 12:12
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>
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>
@mathieuHa mathieuHa force-pushed the fix/323-grpc-appsec-unreadable-body branch from 1de51c4 to 4305080 Compare June 6, 2026 10:12
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Silently returns 403 on gRPC streaming connections

2 participants