CVE-2022-24794: URL Redirection to Untrusted Site ('Open Redirect') (CWE-601)
value: Express OpenID Connect is an Express JS middleware implementing sign on for Express web apps using OpenID Connect. Users of the `requiresAuth` middleware, either directly or through the default `authRequired` option, are vulnerable to an Open Redirect when the middleware is applied to a catch all route. If all routes under `example.com` are protected with the `requiresAuth` middleware, a visit to `http://example.com//google.com` will be redirected to `google.com` after login because the original url reported by the Express framework is not properly sanitized. This vulnerability affects versions prior to 2.7.2. Users are advised to upgrade. There are no known workarounds.
CWE description: A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.
CVSS v3 base score: 6.1
Published: 2022-03-31T23:15Z
Repository: https://github.com/auth0/express-openid-connect
Language: JavaScript
Files changed: lib/context.js, test/requiresAuth.tests.js
Commit date: 2022-03-29 12:11:14 +0100
Commit message: Merge pull request from GHSA-7p99-3798-f85c


Diff: diff --git a/lib/context.js b/lib/context.js
index c95e363..0949773 100644
--- a/lib/context.js
+++ b/lib/context.js
@@ -185,11 +185,12 @@ class ResponseContext {
     let returnTo = config.baseURL;
     if (options.returnTo) {
       returnTo = options.returnTo;
       debug('req.oidc.login() called with returnTo: %s', returnTo);
     } else if (req.method === 'GET' && req.originalUrl) {
-      returnTo = req.originalUrl;
+      // Collapse any leading slashes to a single slash to prevent Open Redirects
+      returnTo = req.originalUrl.replace(/^\/+/, '/');
       debug('req.oidc.login() without returnTo, using: %s', returnTo);
     }
 
     options = {
       authorizationParams: {},
diff --git a/test/requiresAuth.tests.js b/test/requiresAuth.tests.js
index 25c8bee..818fe91 100644
--- a/test/requiresAuth.tests.js
+++ b/test/requiresAuth.tests.js
@@ -352,6 +352,23 @@ describe('requiresAuth', () => {
     const response = await request({ baseUrl, url: '/protected' });
 
     assert.equal(response.statusCode, 401);
     sinon.assert.notCalled(checkSpy);
   });
+
+  it('should collapse leading slashes on returnTo', async () => {
+    server = await createServer(auth(defaultConfig));
+    const payloads = ['//google.com', '///google.com', '//google.com'];
+    for (const payload of payloads) {
+      const response = await request({ url: `${baseUrl}${payload}` });
+      const state = new URL(response.headers.location).searchParams.get(
+        'state'
+      );
+      const decoded = Buffer.from(state, 'base64');
+      const parsed = JSON.parse(decoded);
+
+      assert.equal(response.statusCode, 302);
+      assert.include(response.headers.location, 'https://op.example.com');
+      assert.equal(parsed.returnTo, '/google.com');
+    }
+  });
 });

Vulnerable code:       returnTo = req.originalUrl;
Fixed code:       // Collapse any leading slashes to a single slash to prevent Open Redirects
      returnTo = req.originalUrl.replace(/^\/+/, '/');

  it('should collapse leading slashes on returnTo', async () => {
    server = await createServer(auth(defaultConfig));
    const payloads = ['//google.com', '///google.com', '//google.com'];
    for (const payload of payloads) {
      const response = await request({ url: `${baseUrl}${payload}` });
      const state = new URL(response.headers.location).searchParams.get(
        'state'
      );
      const decoded = Buffer.from(state, 'base64');
      const parsed = JSON.parse(decoded);

      assert.equal(response.statusCode, 302);
      assert.include(response.headers.location, 'https://op.example.com');
      assert.equal(parsed.returnTo, '/google.com');
    }
  });