Reject non-GET WebSocket upgrade requests with 405 (RFC 6455) - #773
Merged
Conversation
WebSocketHandler gated the upgrade solely on a ws route match and the Upgrade/Connection headers, so any HTTP method (POST, QUERY, ...) with the right headers completed the handshake. RFC 6455 §4.1 requires the opening handshake to be a GET request; respond with 405 Method Not Allowed plus Allow: GET instead of upgrading, keeping Connection: close like the 403 path so a pipelined request cannot reuse the connection. The method check runs before the Origin check, both rejections share a single HTTP::Status-driven helper, and rejections are skipped when an upstream handler (e.g. a before filter) already closed the response. Closes #770
Mirror the connection-smuggling regression spec (#767) for the new non-GET rejection path: a POST upgrade with a compound `Connection: keep-alive, Upgrade` must produce a single 405 with Connection: close, and the pipelined request must not be served.
sdogruyol
reviewed
Aug 15, 2026
Member
Author
|
@sdogruyol |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description of the Change
Kemal::WebSocketHandlerpreviously gated the WebSocket upgrade solely on a matching ws route and the presence ofUpgrade/Connectionheaders. The HTTP method itself was never validated, so any method (POST,QUERY, …) carrying valid upgrade headers could complete the handshake. RFC 6455 §4.1 requires the opening handshake to be a GET request.This PR adds a method guard in
WebSocketHandler#call, placed after the existing “is this an upgrade request for a ws route?” check and before the Origin check:405 Method Not Allowed, anAllow: GETheader (required for 405 by RFC 9110 §10.2.1), andConnection: close— the same connection-smuggling protection already used by the 403 Origin rejection ([Security] Close connection after rejected WebSocket upgrade #767).reject_websocket!helper driven byHTTP::Status. The helper also skips writing if an upstream handler (e.g. a before filter) has already closed the response.Before / after, using the reproduction from #770:
Specs cover the full 405 response shape (status,
Allow,Connection: close,Content-Type, body) forPOST/QUERY/HEAD, confirm that the method check runs before the Origin check, and verify the unchanged fall-through for non-upgrade requests. A CHANGELOG entry is included.Alternate Designs
Fall through to the next handler instead of rejecting.
RFC 9110 §7.8 allows a server to ignore
Upgradeand process the request normally. Rejected: before this change a non-GET upgrade request never reachedRouteHandleranyway (it was upgraded or returned 403), so falling through would be a larger behavior change. An explicit 405 also matches the wider ecosystem — gorilla/websocket and Nodewsreturn 405, python-websockets returns 405 +Allow: GET, and Cowboy only upgrades GET.Route the rejection through custom
error 405handlers.Rejected for consistency: the 403 Origin rejection deliberately writes the response directly so the security-relevant shape (
Connection: close) cannot be altered; the 405 path follows the same rule.Compute
Allowdynamically from coexisting routes on the same path.Rejected as overkill: it would couple
WebSocketHandlertoRouteHandlerinternals. A staticAllow: GETcorrectly describes the upgrade resource and matches python-websockets’ behavior.Benefits
405+Allow: GETinstead of a protocol-violating101that can confuse intermediaries (or a misleading403that suggests an Origin problem).HTTP::Status, and no write attempt on an already-closed response.Possible Drawbacks
101(allowed origin) or403, and now receive405. This is documented in the CHANGELOG; such clients were already violating RFC 6455.405rather than reaching the HTTP route. This is not a regression — previously the request never reached the route either (it was upgraded or returned 403) — and requests without upgrade headers are unaffected.Allow: GETis static; it does not enumerate methods of HTTP routes that coexist on the same path (see Alternate Designs).