Emond Papegaaij created WICKET-7197:
---------------------------------------

             Summary: Validate the origin of WebSocket handshakes
                 Key: WICKET-7197
                 URL: https://issues.apache.org/jira/browse/WICKET-7197
             Project: Wicket
          Issue Type: New Feature
          Components: wicket-core, wicket-native-websocket
            Reporter: Emond Papegaaij
            Assignee: Martin Tzvetanov Grigorov


Wicket does not validate the origin of a WebSocket handshake by default. Three
opt-in hooks exist and all three are inert out of the box:

* {{WebSocketSettings#connectionFilter}} has no initialiser, so it is null.
* {{WebSocketConnectionOriginFilter}} accepts everything while its 
allowed-domains
  list is empty, which is its initial state.
* {{AbstractUpgradeFilter#verifyOrigin}} returns true unconditionally, and is 
never
  called (see the notes below).

For HTTP, {{ResourceIsolationRequestCycleListener}} already implements exactly 
this
decision, with a pluggable {{IResourceIsolationPolicy}} and a Fetch Metadata
implementation. WebSocket traffic does not use it:
{{WebSocketAwareResourceIsolationRequestCycleListener}} exempts the WebSocket 
handler
from the check.

*That exemption is correct and should stay.* At frame time the available request
object reports its scheme as {{ws}}/{{wss}}, so an Origin-based policy could 
never
match a browser's {{Origin}} value; its header map is case-sensitive, so the
lowercase Fetch Metadata lookups miss; and its header values are single-use
enumerations. Removing the exemption would abort every WebSocket frame, 
including
same-origin ones.

*The check belongs at the handshake, not at the frame.* Origin is a property of 
the
connection: once a socket is accepted, every frame on it comes from whoever 
opened
it, so a per-frame check is both redundant and the wrong shape. At handshake 
time
there is a real {{HttpServletRequest}} with real headers.

h3. Proposal

# Gate the WebSocket upgrade on {{IResourceIsolationPolicy}}, evaluated once 
against
the handshake request, so there is a single place where Wicket decides whether
another origin may talk to the application.
# Teach {{FetchMetadataResourceIsolationPolicy}} about WebSockets. Browsers send
{{Sec-Fetch-Dest: websocket}}, {{Sec-Fetch-Mode: websocket}} and the usual
{{Sec-Fetch-Site: same-origin | same-site | cross-site}} on the upgrade 
request. The
policy currently knows {{document}}, {{script}}, {{image}}, {{object}} and 
{{embed}};
{{websocket}} needs adding. The header values should be confirmed against 
current
browsers before implementing, since this is external behaviour.
# Consider retiring the header-based {{OriginResourceIsolationPolicy}} in 
favour of
Fetch Metadata only. Fetch Metadata has shipped in Chrome since 2019, Firefox 
since
2021 and Safari since early 2023. {{OriginResourceIsolationPolicy}} implements 
only
the two-argument {{isRequestAllowed}}, so it ignores {{RequestType}} and cannot
distinguish a top-level navigation from a subresource load, which makes it 
awkward to
keep correct. It is public API, so this needs a deprecation cycle rather than 
removal
in a patch release.
# Decide explicitly what a request carrying *no* Fetch Metadata headers means: 
allow
(compatible, fail-open) or deny (strict). Today that choice is masked by the 
Origin
policy picking up the slack; removing the fallback forces it into the open.

h3. Notes

* {{AbstractUpgradeFilter#verifyOrigin}} is documented as the extension point 
for
validating the origin of a handshake ("intended to be overridden by sub-classes 
that
wish to verify the origin of a WebSocket request before processing it"), but the
override can never take effect. The only concrete subclass, 
{{JavaxWebSocketFilter}},
wraps its own {{FilterConfig}} in {{init()}} specifically to add the WebSocket 
path to
the filter's ignored-paths list. The endpoint is registered with the container 
by
class scan, so the filter code containing the {{verifyOrigin}} call does not 
run for a
handshake in a normal deployment. An application that follows the javadoc and
implements a real origin check believes it is protected and is not, with nothing
logged. Once the handshake is gated on {{IResourceIsolationPolicy}}, removing
{{verifyOrigin}} and its call site in favour of the single enforcement point is 
the
cleaner outcome, with the usual deprecation cycle since it is public API.
* {{FetchMetadataResourceIsolationPolicy}} looks its headers up in lowercase.
{{HttpServletRequest#getHeader}} is case-insensitive by specification, so this 
is fine
for a real request, but any code path that reads headers from a *copy* of a 
request
needs to preserve case-insensitive lookup, or the policy will silently see 
nothing and
return its no-header answer.
* Fetch Metadata is sent by the browser whether or not a cookie accompanies the
request, so a handshake-time check does not depend on the container's cookie
configuration. That is the main practical argument for this approach over 
anything
cookie-based.
* Neither {{WebSocketSettings#setConnectionFilter}} nor
{{WebSocketConnectionOriginFilter}} appears in the user guide, so an 
application that
wants to restrict which origins may open a socket has no documented route to 
doing so.
Whatever mechanism comes out of this issue needs to be written down.
* This is hardening, not a vulnerability. A cross-site handshake only reaches an
authenticated session if the browser attaches the container's session cookie to 
a
cross-site subresource request, which requires {{SameSite=None}}; modern 
browsers
apply lax-by-default, and that cookie is issued by the servlet container, not by
Wicket. Cross-origin WebSocket protection has been opt-in and off by default 
since
7.0, so no default was lost and there is no regression. This is worth doing 
because
the current situation is confusing and one documented hook does not work.




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to