websocket.adoc 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. [[websocket]]
  2. = WebSocket Security
  3. Spring Security 4 added support for securing https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html[Spring's WebSocket support].
  4. This section describes how to use Spring Security's WebSocket support.
  5. .Direct JSR-356 Support
  6. ****
  7. Spring Security does not provide direct JSR-356 support because doing so would provide little value.
  8. This is because the format is unknown, so there is https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-intro-sub-protocol[little Spring can do to secure an unknown format].
  9. Additionally, JSR-356 does not provide a way to intercept messages, so security would be rather invasive.
  10. ****
  11. [[websocket-configuration]]
  12. == WebSocket Configuration
  13. Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
  14. To configure authorization using Java Configuration, simply extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`.
  15. For example:
  16. ====
  17. .Java
  18. [source,java,role="primary"]
  19. ----
  20. @Configuration
  21. public class WebSocketSecurityConfig
  22. extends AbstractSecurityWebSocketMessageBrokerConfigurer { // <1> <2>
  23. protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
  24. messages
  25. .simpDestMatchers("/user/**").authenticated() // <3>
  26. }
  27. }
  28. ----
  29. .Kotlin
  30. [source,kotlin,role="secondary"]
  31. ----
  32. @Configuration
  33. open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() { // <1> <2>
  34. override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
  35. messages.simpDestMatchers("/user/**").authenticated() // <3>
  36. }
  37. }
  38. ----
  39. ====
  40. This will ensure that:
  41. <1> Any inbound CONNECT message requires a valid CSRF token to enforce <<websocket-sameorigin,Same Origin Policy>>
  42. <2> The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request.
  43. <3> Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in <<websocket-authorization>>
  44. Spring Security also provides xref:servlet/appendix/namespace.adoc#nsa-websocket-security[XML Namespace] support for securing WebSockets.
  45. A comparable XML based configuration looks like the following:
  46. [source,xml]
  47. ----
  48. <websocket-message-broker> <!--1--> <!--2-->
  49. <!--3-->
  50. <intercept-message pattern="/user/**" access="hasRole('USER')" />
  51. </websocket-message-broker>
  52. ----
  53. This will ensure that:
  54. <1> Any inbound CONNECT message requires a valid CSRF token to enforce <<websocket-sameorigin,Same Origin Policy>>
  55. <2> The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request.
  56. <3> Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in <<websocket-authorization>>
  57. [[websocket-authentication]]
  58. == WebSocket Authentication
  59. WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made.
  60. This means that the `Principal` on the `HttpServletRequest` will be handed off to WebSockets.
  61. If you are using Spring Security, the `Principal` on the `HttpServletRequest` is overridden automatically.
  62. 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.
  63. [[websocket-authorization]]
  64. == WebSocket Authorization
  65. Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
  66. To configure authorization using Java Configuration, simply extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`.
  67. For example:
  68. ====
  69. .Java
  70. [source,java,role="primary"]
  71. ----
  72. @Configuration
  73. public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
  74. @Override
  75. protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
  76. messages
  77. .nullDestMatcher().authenticated() // <1>
  78. .simpSubscribeDestMatchers("/user/queue/errors").permitAll() // <2>
  79. .simpDestMatchers("/app/**").hasRole("USER") // <3>
  80. .simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4>
  81. .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() // <5>
  82. .anyMessage().denyAll(); // <6>
  83. }
  84. }
  85. ----
  86. .Kotlin
  87. [source,kotlin,role="secondary"]
  88. ----
  89. @Configuration
  90. open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {
  91. override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
  92. messages
  93. .nullDestMatcher().authenticated() // <1>
  94. .simpSubscribeDestMatchers("/user/queue/errors").permitAll() // <2>
  95. .simpDestMatchers("/app/**").hasRole("USER") // <3>
  96. .simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4>
  97. .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() // <5>
  98. .anyMessage().denyAll() // <6>
  99. }
  100. }
  101. ----
  102. ====
  103. This will ensure that:
  104. <1> Any message without a destination (i.e. anything other than Message type of MESSAGE or SUBSCRIBE) will require the user to be authenticated
  105. <2> Anyone can subscribe to /user/queue/errors
  106. <3> Any message that has a destination starting with "/app/" will be require the user to have the role ROLE_USER
  107. <4> Any message that starts with "/user/" or "/topic/friends/" that is of type SUBSCRIBE will require ROLE_USER
  108. <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.
  109. <6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages.
  110. Spring Security also provides xref:servlet/appendix/namespace.adoc#nsa-websocket-security[XML Namespace] support for securing WebSockets.
  111. A comparable XML based configuration looks like the following:
  112. [source,xml]
  113. ----
  114. <websocket-message-broker>
  115. <!--1-->
  116. <intercept-message type="CONNECT" access="permitAll" />
  117. <intercept-message type="UNSUBSCRIBE" access="permitAll" />
  118. <intercept-message type="DISCONNECT" access="permitAll" />
  119. <intercept-message pattern="/user/queue/errors" type="SUBSCRIBE" access="permitAll" /> <!--2-->
  120. <intercept-message pattern="/app/**" access="hasRole('USER')" /> <!--3-->
  121. <!--4-->
  122. <intercept-message pattern="/user/**" access="hasRole('USER')" />
  123. <intercept-message pattern="/topic/friends/*" access="hasRole('USER')" />
  124. <!--5-->
  125. <intercept-message type="MESSAGE" access="denyAll" />
  126. <intercept-message type="SUBSCRIBE" access="denyAll" />
  127. <intercept-message pattern="/**" access="denyAll" /> <!--6-->
  128. </websocket-message-broker>
  129. ----
  130. This will ensure that:
  131. <1> Any message of type CONNECT, UNSUBSCRIBE, or DISCONNECT will require the user to be authenticated
  132. <2> Anyone can subscribe to /user/queue/errors
  133. <3> Any message that has a destination starting with "/app/" will be require the user to have the role ROLE_USER
  134. <4> Any message that starts with "/user/" or "/topic/friends/" that is of type SUBSCRIBE will require ROLE_USER
  135. <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.
  136. <6> Any other message with a destination is rejected. This is a good idea to ensure that you do not miss any messages.
  137. [[websocket-authorization-notes]]
  138. === WebSocket Authorization Notes
  139. In order to properly secure your application it is important to understand Spring's WebSocket support.
  140. [[websocket-authorization-notes-messagetypes]]
  141. ==== WebSocket Authorization on Message Types
  142. It is important to understand the distinction between SUBSCRIBE and MESSAGE types of messages and how it works within Spring.
  143. Consider a chat application.
  144. * The system can send notifications MESSAGE to all users through a destination of "/topic/system/notifications"
  145. * Clients can receive notifications by SUBSCRIBE to the "/topic/system/notifications".
  146. 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.
  147. If we allowed sending a MESSAGE to "/topic/system/notifications", then clients could send a message directly to that endpoint and impersonate the system.
  148. In general, it is common for applications to deny any MESSAGE sent to a destination that starts with the https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp[broker prefix] (i.e. "/topic/" or "/queue/").
  149. [[websocket-authorization-notes-destinations]]
  150. ==== WebSocket Authorization on Destinations
  151. It is also is important to understand how destinations are transformed.
  152. Consider a chat application.
  153. * Users can send messages to a specific user by sending a message to the destination of "/app/chat".
  154. * The application sees the message, ensures that the "from" attribute is specified as the current user (we cannot trust the client).
  155. * The application then sends the message to the recipient using `SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)`.
  156. * The message gets turned into the destination of "/queue/user/messages-<sessionid>"
  157. With the application above, we want to allow our client to listen to "/user/queue" which is transformed into "/queue/user/messages-<sessionid>".
  158. However, we do not want the client to be able to listen to "/queue/*" because that would allow the client to see messages for every user.
  159. In general, it is common for applications to deny any SUBSCRIBE sent to a message that starts with the https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp[broker prefix] (i.e. "/topic/" or "/queue/").
  160. Of course we may provide exceptions to account for things like
  161. [[websocket-authorization-notes-outbound]]
  162. === Outbound Messages
  163. Spring contains a section titled https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-message-flow[Flow of Messages] that describes how messages flow through the system.
  164. It is important to note that Spring Security only secures the `clientInboundChannel`.
  165. Spring Security does not attempt to secure the `clientOutboundChannel`.
  166. The most important reason for this is performance.
  167. For every message that goes in, there are typically many more that go out.
  168. Instead of securing the outbound messages, we encourage securing the subscription to the endpoints.
  169. [[websocket-sameorigin]]
  170. == Enforcing Same Origin Policy
  171. It is important to emphasize that the browser does not enforce the https://en.wikipedia.org/wiki/Same-origin_policy[Same Origin Policy] for WebSocket connections.
  172. This is an extremely important consideration.
  173. [[websocket-sameorigin-why]]
  174. === Why Same Origin?
  175. Consider the following scenario.
  176. A user visits bank.com and authenticates to their account.
  177. The same user opens another tab in their browser and visits evil.com.
  178. The Same Origin Policy ensures that evil.com cannot read or write data to bank.com.
  179. With WebSockets the Same Origin Policy does not apply.
  180. In fact, unless bank.com explicitly forbids it, evil.com can read and write data on behalf of the user.
  181. This means that anything the user can do over the webSocket (i.e. transfer money), evil.com can do on that users behalf.
  182. Since SockJS tries to emulate WebSockets it also bypasses the Same Origin Policy.
  183. This means developers need to explicitly protect their applications from external domains when using SockJS.
  184. [[websocket-sameorigin-spring]]
  185. === Spring WebSocket Allowed Origin
  186. Fortunately, since Spring 4.1.5 Spring's WebSocket and SockJS support restricts access to the https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-server-allowed-origins[current domain].
  187. Spring Security adds an additional layer of protection to provide https://en.wikipedia.org/wiki/Defense_in_depth_(computing)[defence in depth].
  188. [[websocket-sameorigin-csrf]]
  189. === Adding CSRF to Stomp Headers
  190. By default Spring Security requires the xref:features/exploits/csrf.adoc#csrf[CSRF token] in any CONNECT message type.
  191. This ensures that only a site that has access to the CSRF token can connect.
  192. Since only the *Same Origin* can access the CSRF token, external domains are not allowed to make a connection.
  193. Typically we need to include the CSRF token in an HTTP header or an HTTP parameter.
  194. However, SockJS does not allow for these options.
  195. Instead, we must include the token in the Stomp headers
  196. Applications can xref:servlet/exploits/csrf.adoc#servlet-csrf-include[obtain a CSRF token] by accessing the request attribute named _csrf.
  197. For example, the following will allow accessing the `CsrfToken` in a JSP:
  198. [source,javascript]
  199. ----
  200. var headerName = "${_csrf.headerName}";
  201. var token = "${_csrf.token}";
  202. ----
  203. If you are using static HTML, you can expose the `CsrfToken` on a REST endpoint.
  204. For example, the following would expose the `CsrfToken` on the URL /csrf
  205. ====
  206. .Java
  207. [source,java,role="primary"]
  208. ----
  209. @RestController
  210. public class CsrfController {
  211. @RequestMapping("/csrf")
  212. public CsrfToken csrf(CsrfToken token) {
  213. return token;
  214. }
  215. }
  216. ----
  217. .Kotlin
  218. [source,kotlin,role="secondary"]
  219. ----
  220. @RestController
  221. class CsrfController {
  222. @RequestMapping("/csrf")
  223. fun csrf(token: CsrfToken): CsrfToken {
  224. return token
  225. }
  226. }
  227. ----
  228. ====
  229. The JavaScript can make a REST call to the endpoint and use the response to populate the headerName and the token.
  230. We can now include the token in our Stomp client.
  231. For example:
  232. [source,javascript]
  233. ----
  234. ...
  235. var headers = {};
  236. headers[headerName] = token;
  237. stompClient.connect(headers, function(frame) {
  238. ...
  239. }
  240. ----
  241. [[websocket-sameorigin-disable]]
  242. === Disable CSRF within WebSockets
  243. If you want to allow other domains to access your site, you can disable Spring Security's protection.
  244. For example, in Java Configuration you can use the following:
  245. ====
  246. .Java
  247. [source,java,role="primary"]
  248. ----
  249. @Configuration
  250. public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
  251. ...
  252. @Override
  253. protected boolean sameOriginDisabled() {
  254. return true;
  255. }
  256. }
  257. ----
  258. .Kotlin
  259. [source,kotlin,role="secondary"]
  260. ----
  261. @Configuration
  262. open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {
  263. // ...
  264. override fun sameOriginDisabled(): Boolean {
  265. return true
  266. }
  267. }
  268. ----
  269. ====
  270. [[websocket-sockjs]]
  271. == Working with SockJS
  272. https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-fallback[SockJS] provides fallback transports to support older browsers.
  273. When using the fallback options we need to relax a few security constraints to allow SockJS to work with Spring Security.
  274. [[websocket-sockjs-sameorigin]]
  275. === SockJS & frame-options
  276. SockJS may use an https://github.com/sockjs/sockjs-client/tree/v0.3.4[transport that leverages an iframe].
  277. By default Spring Security will xref:features/exploits/headers.adoc#headers-frame-options[deny] the site from being framed to prevent Clickjacking attacks.
  278. To allow SockJS frame based transports to work, we need to configure Spring Security to allow the same origin to frame the content.
  279. You can customize X-Frame-Options with the xref:servlet/appendix/namespace.adoc#nsa-frame-options[frame-options] element.
  280. For example, the following will instruct Spring Security to use "X-Frame-Options: SAMEORIGIN" which allows iframes within the same domain:
  281. [source,xml]
  282. ----
  283. <http>
  284. <!-- ... -->
  285. <headers>
  286. <frame-options
  287. policy="SAMEORIGIN" />
  288. </headers>
  289. </http>
  290. ----
  291. Similarly, you can customize frame options to use the same origin within Java Configuration using the following:
  292. ====
  293. .Java
  294. [source,java,role="primary"]
  295. ----
  296. @EnableWebSecurity
  297. public class WebSecurityConfig extends
  298. WebSecurityConfigurerAdapter {
  299. @Override
  300. protected void configure(HttpSecurity http) throws Exception {
  301. http
  302. // ...
  303. .headers(headers -> headers
  304. .frameOptions(frameOptions -> frameOptions
  305. .sameOrigin()
  306. )
  307. );
  308. }
  309. }
  310. ----
  311. .Kotlin
  312. [source,kotlin,role="secondary"]
  313. ----
  314. @EnableWebSecurity
  315. open class WebSecurityConfig : WebSecurityConfigurerAdapter() {
  316. override fun configure(http: HttpSecurity) {
  317. http {
  318. // ...
  319. headers {
  320. frameOptions {
  321. sameOrigin = true
  322. }
  323. }
  324. }
  325. }
  326. }
  327. ----
  328. ====
  329. [[websocket-sockjs-csrf]]
  330. === SockJS & Relaxing CSRF
  331. SockJS uses a POST on the CONNECT messages for any HTTP based transport.
  332. Typically we need to include the CSRF token in an HTTP header or an HTTP parameter.
  333. However, SockJS does not allow for these options.
  334. Instead, we must include the token in the Stomp headers as described in <<websocket-sameorigin-csrf>>.
  335. It also means we need to relax our CSRF protection with the web layer.
  336. Specifically, we want to disable CSRF protection for our connect URLs.
  337. We do NOT want to disable CSRF protection for every URL.
  338. Otherwise our site will be vulnerable to CSRF attacks.
  339. We can easily achieve this by providing a CSRF RequestMatcher.
  340. Our Java Configuration makes this extremely easy.
  341. For example, if our stomp endpoint is "/chat" we can disable CSRF protection for only URLs that start with "/chat/" using the following configuration:
  342. ====
  343. .Java
  344. [source,java,role="primary"]
  345. ----
  346. @Configuration
  347. @EnableWebSecurity
  348. public class WebSecurityConfig
  349. extends WebSecurityConfigurerAdapter {
  350. @Override
  351. protected void configure(HttpSecurity http) throws Exception {
  352. http
  353. .csrf(csrf -> csrf
  354. // ignore our stomp endpoints since they are protected using Stomp headers
  355. .ignoringAntMatchers("/chat/**")
  356. )
  357. .headers(headers -> headers
  358. // allow same origin to frame our site to support iframe SockJS
  359. .frameOptions(frameOptions -> frameOptions
  360. .sameOrigin()
  361. )
  362. )
  363. .authorizeRequests(authorize -> authorize
  364. ...
  365. )
  366. ...
  367. ----
  368. .Kotlin
  369. [source,kotlin,role="secondary"]
  370. ----
  371. @Configuration
  372. @EnableWebSecurity
  373. open class WebSecurityConfig : WebSecurityConfigurerAdapter() {
  374. override fun configure(http: HttpSecurity) {
  375. http {
  376. csrf {
  377. ignoringAntMatchers("/chat/**")
  378. }
  379. headers {
  380. frameOptions {
  381. sameOrigin = true
  382. }
  383. }
  384. authorizeRequests {
  385. // ...
  386. }
  387. // ...
  388. ----
  389. ====
  390. If we are using XML based configuration, we can use the xref:servlet/appendix/namespace.adoc#nsa-csrf-request-matcher-ref[csrf@request-matcher-ref].
  391. For example:
  392. [source,xml]
  393. ----
  394. <http ...>
  395. <csrf request-matcher-ref="csrfMatcher"/>
  396. <headers>
  397. <frame-options policy="SAMEORIGIN"/>
  398. </headers>
  399. ...
  400. </http>
  401. <b:bean id="csrfMatcher"
  402. class="AndRequestMatcher">
  403. <b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_CSRF_MATCHER}"/>
  404. <b:constructor-arg>
  405. <b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
  406. <b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
  407. <b:constructor-arg value="/chat/**"/>
  408. </b:bean>
  409. </b:bean>
  410. </b:constructor-arg>
  411. </b:bean>
  412. ----