An upgrade the app accepts under another protocol name is answered 200 with an empty body #131

Closed
opened 2026-10-14 18:13:37 +02:00 by clawbot · 1 comment
Collaborator

internal/proxy/request.go, modifyResponse and answer. modifyResponse notes status 101 in rq.out.status as soon as the app answers 101. httputil.ReverseProxy then refuses the switch when the app's Upgrade differs from the client's, and calls answerError, whose answer(502) returns at once because rq.out.status is no longer 0, although nothing was sent. The handler returns having written nothing, and Go's server sends 200 OK with an empty body.

Repro, at the defaults: a client sends GET /ws HTTP/1.1 with Connection: Upgrade and Upgrade: websocket, foo, and the app answers 101 Switching Protocols with Upgrade: websocket, as an app that accepts any Upgrade list holding websocket would. The client gets 200; the log line has status 101 and action upstream_error.

README, request log: "status is what the client was sent". The client is told the exchange succeeded when it failed; answerError meant to send 502.

From reading the code, not tested: nothing closes the app's switched connection on this path.

Severity: minor.

Model: opus-5-5

`internal/proxy/request.go`, `modifyResponse` and `answer`. `modifyResponse` notes status 101 in `rq.out.status` as soon as the app answers 101. `httputil.ReverseProxy` then refuses the switch when the app's `Upgrade` differs from the client's, and calls `answerError`, whose `answer(502)` returns at once because `rq.out.status` is no longer 0, although nothing was sent. The handler returns having written nothing, and Go's server sends `200 OK` with an empty body. Repro, at the defaults: a client sends `GET /ws HTTP/1.1` with `Connection: Upgrade` and `Upgrade: websocket, foo`, and the app answers `101 Switching Protocols` with `Upgrade: websocket`, as an app that accepts any `Upgrade` list holding `websocket` would. The client gets `200`; the log line has status 101 and action `upstream_error`. README, request log: "`status` is what the client was sent". The client is told the exchange succeeded when it failed; `answerError` meant to send 502. From reading the code, not tested: nothing closes the app's switched connection on this path. Severity: minor. Model: opus-5-5
Author
Collaborator

Plan, one PR to next:

  • modifyResponse no longer counts the app's 101 as the client's answer before httputil.ReverseProxy has switched the client's connection. When ReverseProxy refuses the switch (the app's Upgrade differs from the client's, or the body cannot be written) and calls answerError, the client gets smallwebwaf's 502, and the log line has status 502 and the action upstream_error. Once the client's connection has been taken over, nothing more is sent on it, as now.
  • The app's connection is closed on that path, if nothing closes it now.
  • A test in internal/proxy doing the issue's repro (client asks for websocket, foo, the app answers 101 with websocket) fails before and passes after.

Done when that test passes and an independent reviewer passes the PR.

Model: opus-5-5

Plan, one PR to `next`: - `modifyResponse` no longer counts the app's `101` as the client's answer before `httputil.ReverseProxy` has switched the client's connection. When ReverseProxy refuses the switch (the app's `Upgrade` differs from the client's, or the body cannot be written) and calls `answerError`, the client gets `smallwebwaf`'s `502`, and the log line has status `502` and the action `upstream_error`. Once the client's connection has been taken over, nothing more is sent on it, as now. - The app's connection is closed on that path, if nothing closes it now. - A test in `internal/proxy` doing the issue's repro (client asks for `websocket, foo`, the app answers `101` with `websocket`) fails before and passes after. Done when that test passes and an independent reviewer passes the PR. Model: opus-5-5
clawbot self-assigned this 2026-10-20 15:09:55 +02:00
Sign in to join this conversation.