Permalink
Browse files

Updated Router to enforce same-origin constraints.

Router now enforces same-origin constraints for calls to `save()` and
`replace()`; this is the same constraint that is imposed on the HTML5
history API's `pushState()` method. Additionally, this check is also
applied in Router's `hasRoute()` method.

Attempting to `save()` or `replace()` a URL that does not have the
same-origin as the current URL will cause an error to be logged and no
changes to the URL or history will be made.

Unit tests have been added to test the enforcement of the same-origin
constraints.
  • Loading branch information...
1 parent 83f398b commit ad6fc03d183ed463ecc5483fbc4040ba5479fef3 @ericf ericf committed Nov 28, 2011
Showing with 183 additions and 31 deletions.
  1. +75 −20 src/app/js/router.js
  2. +108 −11 src/app/tests/app-test.js
View
@@ -6,12 +6,12 @@ Provides URL-based routing using HTML5 `pushState()` or the location hash.
**/
var HistoryHash = Y.HistoryHash,
- Lang = Y.Lang,
QS = Y.QueryString,
YArray = Y.Array,
win = Y.config.win,
location = win.location,
+ origin = location.origin || (location.protocol + '//' + location.host),
// We have to queue up pushState calls to avoid race conditions, since the
// popstate event doesn't actually provide any info on what URL it's
@@ -109,7 +109,7 @@ Y.Router = Y.extend(Router, Y.Base, {
@type RegExp
@protected
**/
- _regexPathParam: /([:*])([\w-]+)/g,
+ _regexPathParam: /([:*])([\w\-]+)/g,
/**
Regex that matches and captures the query portion of a URL, minus the
@@ -122,15 +122,15 @@ Y.Router = Y.extend(Router, Y.Base, {
_regexUrlQuery: /\?([^#]*).*$/,
/**
- Regex that matches everything before the path portion of an HTTP or HTTPS
- URL. This will be used to strip this part of the URL from a string when we
+ Regex that matches everything before the path portion of a URL (the origin).
+ This will be used to strip this part of the URL from a string when we
only want the path.
- @property _regexUrlStrip
+ @property _regexUrlOrigin
@type RegExp
@protected
**/
- _regexUrlStrip: /^https?:\/\/[^\/]*/i,
+ _regexUrlOrigin: /^(?:[^\/#?:]+:\/\/|\/\/)[^\/]*/,
// -- Lifecycle Methods ----------------------------------------------------
initializer: function (config) {
@@ -218,12 +218,20 @@ Y.Router = Y.extend(Router, Y.Base, {
Returns `true` if this router has at least one route that matches the
specified URL, `false` otherwise.
+ This method enforces the same-origin security constraint on the specified
+ `url`; any URL which is not from the same origin as the current URL will
+ always return `false`.
+
@method hasRoute
@param {String} url URL to match.
@return {Boolean} `true` if there's at least one matching route, `false`
otherwise.
**/
hasRoute: function (url) {
+ if (!this._hasSameOrigin(url)) {
+ return false;
+ }
+
return !!this.match(this.removeRoot(url)).length;
},
@@ -273,7 +281,7 @@ Y.Router = Y.extend(Router, Y.Base, {
// Strip out the non-path part of the URL, if any (e.g.
// "http://foo.com"), so that we're left with just the path.
- url = url.replace(this._regexUrlStrip, '');
+ url = url.replace(this._regexUrlOrigin, '');
if (root && url.indexOf(root) === 0) {
url = url.substring(root.length);
@@ -306,9 +314,9 @@ Y.Router = Y.extend(Router, Y.Base, {
// New URL: http://example.com/
@method replace
- @param {String} [url] URL to set. Should be a relative URL. If this
- router's `root` property is set, this URL must be relative to the
- root URL. If no URL is specified, the page's current URL will be used.
+ @param {String} [url] URL to set. This URL needs to be of the same origin as
+ the current URL. This can be a URL relative to the router's `root`
+ attribute. If no URL is specified, the page's current URL will be used.
@chainable
@see save()
**/
@@ -422,9 +430,9 @@ Y.Router = Y.extend(Router, Y.Base, {
// New URL: http://example.com/
@method save
- @param {String} [url] URL to set. Should be a relative URL. If this
- router's `root` property is set, this URL must be relative to the
- root URL. If no URL is specified, the page's current URL will be used.
+ @param {String} [url] URL to set. This URL needs to be of the same origin as
+ the current URL. This can be a URL relative to the router's `root`
+ attribute. If no URL is specified, the page's current URL will be used.
@chainable
@see replace()
**/
@@ -574,15 +582,29 @@ Y.Router = Y.extend(Router, Y.Base, {
},
/**
- Gets the current route path.
+ Gets the location origin (i.e., protocol, host, and port) as a URL.
+
+ @example
+ http://example.com
+
+ @method _getOrigin
+ @return {String} Location origin (i.e., protocol, host, and port).
+ @protected
+ **/
+ _getOrigin: function () {
+ return origin;
+ },
+
+ /**
+ Gets the current route path, relative to the `root` (if any).
@method _getPath
@return {String} Current route path.
@protected
**/
_getPath: function () {
- return (!this._html5 && this._getHashPath()) ||
- this.removeRoot(location.pathname);
+ var path = (!this._html5 && this._getHashPath()) || location.pathname;
+ return this.removeRoot(path);
},
/**
@@ -660,8 +682,8 @@ Y.Router = Y.extend(Router, Y.Base, {
@protected
**/
_getResponse: function (req) {
- // For backcompat, the response object is a function that calls `next()`
- // on the request object and returns the result.
+ // For backwards compatibility, the response object is a function that
+ // calls `next()` on the request object and returns the result.
var res = function () {
return req.next.apply(this, arguments);
};
@@ -693,15 +715,38 @@ Y.Router = Y.extend(Router, Y.Base, {
},
/**
+ Returns `true` when the specified `url` is from the same origin as the
+ current URL; i.e., the protocol, host, and port of the URLs are the same.
+
+ All host or path relative URLs are of the same origin. A scheme-relative URL
+ is first prefixed with the current scheme before being evaluated.
+
+ @method _hasSameOrigin
+ @param {String} url URL to compare origin with the current URL.
+ @return {Boolean} Whether the URL has the same origin of the current URL.
+ @protected
+ **/
+ _hasSameOrigin: function (url) {
+ var origin = ((url && url.match(this._regexUrlOrigin)) || [])[0];
+
+ // Prepend current scheme to scheme-relative URLs.
+ if (origin && origin.indexOf('//') === 0) {
+ origin = location.protocol + origin;
+ }
+
+ return !origin || origin === this._getOrigin();
+ },
+
+ /**
Joins the `root` URL to the specified _url_, normalizing leading/trailing
`/` characters.
@example
- router.root = '/foo'
+ router.set('root', '/foo');
router._joinURL('bar'); // => '/foo/bar'
router._joinURL('/bar'); // => '/foo/bar'
- router.root = '/foo/'
+ router.set('root', '/foo/');
router._joinURL('bar'); // => '/foo/bar'
router._joinURL('/bar'); // => '/foo/bar'
@@ -800,6 +845,10 @@ Y.Router = Y.extend(Router, Y.Base, {
/**
Saves a history entry using either `pushState()` or the location hash.
+ This method enforces the same-origin security constraint; attempting to save
+ a `url` that is not from the same origin as the current URL will result in
+ an error.
+
@method _save
@param {String} [url] URL for the history entry.
@param {Boolean} [replace=false] If `true`, the current history entry will
@@ -810,6 +859,12 @@ Y.Router = Y.extend(Router, Y.Base, {
_save: function (url, replace) {
var urlIsString = typeof url === 'string';
+ // Perform same-origin check on the specified URL.
+ if (urlIsString && !this._hasSameOrigin(url)) {
+ Y.error('Security error: The new URL must be of the same origin as the current URL.');
+ return this;
+ }
+
// Force _ready to true to ensure that the history change is handled
// even if _save is called before the `ready` event fires.
this._ready = true;
Oops, something went wrong.

0 comments on commit ad6fc03

Please sign in to comment.