Skip to content

Subrequest auth (TARGET=" ") returns 400 for every multipart POST #1995

Open

Description

@LeaderbotX400

Describe the bug

Anubis in subrequest-auth mode answers 400 Bad Request to the check endpoint whenever the forwarded request has a multipart/form-data Content-Type.

Reverse proxies doing forward auth (Traefik forwardAuth, nginx auth_request, Caddy forward_auth) copy the original request's headers to the check subrequest but not its body. ServeHTTPNext now calls prepareChallengeForm, which runs r.ParseMultipartForm on that empty body. The parse fails, and the error is turned into a hard 400 before the redir logic runs.

The proxy relays the 400 to the client, so every multipart form POST on a protected site fails. JSON and urlencoded POSTs and all GETs keep working, which makes the breakage look random from the application side.

Steps to reproduce

docker network create anubis-repro
docker run -d --name anubis-repro --network anubis-repro \
  -e BIND=:8080 -e 'TARGET= ' ghcr.io/techarohq/anubis:main

# Simulates what a forward-auth proxy sends for a multipart POST:
# original headers, no body.
docker run --rm --network anubis-repro curlimages/curl -s -w ' %{http_code}\n' -o /dev/null \
  -H 'X-Forwarded-Method: POST' \
  -H 'X-Forwarded-Proto: https' \
  -H 'X-Forwarded-Host: example.com' \
  -H 'X-Forwarded-Uri: /upload' \
  -H 'X-Real-Ip: 203.0.113.7' \
  -H 'Content-Type: multipart/form-data; boundary=x' \
  http://anubis-repro:8080/.within.website/x/cmd/anubis/api/check

Results:

Image multipart/form-data application/json no Content-Type
:main (d60d8a8) 400 Bad Request 200 200
:v1.28.0-pre2 200 200 200

Expected: 200, the same as for the other content types. The check endpoint has no business reading the proxied request's body.
Actual: 400 Bad Request (text/plain) from prepareChallengeForm.

Expected behavior

200, the same as for the other content types. The check endpoint has no business reading the proxied request's body.

Your operating system and its version.

Linux 6.18.33 #1-NixOS

Your browser and its version.

Firefox 156

Additional context

If I had to guess this seems like the culprit

lib/http.go at d60d8a8:

func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
	if s.next == nil {
		w.Header().Set("Cache-Control", "no-store")
		if !prepareChallengeForm(w, r) {   // added in #1989
			return
		}
		defer cleanupChallengeForm(r)
		...
		redir := r.FormValue("redir")

lib/challenge_form.go:

func prepareChallengeForm(w http.ResponseWriter, r *http.Request) bool {
	r.Body = http.MaxBytesReader(w, r.Body, 64<<10)
	err := r.ParseForm()
	if err == nil {
		err = r.ParseMultipartForm(64 << 10)
	}
	if err == nil || errors.Is(err, http.ErrNotMultipart) {
		return true
	}
	...
	http.Error(w, http.StatusText(status), status)   // -> 400
	return false
}

With Content-Type: multipart/form-data; boundary=… and an empty body, ParseMultipartForm returns a multipart read error (not http.ErrNotMultipart), so the request is rejected.

Before #1989, this branch only called r.FormValue("redir"). That also parses multipart forms, but it ignores parse errors, so the check went through.

prepareChallengeForm makes sense for MakeChallenge and PassChallenge, which really do get form posts from the challenge page. In ServeHTTPNext's subrequest branch, though, the request is a proxied auth check whose body (if any) belongs to the upstream application.

Activity

  1. asta-kassel commented on Oct 7, 2026

    @asta-kassel

    Thanks for the write up up :). This was quite hard to debug why our php application broke on the newest update. First I suspected our php application and then traefik. Our application allows it to upload files and only that was broken. Only afterwards I checked anubis.

  2. levank311-pixel commented on Oct 8, 2026

    @levank311-pixel

    Mô tả lỗi

    Ở chế độ xác thực yêu cầu con, Anubis sẽ trả lời 400 Bad Requestđiểm cuối kiểm tra bất cứ khi nào yêu cầu được chuyển tiếp có multipart/form-dataContent-Type.

    Các máy chủ proxy ngược thực hiện xác thực chuyển tiếp (Traefik forwardAuth, nginx auth_request, Caddy forward_auth) sao chép tiêu đề của yêu cầu gốc vào yêu cầu con kiểm tra nhưng không sao chép phần thân . ServeHTTPNextBây giờ prepareChallengeForm, nó gọi hàm, hàm này chạy r.ParseMultipartFormtrên phần thân trống đó. Quá trình phân tích cú pháp thất bại và lỗi được chuyển thành lỗi 400 trước khi redirlogic được thực thi.

    Máy chủ proxy chuyển tiếp lỗi 400 đến máy khách, do đó mọi yêu cầu POST dạng đa phần trên trang web được bảo vệ đều thất bại. Các yêu cầu POST JSON và urlencoded cũng như tất cả các yêu cầu GET vẫn hoạt động bình thường, điều này khiến sự cố trông có vẻ ngẫu nhiên từ phía ứng dụng.

    Các bước để tái hiện

    docker network create anubis-repro
    docker run -d --name anubis-repro --network anubis-repro
    -e BIND=:8080 -e 'TARGET= ' ghcr.io/techarohq/anubis:main

    Simulates what a forward-auth proxy sends for a multipart POST:

    original headers, no body.

    docker run --rm --network anubis-repro curlimages/curl -s -w ' %{http_code}\n' -o /dev/null
    -H 'X-Forwarded-Method: POST'
    -H 'X-Forwarded-Proto: https'
    -H 'X-Forwarded-Host: example.com'
    -H 'X-Forwarded-Uri: /upload'
    -H 'X-Real-Ip: 203.0.113.7'
    -H 'Content-Type: multipart/form-data; boundary=x'
    http://anubis-repro:8080/.within.website/x/cmd/anubis/api/check
    Kết quả:

    Hình ảnh multipart/form-data application/json không có Content-Type
    :main( d60d8a8) 400 Bad Request 200 200
    :v1.28.0-pre2 200 200 200
    Dự kiến: 200, giống như các loại nội dung khác. Điểm cuối kiểm tra không có lý do gì để đọc phần thân của yêu cầu được ủy quyền. Thực tế: 400 Bad Request ( text/plain) từ prepareChallengeForm.

    Hành vi dự kiến

    200, giống như đối với các loại nội dung khác. Điểm cuối kiểm tra không có lý do gì để đọc phần thân của yêu cầu được ủy quyền.

    Hệ điều hành và phiên bản của bạn.

    Linux 6.18.33 #1 -NixOS

    Trình duyệt và phiên bản của trình duyệt đó.

    Firefox 156

    Ngữ cảnh bổ sung

    Nếu phải đoán thì có vẻ đây chính là thủ phạm.

    lib/http.goTại d60d8a8:

    func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
    if s.next == nil {
    w.Header().Set("Cache-Control", "no-store")
    if !prepareChallengeForm(w, r) { // added in #1989
    return
    }
    defer cleanupChallengeForm(r)
    ...
    redir := r.FormValue("redir")
    lib/challenge_form.go:

    func prepareChallengeForm(w http.ResponseWriter, r *http.Request) bool {
    r.Body = http.MaxBytesReader(w, r.Body, 64<<10)
    err := r.ParseForm()
    if err == nil {
    err = r.ParseMultipartForm(64 << 10)
    }
    if err == nil || errors.Is(err, http.ErrNotMultipart) {
    return true
    }
    ...
    http.Error(w, http.StatusText(status), status) // -> 400
    return false
    }
    Với Content-Type: multipart/form-data; boundary=…phần thân rỗng, ParseMultipartFormnó trả về lỗi đọc đa phần (không phải http.ErrNotMultipart), do đó yêu cầu bị từ chối.

    Trước #1989 , nhánh này chỉ gọi r.FormValue("redir"). Hàm đó cũng phân tích các dạng đa phần, nhưng nó bỏ qua các lỗi phân tích cú pháp, vì vậy quá trình kiểm tra đã được thông qua.

    prepareChallengeFormĐiều này hợp lý đối với MakeChallengevà PassChallenge, vì chúng thực sự nhận được các bài đăng biểu mẫu từ trang thử thách. ServeHTTPNextTuy nhiên, trong nhánh yêu cầu con của , yêu cầu là một kiểm tra xác thực được ủy quyền mà phần thân (nếu có) thuộc về ứng dụng thượng nguồn.

  3. levank311-pixel commented on Oct 8, 2026

    @levank311-pixel

    Nhận thưởng

  4. levank311-pixel commented on Oct 8, 2026

    @levank311-pixel

    Mô tả lỗi

    Ở chế độ xác thực yêu cầu con, Anubis sẽ trả lời 400 Bad Requestđiểm cuối kiểm tra bất cứ khi nào yêu cầu được chuyển tiếp có multipart/form-dataContent-Type.

    Các máy chủ proxy ngược thực hiện xác thực chuyển tiếp (Traefik forwardAuth, nginx auth_request, Caddy forward_auth) sao chép tiêu đề của yêu cầu gốc vào yêu cầu con kiểm tra nhưng không sao chép phần thân . ServeHTTPNextBây giờ prepareChallengeForm, nó gọi hàm, hàm này chạy r.ParseMultipartFormtrên phần thân trống đó. Quá trình phân tích cú pháp thất bại và lỗi được chuyển thành lỗi 400 trước khi redirlogic được thực thi.

    Máy chủ proxy chuyển tiếp lỗi 400 đến máy khách, do đó mọi yêu cầu POST dạng đa phần trên trang web được bảo vệ đều thất bại. Các yêu cầu POST JSON và urlencoded cũng như tất cả các yêu cầu GET vẫn hoạt động bình thường, điều này khiến sự cố trông có vẻ ngẫu nhiên từ phía ứng dụng.

    Các bước để tái hiện

    docker network create anubis-repro
    docker run -d --name anubis-repro --network anubis-repro
    -e BIND=:8080 -e 'TARGET= ' ghcr.io/techarohq/anubis:main

    Simulates what a forward-auth proxy sends for a multipart POST:

    original headers, no body.

    docker run --rm --network anubis-repro curlimages/curl -s -w ' %{http_code}\n' -o /dev/null
    -H 'X-Forwarded-Method: POST'
    -H 'X-Forwarded-Proto: https'
    -H 'X-Forwarded-Host: example.com'
    -H 'X-Forwarded-Uri: /upload'
    -H 'X-Real-Ip: 203.0.113.7'
    -H 'Content-Type: multipart/form-data; boundary=x'
    http://anubis-repro:8080/.within.website/x/cmd/anubis/api/check
    Kết quả:

    Hình ảnh multipart/form-data application/json không có Content-Type
    :main( d60d8a8) 400 Bad Request 200 200
    :v1.28.0-pre2 200 200 200
    Dự kiến: 200, giống như các loại nội dung khác. Điểm cuối kiểm tra không có lý do gì để đọc phần thân của yêu cầu được ủy quyền. Thực tế: 400 Bad Request ( text/plain) từ prepareChallengeForm.

    Hành vi dự kiến

    200, giống như đối với các loại nội dung khác. Điểm cuối kiểm tra không có lý do gì để đọc phần thân của yêu cầu được ủy quyền.

    Hệ điều hành và phiên bản của bạn.

    Linux 6.18.33 #1 -NixOS

    Trình duyệt và phiên bản của trình duyệt đó.

    Firefox 156

    Ngữ cảnh bổ sung

    Nếu phải đoán thì có vẻ đây chính là thủ phạm.

    lib/http.goTại d60d8a8:

    func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
    if s.next == nil {
    w.Header().Set("Cache-Control", "no-store")
    if !prepareChallengeForm(w, r) { // added in #1989
    return
    }
    defer cleanupChallengeForm(r)
    ...
    redir := r.FormValue("redir")
    lib/challenge_form.go:

    func prepareChallengeForm(w http.ResponseWriter, r *http.Request) bool {
    r.Body = http.MaxBytesReader(w, r.Body, 64<<10)
    err := r.ParseForm()
    if err == nil {
    err = r.ParseMultipartForm(64 << 10)
    }
    if err == nil || errors.Is(err, http.ErrNotMultipart) {
    return true
    }
    ...
    http.Error(w, http.StatusText(status), status) // -> 400
    return false
    }
    Với Content-Type: multipart/form-data; boundary=…phần thân rỗng, ParseMultipartFormnó trả về lỗi đọc đa phần (không phải http.ErrNotMultipart), do đó yêu cầu bị từ chối.

    Trước #1989 , nhánh này chỉ gọi r.FormValue("redir"). Hàm đó cũng phân tích các dạng đa phần, nhưng nó bỏ qua các lỗi phân tích cú pháp, vì vậy quá trình kiểm tra đã được thông qua.

    prepareChallengeFormĐiều này hợp lý đối với MakeChallengevà PassChallenge, vì chúng thực sự nhận được các bài đăng biểu mẫu từ trang thử thách. ServeHTTPNextTuy nhiên, trong nhánh yêu cầu con của , yêu cầu là một kiểm tra xác thực được ủy quyền mà phần thân (nếu có) thuộc về ứng dụng thượng nguồn.

  5. levank311-pixel commented on Oct 8, 2026

    @levank311-pixel

    Mô tả lỗi

    Ở chế độ xác thực yêu cầu con, Anubis sẽ trả lời 400 Bad Requestđiểm cuối kiểm tra bất cứ khi nào yêu cầu được chuyển tiếp có multipart/form-dataContent-Type.

    Các máy chủ proxy ngược thực hiện xác thực chuyển tiếp (Traefik forwardAuth, nginx auth_request, Caddy forward_auth) sao chép tiêu đề của yêu cầu gốc vào yêu cầu con kiểm tra nhưng không sao chép phần thân . ServeHTTPNextBây giờ prepareChallengeForm, nó gọi hàm, hàm này chạy r.ParseMultipartFormtrên phần thân trống đó. Quá trình phân tích cú pháp thất bại và lỗi được chuyển thành lỗi 400 trước khi redirlogic được thực thi.

    Máy chủ proxy chuyển tiếp lỗi 400 đến máy khách, do đó mọi yêu cầu POST dạng đa phần trên trang web được bảo vệ đều thất bại. Các yêu cầu POST JSON và urlencoded cũng như tất cả các yêu cầu GET vẫn hoạt động bình thường, điều này khiến sự cố trông có vẻ ngẫu nhiên từ phía ứng dụng.

    Các bước để tái hiện

    docker network create anubis-repro
    docker run -d --name anubis-repro --network anubis-repro
    -e BIND=:8080 -e 'TARGET= ' ghcr.io/techarohq/anubis:main

    Simulates what a forward-auth proxy sends for a multipart POST:

    original headers, no body.

    docker run --rm --network anubis-repro curlimages/curl -s -w ' %{http_code}\n' -o /dev/null
    -H 'X-Forwarded-Method: POST'
    -H 'X-Forwarded-Proto: https'
    -H 'X-Forwarded-Host: example.com'
    -H 'X-Forwarded-Uri: /upload'
    -H 'X-Real-Ip: 203.0.113.7'
    -H 'Content-Type: multipart/form-data; boundary=x'
    http://anubis-repro:8080/.within.website/x/cmd/anubis/api/check
    Kết quả:

    Hình ảnh multipart/form-data application/json không có Content-Type
    :main( d60d8a8) 400 Bad Request 200 200
    :v1.28.0-pre2 200 200 200
    Dự kiến: 200, giống như các loại nội dung khác. Điểm cuối kiểm tra không có lý do gì để đọc phần thân của yêu cầu được ủy quyền. Thực tế: 400 Bad Request ( text/plain) từ prepareChallengeForm.

    Hành vi dự kiến

    200, giống như đối với các loại nội dung khác. Điểm cuối kiểm tra không có lý do gì để đọc phần thân của yêu cầu được ủy quyền.

    Hệ điều hành và phiên bản của bạn.

    Linux 6.18.33 #1 -NixOS

    Trình duyệt và phiên bản của trình duyệt đó.

    Firefox 156

    Ngữ cảnh bổ sung

    Nếu phải đoán thì có vẻ đây chính là thủ phạm.

    lib/http.goTại d60d8a8:

    func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
    if s.next == nil {
    w.Header().Set("Cache-Control", "no-store")
    if !prepareChallengeForm(w, r) { // added in #1989
    return
    }
    defer cleanupChallengeForm(r)
    ...
    redir := r.FormValue("redir")
    lib/challenge_form.go:

    func prepareChallengeForm(w http.ResponseWriter, r *http.Request) bool {
    r.Body = http.MaxBytesReader(w, r.Body, 64<<10)
    err := r.ParseForm()
    if err == nil {
    err = r.ParseMultipartForm(64 << 10)
    }
    if err == nil || errors.Is(err, http.ErrNotMultipart) {
    return true
    }
    ...
    http.Error(w, http.StatusText(status), status) // -> 400
    return false
    }
    Với Content-Type: multipart/form-data; boundary=…phần thân rỗng, ParseMultipartFormnó trả về lỗi đọc đa phần (không phải http.ErrNotMultipart), do đó yêu cầu bị từ chối.

    Trước #1989 , nhánh này chỉ gọi r.FormValue("redir"). Hàm đó cũng phân tích các dạng đa phần, nhưng nó bỏ qua các lỗi phân tích cú pháp, vì vậy quá trình kiểm tra đã được thông qua.

    prepareChallengeFormĐiều này hợp lý đối với MakeChallengevà PassChallenge, vì chúng thực sự nhận được các bài đăng biểu mẫu từ trang thử thách. ServeHTTPNextTuy nhiên, trong nhánh yêu cầu con của , yêu cầu là một kiểm tra xác thực được ủy quyền mà phần thân (nếu có) thuộc về ứng dụng thượng nguồn.

    Xát nhận

  6. levank311-pixel commented on Oct 8, 2026

    @levank311-pixel
  7. tari commented on Oct 11, 2026

    @tari

    My site only seems to have an issue with this on one path (that I know of so far), but I confirmed that both 1.28.0 and 1.28.1 are returning 400 errors for request authentication while anubis 1.27.0 does not.

  8. chrl-p commented on Oct 12, 2026

    @chrl-p

    I can confirm that I'm experiencing the same bug. Is there a temporary fix? I haven't been able to allow POST requests using a botPolicy.

  9. Xe commented on Oct 12, 2026

    @Xe
    Contributor

    Please try running the main branch docker image when CI passes after #2026 is merged.

    ghcr.io/techarohq/anubis:main
    

    This requires a patch to be fixed, I am not aware of a workaround other than changing your nginx configuration to not create this exact circumstance.

  10. chrl-p commented on Oct 13, 2026

    @chrl-p

    Thanks for the fix and your quick reply @Xe! I’ve deployed the latest ‘main’ image and the upload is working properly; that’s perfect! Do you have any idea when this might be merged into the ‘latest’ image?

  11. ne20002 commented on Oct 24, 2026

    @ne20002

    I believe this is fixed with Anubis 1.28.2.

  12. tari commented on Oct 24, 2026

    @tari

    Yes, 1.28.2 contains #2026 and I've confirmed on my setup (using nginx auth_request) that file uploads work with that version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions