Skip to content

Response headers from backend are lost when modsecurity allows the request #29

@guillaumeaversa

Description

@guillaumeaversa

Description

When the modsecurity plugin allows a request (status < 400), the response headers set by the backend application are not forwarded to the client. Only the response body and status code are preserved.

Reproduction

Setup:

  • Traefik v3 with traefik-modsecurity-plugin v1.3.0
  • ModSecurity sidecar (OWASP CRS 4.23.0, Apache)
  • Backend: PHP application that returns CORS headers (Access-Control-Allow-Origin, etc.)

Steps:

  1. Call the backend directly (bypassing Traefik):
kubectl exec deploy/portal-api-php -- curl -s -D- -o /dev/null \
  -H "Origin: https://app.example.com" \
  "http://localhost/api/portal/services/2000"

Result: ✅ CORS headers present

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: X-Requested-With, Content-Type, Accept, Origin, Authorization
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
  1. Call through Traefik without the modsecurity plugin (IngressRoute with no middlewares):
curl -sk -D- -o /dev/null \
  -H "Origin: https://app.example.com" \
  "https://app.example.com/api/portal/services/2000"

Result: ✅ CORS headers present

HTTP/2 200
access-control-allow-origin: *
access-control-allow-headers: X-Requested-With, Content-Type, Accept, Origin, Authorization
access-control-allow-methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
  1. Call through Traefik with the modsecurity plugin middleware:
curl -sk -D- -o /dev/null \
  -H "Origin: https://app.example.com" \
  "https://app.example.com/api/portal/services/2000"

Result: ❌ CORS headers missing

HTTP/2 200

Analysis

In modsecurity.go, when modsecurity allows the request (status < 400), the plugin calls:

a.next.ServeHTTP(rw, req)

This passes the original ResponseWriter to the next handler, so response headers from the backend should be preserved. However, they are not.

A possible cause is the interaction between http.MaxBytesReader and Traefik's ResponseWriter:

body, err := ioutil.ReadAll(http.MaxBytesReader(rw, req.Body, a.maxBodySize))

http.MaxBytesReader stores a reference to rw. In Go 1.20+, this wrapper can interact with the ResponseWriter's internal state (via the requestTooLarge() interface), which may interfere with how Traefik's ResponseWriter wrapper flushes response headers.

Another possible cause: defer resp.Body.Close() on the modsecurity sidecar response runs after a.next.ServeHTTP(rw, req) returns, which could interfere with the final response flush in streaming scenarios.

Expected behavior

Response headers from the backend should be forwarded to the client when modsecurity allows the request, exactly as if the plugin were not in the middleware chain.

Environment

  • Traefik v3 (chart v39.0.5)
  • Plugin v1.3.0
  • ModSecurity sidecar: owasp/modsecurity-crs:4.23.0-apache-alpine-202602050102
  • Go (Traefik binary)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions