Skip to content

Commit

Permalink
Updated Router to enforce same-origin constraints.
Browse files Browse the repository at this point in the history
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
ericf committed Nov 28, 2011
1 parent 83f398b commit ad6fc03
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 31 deletions.
95 changes: 75 additions & 20 deletions src/app/js/router.js
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
},

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()
**/
Expand Down Expand Up @@ -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()
**/
Expand Down Expand Up @@ -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);
},

/**
Expand Down Expand Up @@ -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);
};
Expand Down Expand Up @@ -692,16 +714,39 @@ Y.Router = Y.extend(Router, Y.Base, {
return location.toString();
},

/**
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'
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down

0 comments on commit ad6fc03

Please sign in to comment.