Commit 5b266c80 authored by Randall Leeds's avatar Randall Leeds

Check cross site request token for WebSocket

http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html
parent 7ac29e8a
...@@ -6,6 +6,7 @@ imports = [ ...@@ -6,6 +6,7 @@ imports = [
'h.account' 'h.account'
'h.helpers' 'h.helpers'
'h.identity' 'h.identity'
'h.session'
'h.streamer' 'h.streamer'
] ]
...@@ -57,7 +58,10 @@ configure = [ ...@@ -57,7 +58,10 @@ configure = [
basePattern = baseURI.replace /\/[^\/]*$/, '/**.html' basePattern = baseURI.replace /\/[^\/]*$/, '/**.html'
$sceDelegateProvider.resourceUrlWhitelist ['self', basePattern] $sceDelegateProvider.resourceUrlWhitelist ['self', basePattern]
streamerProvider.url = baseURI.replace('http', 'ws') + 'ws' streamerProvider.urlFn = ['xsrf', (xsrf) ->
base = baseURI.replace(/^http/, 'ws')
"#{base}ws?csrf_token=#{xsrf.token}"
]
] ]
......
...@@ -40,19 +40,13 @@ class SessionProvider ...@@ -40,19 +40,13 @@ class SessionProvider
# }); # });
### ###
$get: [ $get: [
'$http', '$q', '$resource', 'documentHelpers', 'flash', '$http', '$q', '$resource', 'documentHelpers', 'flash', 'xsrf',
($http, $q, $resource, documentHelpers, flash) -> ($http, $q, $resource, documentHelpers, flash, xsrf) ->
actions = {} actions = {}
provider = this provider = this
# Capture the state of the cross site request forgery token.
# If cookies are blocked this is our only way to get it.
xsrfToken = null
prepare = (data, headersGetter) -> prepare = (data, headersGetter) ->
if xsrfToken headersGetter()[$http.defaults.xsrfHeaderName] = xsrf.token
headers = headersGetter()
headers[$http.defaults.xsrfHeaderName] = xsrfToken
return angular.toJson data return angular.toJson data
process = (data, headersGetter) -> process = (data, headersGetter) ->
...@@ -68,7 +62,7 @@ class SessionProvider ...@@ -68,7 +62,7 @@ class SessionProvider
for q, msgs of data.flash for q, msgs of data.flash
flash q, msgs flash q, msgs
xsrfToken = model.csrf xsrf.token = model.csrf
# Return the model # Return the model
model model
...@@ -85,3 +79,4 @@ class SessionProvider ...@@ -85,3 +79,4 @@ class SessionProvider
angular.module('h.session') angular.module('h.session')
.provider('session', SessionProvider) .provider('session', SessionProvider)
.value('xsrf', token: null)
...@@ -6,20 +6,21 @@ ST_OPEN = 3 ...@@ -6,20 +6,21 @@ ST_OPEN = 3
# @ngdoc service # @ngdoc service
# @name Streamer # @name Streamer
# #
# @param {String} url The base URL for the socket connection # @param {String} urlFn A function that will be called with injections to
# generate the socket URL.
# #
# @description # @description
# Provides access to the streamer websocket. # Provides access to the streamer websocket.
### ###
class Streamer class Streamer
constructor: (transport, url) -> constructor: (transport, urlFn) ->
this.onmessage = -> this.onmessage = ->
this._failCount = 0 this._failCount = 0
this._queue = [] this._queue = []
this._state = ST_CLOSED this._state = ST_CLOSED
this._transport = transport this._transport = transport
this._url = url this._urlFn = urlFn
###* ###*
# @ngdoc method # @ngdoc method
...@@ -34,7 +35,7 @@ class Streamer ...@@ -34,7 +35,7 @@ class Streamer
return return
self = this self = this
this._sock = new this._transport(this._url) this._sock = new this._transport(this._urlFn())
this._state = ST_CONNECTING this._state = ST_CONNECTING
this._sock.onopen = -> this._sock.onopen = ->
...@@ -112,9 +113,10 @@ setAjaxClientId = (clientId) -> ...@@ -112,9 +113,10 @@ setAjaxClientId = (clientId) ->
streamerProvider = -> streamerProvider = ->
provider = {} provider = {}
provider.url = null provider.urlFn = null
provider.$get = ['$window', ($window) -> provider.$get = ['$injector', '$window', ($injector, $window) ->
new Streamer($window.WebSocket, provider.url) urlFn = angular.bind $injector, $injector.invoke, provider.urlFn
new Streamer($window.WebSocket, urlFn)
] ]
return provider return provider
......
...@@ -23,10 +23,12 @@ describe 'session', -> ...@@ -23,10 +23,12 @@ describe 'session', ->
describe 'sessionService', -> describe 'sessionService', ->
$httpBackend = null $httpBackend = null
session = null session = null
xsrf = null
beforeEach inject (_$httpBackend_, _session_) -> beforeEach inject (_$httpBackend_, _session_, _xsrf_) ->
$httpBackend = _$httpBackend_ $httpBackend = _$httpBackend_
session = _session_ session = _session_
xsrf = _xsrf_
describe '#<action>()', -> describe '#<action>()', ->
url = '/login' url = '/login'
...@@ -60,15 +62,17 @@ describe 'session', -> ...@@ -60,15 +62,17 @@ describe 'session', ->
assert.match result.reason, response.reason, 'the reason is present' assert.match result.reason, response.reason, 'the reason is present'
it 'should capture and send the xsrf token', -> it 'should capture and send the xsrf token', ->
xsrf = 'deadbeef' token = 'deadbeef'
headers = headers =
'Accept': 'application/json, text/plain, */*' 'Accept': 'application/json, text/plain, */*'
'Content-Type': 'application/json;charset=utf-8' 'Content-Type': 'application/json;charset=utf-8'
'X-XSRF-TOKEN': xsrf 'X-XSRF-TOKEN': token
model = {csrf: xsrf} model = {csrf: token}
request = $httpBackend.expectPOST(url).respond({model}) request = $httpBackend.expectPOST(url).respond({model})
result = session.login({}) result = session.login({})
$httpBackend.flush() $httpBackend.flush()
assert.equal xsrf.token, token
$httpBackend.expectPOST(url, {}, headers).respond({}) $httpBackend.expectPOST(url, {}, headers).respond({})
session.login({}) session.login({})
$httpBackend.flush() $httpBackend.flush()
...@@ -17,7 +17,8 @@ describe 'streamer', -> ...@@ -17,7 +17,8 @@ describe 'streamer', ->
WebSocket = sandbox.stub().returns(fakeSock) WebSocket = sandbox.stub().returns(fakeSock)
$provide.decorator '$window', ($delegate) -> $provide.decorator '$window', ($delegate) ->
angular.extend $delegate, {WebSocket} angular.extend $delegate, {WebSocket}
streamerProvider.url = 'http://magicstreemz/giraffe' $provide.value 'webSocketUrl', 'wss://magicstreemz/giraffe'
streamerProvider.urlFn = (webSocketUrl) -> webSocketUrl
return return
beforeEach inject (_streamer_) -> beforeEach inject (_streamer_) ->
...@@ -26,10 +27,14 @@ describe 'streamer', -> ...@@ -26,10 +27,14 @@ describe 'streamer', ->
afterEach -> afterEach ->
sandbox.restore() sandbox.restore()
it 'calls the transport function with the new keyword', ->
streamer.open()
assert.calledWithNew(WebSocket)
it 'creates a socket with the correct base URL', -> it 'creates a socket with the correct base URL', ->
streamer.open() streamer.open()
assert.calledWith(WebSocket, 'http://magicstreemz/giraffe') assert.calledWith(WebSocket, 'wss://magicstreemz/giraffe')
it 'does not open another socket while a socket is connecting', -> it 'does not open another socket while a socket is connecting', ->
streamer.open() streamer.open()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment