700 Software
700 Software

Reputation: 87853

Ensure URL is relative before navigating via JavaScript's location.replace()

I have a login page https://example.com/login#destination where destination is the target URL the user was trying to navigate to when they were required to log in.
(i.e. https://example.com/destination)

The JavaScript I was thinking about using was

function onSuccessfulLogin() {
    location.replace(location.hash.substring(1) || 'default')
}

How can I adjust onSuccessfulLogin to ensure the URL provided in the hash # portion is a relative URL, and not starting with javascript:, https:, // or any other absolute navigation scheme?

One thought is to evaluate the URL, and see if location.origin remains unchanged before navigating. Can you suggest how to do this, or a better approach?

Upvotes: 4

Views: 381

Answers (2)

From OWASP recommendations on Preventing Unvalidated Redirects and Forwards:

It is recommended that any such destination input be mapped to a value, rather than the actual URL or portion of the URL, and that server side code translate this value to the target URL.

So a safe approach would be mapping some keys to actual URLs:

// https://example.com/login#destination

var keyToUrl = {
  destination: 'https://example.com/destination',
  defaults: 'https://example.com/default'
};

function onSuccessfulLogin() {
  var hash = location.hash.substring(1);
  var url = keyToUrl[hash] || keyToUrl.defaults;

  location.replace(url);
}

You could also consider providing only path part of the URL and appending it with a hostname in the code:

// https://example.com/login#destination

function onSuccessfulLogin() {
  var path = location.hash.substring(1);
  var url = 'https://example.com/' + path;

  location.replace(url);
}

I would stick to the mapping though.

Upvotes: 1

T.J. Crowder
T.J. Crowder

Reputation: 1075209

That is a very good point about the XSS vulnerability.

I believe all protocols only use English alphabetic characters, so a regex like /^[a-z]+:/i would check for those. Alternately if we're feeling more inclusive, /^[^:\/?]+:/ allows anything but a / or ? followed by a :. Then we can combine that with /^\/\/ to test for a protocol-free URL, which gives us:

// Either
var rexIsProtocol = /(?:^[a-z]+:)|(?:^\/\/)/i;
// Or
var rexIsProtocol = /(?:^[^:\/?]+:)|(?:^\/\/)/i;

Then the test is like this:

var url = location.hash.substring(1).trim(); // trim to deal with whitespace
if (rexIsProtocol.test(url)) {
    // It starts with a protocol
} else {
    // It doesn't
}

That said, the only one I think you need to be particularly bothered by is the javascript: pseudo-protcol, so you might just test for that.

Upvotes: 0

Related Questions