diff options
| author | Misko Hevery | 2011-03-10 13:50:00 -0800 |
|---|---|---|
| committer | Misko Hevery | 2011-03-11 14:16:53 -0800 |
| commit | c578f8c3ed0ca23b03ccde146cb13cfaf24f17cd (patch) | |
| tree | 12182c82ee4411091b6d92f81829dd52f8792e27 /src | |
| parent | 5b05c0de036f77db0cc493082e21b1451c6b9a5f (diff) | |
| download | angular.js-c578f8c3ed0ca23b03ccde146cb13cfaf24f17cd.tar.bz2 | |
Added XSRF prevention logic to $xhr service
Diffstat (limited to 'src')
| -rw-r--r-- | src/Browser.js | 20 | ||||
| -rw-r--r-- | src/angular-mocks.js | 27 | ||||
| -rw-r--r-- | src/service/xhr.js | 78 |
3 files changed, 98 insertions, 27 deletions
diff --git a/src/Browser.js b/src/Browser.js index fe6220ed..abafb2a5 100644 --- a/src/Browser.js +++ b/src/Browser.js @@ -7,6 +7,11 @@ var XHR = window.XMLHttpRequest || function () { try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} throw new Error("This browser does not support XMLHttpRequest."); }; +var XHR_HEADERS = { + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json, text/plain, */*", + "X-Requested-With": "XMLHttpRequest" +}; /** * @private @@ -72,11 +77,18 @@ function Browser(window, document, body, XHR, $log) { * @param {string} url Requested url * @param {?string} post Post data to send (null if nothing to post) * @param {function(number, string)} callback Function that will be called on response + * @param {object=} header additional HTTP headers to send with XHR. + * Standard headers are: + * <ul> + * <li><tt>Content-Type</tt>: <tt>application/x-www-form-urlencoded</tt></li> + * <li><tt>Accept</tt>: <tt>application/json, text/plain, */*</tt></li> + * <li><tt>X-Requested-With</tt>: <tt>XMLHttpRequest</tt></li> + * </ul> * * @description * Send ajax request */ - self.xhr = function(method, url, post, callback) { + self.xhr = function(method, url, post, callback, headers) { outstandingRequestCount ++; if (lowercase(method) == 'json') { var callbackId = "angular_" + Math.random() + '_' + (idCounter++); @@ -92,9 +104,9 @@ function Browser(window, document, body, XHR, $log) { } else { var xhr = new XHR(); xhr.open(method, url, true); - xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); - xhr.setRequestHeader("Accept", "application/json, text/plain, */*"); - xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + forEach(extend(XHR_HEADERS, headers || {}), function(value, key){ + if (value) xhr.setRequestHeader(key, value); + }); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { completeOutstandingRequest(callback, xhr.status || 200, xhr.responseText); diff --git a/src/angular-mocks.js b/src/angular-mocks.js index 762148fd..558e71e3 100644 --- a/src/angular-mocks.js +++ b/src/angular-mocks.js @@ -101,28 +101,27 @@ function MockBrowser() { }; - self.xhr = function(method, url, data, callback) { - if (angular.isFunction(data)) { - callback = data; - data = null; - } + self.xhr = function(method, url, data, callback, headers) { + headers = headers || {}; if (data && angular.isObject(data)) data = angular.toJson(data); if (data && angular.isString(data)) url += "|" + data; var expect = expectations[method] || {}; - var response = expect[url]; - if (!response) { - throw { - message: "Unexpected request for method '" + method + "' and url '" + url + "'.", - name: "Unexpected Request" - }; + var expectation = expect[url]; + if (!expectation) { + throw new Error("Unexpected request for method '" + method + "' and url '" + url + "'."); } requests.push(function(){ - callback(response.code, response.response); + forEach(expectation.headers, function(value, key){ + if (headers[key] !== value) { + throw new Error("Missing HTTP request header: " + key + ": " + value); + } + }); + callback(expectation.code, expectation.response); }); }; self.xhr.expectations = expectations; self.xhr.requests = requests; - self.xhr.expect = function(method, url, data) { + self.xhr.expect = function(method, url, data, headers) { if (data && angular.isObject(data)) data = angular.toJson(data); if (data && angular.isString(data)) url += "|" + data; var expect = expectations[method] || (expectations[method] = {}); @@ -132,7 +131,7 @@ function MockBrowser() { response = code; code = 200; } - expect[url] = {code:code, response:response}; + expect[url] = {code:code, response:response, headers: headers || {}}; } }; }; diff --git a/src/service/xhr.js b/src/service/xhr.js index b1b57a82..1de72b22 100644 --- a/src/service/xhr.js +++ b/src/service/xhr.js @@ -3,13 +3,71 @@ * @ngdoc service * @name angular.service.$xhr * @function - * @requires $browser - * @requires $xhr.error - * @requires $log + * @requires $browser $xhr delegates all XHR requests to the `$browser.xhr()`. A mock version + * of the $browser exists which allows setting expectaitions on XHR requests + * in your tests + * @requires $xhr.error $xhr delegates all non `2xx` response code to this service. + * @requires $log $xhr delegates all exceptions to `$log.error()`. + * @requires $updateView After a server response the view needs to be updated for data-binding to + * take effect. * * @description - * Generates an XHR request. The $xhr service adds error handling then delegates all requests to - * {@link angular.service.$browser $browser.xhr()}. + * Generates an XHR request. The $xhr service delegates all requests to + * {@link angular.service.$browser $browser.xhr()} and adds error handling and security features. + * While $xhr service provides nicer api than raw XmlHttpRequest, it is still considered a lower + * level api in angular. For a higher level abstraction that utilizes `$xhr`, please check out the + * {@link angular.service$resource $resource} service. + * + * # Error handling + * All XHR responses with response codes other then `2xx` are delegated to + * {@link angular.service.$xhr.error $xhr.error}. The `$xhr.error` can intercept the request + * and process it in application specific way, or resume normal execution by calling the + * request callback method. + * + * # Security Considerations + * When designing web applications your design needs to consider security threats from + * {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx + * JSON Vulnerability} and {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF}. + * Both server and the client must cooperate in order to eliminate these threats. Angular comes + * pre-configured with strategies that address these issues, but for this to work backend server + * cooperation is required. + * + * ## JSON Vulnerability Protection + * A {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx + * JSON Vulnerability} allows third party web-site to turn your JSON resource URL into + * {@link http://en.wikipedia.org/wiki/JSON#JSONP JSONP} request under some conditions. To + * counter this your server can prefix all JSON requests with following string `")]}',\n"`. + * Angular will automatically strip the prefix before processing it as JSON. + * + * For example if your server needs to return: + * <pre> + * ['one','two'] + * </pre> + * + * which is vulnerable to attack, your server can return: + * <pre> + * )]}', + * ['one','two'] + * </pre> + * + * angular will strip the prefix, before processing the JSON. + * + * + * ## Cross Site Request Forgery (XSRF) Protection + * {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} is a technique by which an + * unauthorized site can gain your user's private data. Angular provides following mechanism to + * counter XSRF. When performing XHR requests, the $xhr service reads a token from a cookie + * called `XSRF-TOKEN` and sets it as the HTTP header `X-XSRF-TOKEN`. Since only JavaScript that + * runs on your domain could read the cookie, your server can be assured that the XHR came from + * JavaScript running on your domain. + * + * To take advantage of this, your server needs to set a token in a JavaScript readable session + * cookie called `XSRF-TOKEN` on first HTTP GET request. On subsequent non-GET requests the server + * can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure that only + * JavaScript running on your domain could have read the token. The token must be unique for each + * user and must be verifiable by the server (to prevent the JavaScript making up its own tokens). + * We recommend that the token is a digest of your site's authentication cookie with + * {@link http://en.wikipedia.org/wiki/Rainbow_table salt for added security}. * * @param {string} method HTTP method to use. Valid values are: `GET`, `POST`, `PUT`, `DELETE`, and * `JSON`. `JSON` is a special case which causes a @@ -67,8 +125,7 @@ </doc:source> </doc:example> */ -angularServiceInject('$xhr', function($browser, $error, $log){ - var self = this; +angularServiceInject('$xhr', function($browser, $error, $log, $updateView){ return function(method, url, post, callback){ if (isFunction(post)) { callback = post; @@ -77,6 +134,7 @@ angularServiceInject('$xhr', function($browser, $error, $log){ if (post && isObject(post)) { post = toJson(post); } + $browser.xhr(method, url, post, function(code, response){ try { if (isString(response)) { @@ -95,8 +153,10 @@ angularServiceInject('$xhr', function($browser, $error, $log){ } catch (e) { $log.error(e); } finally { - self.$eval(); + $updateView(); } + }, { + 'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN'] }); }; -}, ['$browser', '$xhr.error', '$log']); +}, ['$browser', '$xhr.error', '$log', '$updateView']); |
