Tran Ngoc Nhan 0f7a6d45fd Polish websocket
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-03-09 15:59:27 -04:00

322 lines
15 KiB
Plaintext

[[websocket]]
= WebSocket Security
Spring Security 4 added support for securing {spring-framework-reference-url}web/websocket.html[Spring's WebSocket support].
This section describes how to use Spring Security's WebSocket support.
.Direct JSR-356 Support
****
Spring Security does not provide direct JSR-356 support, because doing so would provide little value.
Additionally, JSR-356 does not provide a way to intercept messages, so security would be invasive.
Spring Framework provides support for using STOMP — a simple, messaging protocol originally created for use in scripting languages with frames inspired by HTTP. STOMP is widely supported and well suited for use over WebSocket and over the web. For more details, see {spring-framework-reference-url}web/websocket/stomp.html[here].
****
[[websocket-authentication]]
== WebSocket Authentication
WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made.
This means that the `Principal` on the `HttpServletRequest` will be handed off to WebSockets.
If you are using Spring Security, the `Principal` on the `HttpServletRequest` is overridden automatically.
More concretely, to ensure a user has authenticated to your WebSocket application, all that is necessary is to ensure that you setup Spring Security to authenticate your HTTP based web application.
[[websocket-authorization]]
== WebSocket Authorization
Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
In Spring Security 5.8, this support has been refreshed to use the `AuthorizationManager` API.
To configure authorization using Java Configuration, simply include the `@EnableWebSocketSecurity` annotation and publish an `AuthorizationManager<Message<?>>` bean or in xref:servlet/appendix/namespace/websocket.adoc#nsa-websocket-security[XML] use the `use-authorization-manager` attribute.
One way to do this is by using the `AuthorizationManagerMessageMatcherRegistry` to specify endpoint patterns like so:
include-code::./WebSocketSecurityConfig[tag=snippet,indent=0]
<1> Any inbound CONNECT message requires a valid CSRF token to enforce the <<websocket-sameorigin,Same Origin Policy>>.
<2> The `SecurityContextHolder` is populated with the user within the `simpUser` header attribute for any inbound request.
<3> Our messages require the proper authorization. Specifically, any inbound message that starts with `/user/` will require `ROLE_USER`. You can find additional details on authorization in <<websocket-authorization>>
[[custom-authorization]]
=== Custom Authorization
When using `AuthorizationManager`, customization is quite simple.
For example, you can publish an `AuthorizationManager` that requires that all messages have a role of `"USER"` using `AuthorityAuthorizationManager`, as seen below:
include-code::./WebSocketSecurityConfig[tag=snippet,indent=0]
There are several ways to further match messages, as can be seen in a more advanced example below:
include-code::./AdvancedWebSocketSecurityConfig[tag=snippet,indent=0]
This will ensure that:
<1> Any message without a destination (i.e. anything other than Message type of `MESSAGE` or `SUBSCRIBE`) will require the user to be authenticated
<2> Anyone can subscribe to `/user/queue/errors`
<3> Any message that has a destination starting with `"/app/"` will be required the user to have the role `ROLE_USER`
<4> Any message that starts with `"/user/"` or `"/topic/friends/"` that is of type SUBSCRIBE will require `ROLE_USER`
<5> Any other message of type `MESSAGE` or `SUBSCRIBE` is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types.
<6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages.
[[migrating-spel-expressions]]
=== Migrating SpEL Expressions
If you are migrating from an older version of Spring Security, your destination matchers may include SpEL expressions.
It's recommended that these be changed to using concrete implementations of `AuthorizationManager` since this is independently testable.
However, to ease migration, you can use
`org.springframework.security.messaging.access.expression.MessageExpressionAuthorizationManager`.
And specify an instance for each matcher that you cannot yet migrate:
include-code::./WebSocketSecurityConfig[tag=snippet,indent=0]
[[websocket-authorization-notes]]
=== WebSocket Authorization Notes
To properly secure your application, you need to understand Spring's WebSocket support.
[[websocket-authorization-notes-messagetypes]]
==== WebSocket Authorization on Message Types
You need to understand the distinction between `SUBSCRIBE` and `MESSAGE` types of messages and how they work within Spring.
Consider a chat application:
* The system can send a notification `MESSAGE` to all users through a destination of `/topic/system/notifications`.
* Clients can receive notifications by `SUBSCRIBE` to the `/topic/system/notifications`.
While we want clients to be able to `SUBSCRIBE` to `/topic/system/notifications`, we do not want to enable them to send a `MESSAGE` to that destination.
If we allowed sending a `MESSAGE` to `/topic/system/notifications`, clients could send a message directly to that endpoint and impersonate the system.
In general, it is common for applications to deny any `MESSAGE` sent to a destination that starts with the {spring-framework-reference-url}web/websocket/stomp.html[broker prefix] (`/topic/` or `/queue/`).
[[websocket-authorization-notes-destinations]]
==== WebSocket Authorization on Destinations
You should also understand how destinations are transformed.
Consider a chat application:
* Users can send messages to a specific user by sending a message to the `/app/chat` destination.
* The application sees the message, ensures that the `from` attribute is specified as the current user (we cannot trust the client).
* The application then sends the message to the recipient by using `SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)`.
* The message gets turned into the destination of `/queue/user/messages-<sessionid>`.
With this chat application, we want to let our client to listen `/user/queue`, which is transformed into `/queue/user/messages-<sessionid>`.
However, we do not want the client to be able to listen to `/queue/*`, because that would let the client see messages for every user.
In general, it is common for applications to deny any `SUBSCRIBE` sent to a message that starts with the {spring-framework-reference-url}web/websocket/stomp.html[broker prefix] (`/topic/` or `/queue/`).
We may provide exceptions to account for things like
//FIXME: Like what?
[[websocket-authorization-notes-outbound]]
=== Outbound Messages
The Spring Framework reference documentation contains a section titled {spring-framework-reference-url}web/websocket/stomp/message-flow.html["`Flow of Messages`"] that describes how messages flow through the system.
Note that Spring Security secures only the `clientInboundChannel`.
Spring Security does not attempt to secure the `clientOutboundChannel`.
The most important reason for this is performance.
For every message that goes in, typically many more go out.
Instead of securing the outbound messages, we encourage securing the subscription to the endpoints.
[[websocket-sameorigin]]
== Enforcing Same Origin Policy
Note that the browser does not enforce the https://en.wikipedia.org/wiki/Same-origin_policy[Same Origin Policy] for WebSocket connections.
This is an extremely important consideration.
[[websocket-sameorigin-why]]
=== Why Same Origin?
Consider the following scenario.
A user visits `bank.com` and authenticates to their account.
The same user opens another tab in their browser and visits `evil.com`.
The Same Origin Policy ensures that `evil.com` cannot read data from or write data to `bank.com`.
With WebSockets, the Same Origin Policy does not apply.
In fact, unless `bank.com` explicitly forbids it, `evil.com` can read and write data on behalf of the user.
This means that anything the user can do over the webSocket (such as transferring money), `evil.com` can do on that user's behalf.
Since SockJS tries to emulate WebSockets, it also bypasses the Same Origin Policy.
This means that developers need to explicitly protect their applications from external domains when they use SockJS.
[[websocket-sameorigin-spring]]
=== Spring WebSocket Allowed Origin
Fortunately, since Spring 4.1.5 Spring's WebSocket and SockJS support restricts access to the {spring-framework-reference-url}web/websocket/server.html#websocket-server-allowed-origins[current domain].
Spring Security adds an additional layer of protection to provide https://en.wikipedia.org/wiki/Defence_in_depth_(non-military)#Information_security[defense in depth].
[[websocket-sameorigin-csrf]]
=== Adding CSRF to Stomp Headers
By default, Spring Security requires the xref:features/exploits/csrf.adoc#csrf[CSRF token] in any `CONNECT` message type.
This ensures that only a site that has access to the CSRF token can connect.
Since only the *same origin* can access the CSRF token, external domains are not allowed to make a connection.
Typically we need to include the CSRF token in an HTTP header or an HTTP parameter.
However, SockJS does not allow for these options.
Instead, we must include the token in the Stomp headers.
Applications can xref:servlet/exploits/csrf.adoc#csrf-integration[obtain a CSRF token] by accessing the request attribute named `_csrf`.
For example, the following allows accessing the `CsrfToken` in a JSP:
[source,javascript]
----
var headerName = "${_csrf.headerName}";
var token = "${_csrf.token}";
----
If you use static HTML, you can expose the `CsrfToken` on a REST endpoint.
For example, the following would expose the `CsrfToken` on the `/csrf` URL:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@RestController
public class CsrfController {
@RequestMapping("/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@RestController
class CsrfController {
@RequestMapping("/csrf")
fun csrf(token: CsrfToken): CsrfToken {
return token
}
}
----
======
The JavaScript can make a REST call to the endpoint and use the response to populate the `headerName` and the token.
We can now include the token in our Stomp client:
[source,javascript]
----
...
var headers = {};
headers[headerName] = token;
stompClient.connect(headers, function(frame) {
...
})
----
[[websocket-sameorigin-disable]]
=== Disable CSRF within WebSockets
NOTE: At this point, CSRF is not configurable when using `@EnableWebSocketSecurity`, though this will likely be added in a future release.
To disable CSRF, instead of using `@EnableWebSocketSecurity`, you can use XML support or add the Spring Security components yourself, like so:
include-code::./WebSocketSecurityConfig[tag=snippet,indent=0]
[[websocket-expression-handler]]
=== Custom Expression Handler
At times, there may be value in customizing how the `access` expressions are handled defined in your `intercept-message` XML elements.
To do this, you can create a class of type `SecurityExpressionHandler<MessageAuthorizationContext<?>>` and refer to it in your XML definition like so:
[source,xml]
----
<websocket-message-broker use-authorization-manager="true">
<expression-handler ref="myRef"/>
...
</websocket-message-broker>
<b:bean ref="myRef" class="org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler"/>
----
If you are migrating from a legacy usage of `websocket-message-broker` that implements a `SecurityExpressionHandler<Message<?>>`, you can:
1. Additionally implement the `createEvaluationContext(Supplier, Message)` method and then
2. Wrap that value in a `MessageAuthorizationContextSecurityExpressionHandler` like so:
[source,xml]
----
<websocket-message-broker use-authorization-manager="true">
<expression-handler ref="myRef"/>
...
</websocket-message-broker>
<b:bean ref="myRef" class="org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler">
<b:constructor-arg>
<b:bean class="org.example.MyLegacyExpressionHandler"/>
</b:constructor-arg>
</b:bean>
----
[[websocket-sockjs]]
== Working with SockJS
{spring-framework-reference-url}web/websocket/fallback.html[SockJS] provides fallback transports to support older browsers.
When using the fallback options, we need to relax a few security constraints to allow SockJS to work with Spring Security.
[[websocket-sockjs-sameorigin]]
=== SockJS & frame-options
SockJS may use a https://github.com/sockjs/sockjs-client/tree/v0.3.4[transport that leverages an iframe].
By default, Spring Security xref:features/exploits/headers.adoc#headers-frame-options[denies] the site from being framed to prevent clickjacking attacks.
To allow SockJS frame-based transports to work, we need to configure Spring Security to let the same origin frame the content.
You can customize `X-Frame-Options` with the xref:servlet/appendix/namespace/http.adoc#nsa-frame-options[frame-options] element.
For example, the following instructs Spring Security to use `X-Frame-Options: SAMEORIGIN`, which allows iframes within the same domain:
[source,xml]
----
<http>
<!-- ... -->
<headers>
<frame-options
policy="SAMEORIGIN" />
</headers>
</http>
----
Similarly, you can customize frame options to use the same origin within Java Configuration by using the following:
include-code::./WebSecurityConfig[tag=snippet,indent=0]
[[websocket-sockjs-csrf]]
=== SockJS & Relaxing CSRF
SockJS uses a POST on the CONNECT messages for any HTTP-based transport.
Typically, we need to include the CSRF token in an HTTP header or an HTTP parameter.
However, SockJS does not allow for these options.
Instead, we must include the token in the Stomp headers as described in <<websocket-sameorigin-csrf>>.
It also means that we need to relax our CSRF protection with the web layer.
Specifically, we want to disable CSRF protection for our connect URLs.
We do NOT want to disable CSRF protection for every URL.
Otherwise, our site is vulnerable to CSRF attacks.
We can easily achieve this by providing a CSRF `RequestMatcher`.
Our Java configuration makes this easy.
For example, if our stomp endpoint is `/chat`, we can disable CSRF protection only for URLs that start with `/chat/` by using the following configuration:
include-code::./WebSecurityConfig[tag=snippet,indent=0]
If we use XML-based configuration, we can use the xref:servlet/appendix/namespace/http.adoc#nsa-csrf-request-matcher-ref[csrf@request-matcher-ref].
include-code::./WebSocketSecurityConfig[tag=snippet,indent=0]
[[legacy-websocket-configuration]]
== Legacy WebSocket Configuration
`AbstractSecurityWebSocketMessageBrokerConfigurer` and `MessageSecurityMetadataSourceRegistry` are removed as of Spring Security 7.
Please see {site-url}/5.8/migration/servlet/authorization.html#_use_authorizationmanager_for_message_security[the 5.8 migration guide] for guidance.