Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Xsrf #303

Closed
wants to merge 6 commits into from
Closed

Xsrf #303

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
<a name="0.9.13"><a/>
# <angular/> 0.9.13 curdling-stare (in-progress) #

### Bug Fixes
- Fixed cookies which contained unescaped '=' would not show up in cookie service.
- Consider all 2xx responses as OK, not just 200
- Remove the script tag after successful JSONP request



### Breaking changes
- Changed the $browser.xhr parameter post from optional to required. Since everyone should be
using the $xhr instead of $browser.xhr, this should not break anyone. If you do use $browser.xhr
then just add null for the post value argument.
- Added XSRF prevention logic to $xhr service


<a name="0.9.12"><a/>
Expand Down
11 changes: 9 additions & 2 deletions docs/spec/ngdocSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,16 @@ describe('ngdoc', function(){

describe('@requires', function() {
it('should parse more @requires tag into array', function() {
var doc = new Doc('@requires $service\n@requires $another');
var doc = new Doc('@requires $service for \n`A`\n@requires $another for `B`');
doc.ngdoc = 'service';
doc.parse();
expect(doc.requires).toEqual(['$service', '$another']);
expect(doc.requires).toEqual([
{name:'$service', text:'<p>for \n<code>A</code></p>'},
{name:'$another', text:'<p>for <code>B</code></p>'}]);
expect(doc.html()).toContain('<a href="#!angular.service.$service">$service</a>');
expect(doc.html()).toContain('<a href="#!angular.service.$another">$another</a>');
expect(doc.html()).toContain('<p>for \n<code>A</code></p>');
expect(doc.html()).toContain('<p>for <code>B</code></p>');
});
});

Expand Down
29 changes: 14 additions & 15 deletions docs/src/ngdoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,11 @@ Doc.prototype = {
description: self.markdown(text.replace(match[0], match[2]))
};
} else if(atName == 'requires') {
self.requires.push(text);
var match = text.match(/^([^\s]*)\s*([\S\s]*)/);
self.requires.push({
name: match[1],
text: self.markdown(match[2])
});
} else if(atName == 'property') {
var match = text.match(/^{(\S+)}\s+(\S+)(\s+(.*))?/);
if (!match) {
Expand Down Expand Up @@ -185,6 +189,15 @@ Doc.prototype = {
'This page is currently being revised. It might be incomplete or contain inaccuracies.');
notice('deprecated', 'Deprecated API', self.deprecated);

if (self.ngdoc != 'overview')
dom.h('Description', self.description, dom.html);
dom.h('Dependencies', self.requires, function(require){
dom.tag('code', function(){
dom.tag('a', {href:"#!angular.service." + require.name}, require.name);
});
dom.html(require.text);
});

(self['html_usage_' + self.ngdoc] || function(){
throw new Error("Don't know how to format @ngdoc: " + self.ngdoc);
}).call(self, dom);
Expand Down Expand Up @@ -251,8 +264,6 @@ Doc.prototype = {

html_usage_function: function(dom){
var self = this;
dom.h('Description', self.description, dom.html);
dom.h('Dependencies', self.requires);
dom.h('Usage', function(){
dom.code(function(){
dom.text(self.name);
Expand All @@ -269,8 +280,6 @@ Doc.prototype = {

html_usage_directive: function(dom){
var self = this;
dom.h('Description', self.description, dom.html);
dom.h('Dependencies', self.requires);
dom.h('Usage', function(){
dom.tag('pre', {'class':"brush: js; html-script: true;"}, function(){
dom.text('<' + self.element + ' ');
Expand All @@ -287,8 +296,6 @@ Doc.prototype = {

html_usage_filter: function(dom){
var self = this;
dom.h('Description', self.description, dom.html);
dom.h('Dependencies', self.requires);
dom.h('Usage', function(){
dom.h('In HTML Template Binding', function(){
dom.tag('code', function(){
Expand Down Expand Up @@ -319,8 +326,6 @@ Doc.prototype = {

html_usage_formatter: function(dom){
var self = this;
dom.h('Description', self.description, dom.html);
dom.h('Dependencies', self.requires);
dom.h('Usage', function(){
dom.h('In HTML Template Binding', function(){
dom.code(function(){
Expand Down Expand Up @@ -359,8 +364,6 @@ Doc.prototype = {

html_usage_validator: function(dom){
var self = this;
dom.h('Description', self.description, dom.html);
dom.h('Dependencies', self.requires);
dom.h('Usage', function(){
dom.h('In HTML Template Binding', function(){
dom.code(function(){
Expand Down Expand Up @@ -389,8 +392,6 @@ Doc.prototype = {

html_usage_widget: function(dom){
var self = this;
dom.h('Description', self.description, dom.html);
dom.h('Dependencies', self.requires);
dom.h('Usage', function(){
dom.h('In HTML Template Binding', function(){
dom.code(function(){
Expand Down Expand Up @@ -435,8 +436,6 @@ Doc.prototype = {

html_usage_service: function(dom){
var self = this;
dom.h('Description', this.description, dom.html);
dom.h('Dependencies', this.requires);

if (this.param.length) {
dom.h('Usage', function(){
Expand Down
44 changes: 26 additions & 18 deletions src/Browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -70,35 +75,37 @@ function Browser(window, document, body, XHR, $log) {
*
* @param {string} method Requested method (get|post|put|delete|head|json)
* @param {string} url Requested url
* @param {string=} post Post data to send
* @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, &#42;/&#42;</tt></li>
* <li><tt>X-Requested-With</tt>: <tt>XMLHttpRequest</tt></li>
* </ul>
*
* @description
* Send ajax request
*/
self.xhr = function(method, url, post, callback) {
if (isFunction(post)) {
callback = post;
post = _null;
}
self.xhr = function(method, url, post, callback, headers) {
outstandingRequestCount ++;
if (lowercase(method) == 'json') {
var callbackId = "angular_" + Math.random() + '_' + (idCounter++);
callbackId = callbackId.replace(/\d\./, '');
var script = document[0].createElement('script');
script.type = 'text/javascript';
script.src = url.replace('JSON_CALLBACK', callbackId);
var callbackId = ("angular_" + Math.random() + '_' + (idCounter++)).replace(/\d\./, '');
var script = jqLite('<script>')
.attr({type: 'text/javascript', src: url.replace('JSON_CALLBACK', callbackId)});
window[callbackId] = function(data){
window[callbackId] = _undefined;
script.remove();
completeOutstandingRequest(callback, 200, data);
};
body.append(script);
} 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);
Expand Down Expand Up @@ -280,7 +287,7 @@ function Browser(window, document, body, XHR, $log) {
* @returns {Object} Hash of all cookies (if called without any parameter)
*/
self.cookies = function (name, value) {
var cookieLength, cookieArray, i, keyValue;
var cookieLength, cookieArray, cookie, i, keyValue, index;

if (name) {
if (value === _undefined) {
Expand All @@ -307,9 +314,10 @@ function Browser(window, document, body, XHR, $log) {
lastCookies = {};

for (i = 0; i < cookieArray.length; i++) {
keyValue = cookieArray[i].split("=");
if (keyValue.length === 2) { //ignore nameless cookies
lastCookies[unescape(keyValue[0])] = unescape(keyValue[1]);
cookie = cookieArray[i];
index = cookie.indexOf('=');
if (index >= 0) { //ignore nameless cookies
lastCookies[unescape(cookie.substring(0, index))] = unescape(cookie.substring(index + 1));
}
}
}
Expand Down
27 changes: 13 additions & 14 deletions src/angular-mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {});
Expand All @@ -132,7 +131,7 @@ function MockBrowser() {
response = code;
code = 200;
}
expect[url] = {code:code, response:response};
expect[url] = {code:code, response:response, headers: headers || {}};
}
};
};
Expand Down
78 changes: 68 additions & 10 deletions src/service/xhr.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,69 @@
* @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.
* The $xhr service is the underlying service for all XHR activity in angular (namely
* {@link angular.service.$resource $resource}).
*
* # 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 it needs help from your server.
*
* ## JSON Vulnerability Protection
* {@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 the token from a cookie
* called `XSRF-TOKEN` and sets it as 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
Expand Down Expand Up @@ -67,8 +123,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;
Expand All @@ -77,6 +132,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)) {
Expand All @@ -85,7 +141,7 @@ angularServiceInject('$xhr', function($browser, $error, $log){
response = fromJson(response, true);
}
}
if (code == 200) {
if (200 <= code && code < 300) {
callback(code, response);
} else {
$error(
Expand All @@ -95,8 +151,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']);
Loading