CVE-2021-3837 (published 2022-01-03T15:15Z)

openwhyd is vulnerable to Improper Authorization

Severity: MEDIUM
CVSS v2 base score: 4.3 (AV:N/AC:M/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 6.1, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: NVD-CWE-Other
cwe_name: Other
description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/openwhyd/openwhyd
commit_details: repo_url: https://github.com/openwhyd/openwhyd
author: Adrien Joly
author_date: 2021-12-26 21:17:12+01:00
msg: fix(security): Make login redirects more secure (#519)

Addresses:
- [Improper Authorization vulnerability found in openwhyd](https://huntr.dev/bounties/d66f90d6-1b5f-440d-8be6-cdffc9d4587e/)
- [Open Redirect vulnerability found in openwhyd](https://huntr.dev/bounties/8852bb22-7312-4af5-b9c2-507a68e37a6a/)
- [Cross-site Scripting (XSS) - Reflected vulnerability found in openwhyd](https://huntr.dev/bounties/84a14deb-5dd8-4cc0-810f-d0987566f845/)

Tests:
* test: /login should allow redirect to /stream
* test: /login should NOT allow javascript in redirect URL
* test: /login should should NOT allow redirect to other domain
* test: /login should NOT allow script element in redirect URL
repository: repo_url: https://github.com/openwhyd/openwhyd
repo_name: openwhyd/openwhyd
description: 💎 Like Pinterest, but for Music 🎧
repo_language: JavaScript
owner: openwhyd
file_changes: filename: login.js
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,8 @@
 /**
  * login controller, to authenticate users
  */
+const snip = require('../../snip.js');
+const config = require('../../models/config.js');
 var emailModel = require('../../models/email.js');
 var userModel = require('../../models/user.js');
 var notifEmails = require('../../models/notifEmails.js');
@@ -60,7 +62,12 @@ exports.handleRequest = function (request, form, response, ignorePassword) {
     userModel[form.email.indexOf('@') > -1 ? 'fetchByEmail' : 'fetchByHandle'](
       form.email,
       function (dbUser) {
-        if (!dbUser) {
+        if (
+          form.redirect &&
+          snip.getSafeOpenwhydURL(form.redirect, config.urlPrefix) === false
+        ) {
+          form.error = 'Unsafe redirect target';
+        } else if (!dbUser) {
           form.error = "Are you sure? We don't recognize your email address!";
         } else if (form.action == 'forgot') {
           notifEmails.sendPasswordReset(dbUser._id, dbUser.pwd, form.redirect);

programming_language: JavaScript
filename: logging.js
change_type: ModificationType.MODIFY
diff: @@ -261,9 +261,8 @@ http.ServerResponse.prototype.redirect = function (url) {
 };
 
 http.ServerResponse.prototype.safeRedirect = function (url) {
-  const fullURL = new URL(url, config.urlPrefix);
-  if (`${fullURL.protocol}//${fullURL.host}` !== config.urlPrefix)
-    return this.forbidden();
+  const safeURL = snip.getSafeOpenwhydURL(url, config.urlPrefix);
+  if (safeURL === false) return this.forbidden();
   this.redirect(url);
 };
 

programming_language: JavaScript
filename: snip.js
change_type: ModificationType.MODIFY
diff: @@ -109,6 +109,13 @@ exports.sanitizeJsStringInHtml = function (str) {
   return exports.htmlEntities(exports.addSlashes(str || ''));
 };
 
+exports.getSafeOpenwhydURL = function (url, safeUrlPrefix) {
+  if (typeof url !== 'string' || url.includes('<script')) return false;
+  const fullURL = new URL(url, safeUrlPrefix);
+  if (`${fullURL.protocol}//${fullURL.host}` !== safeUrlPrefix) return false;
+  else return fullURL;
+};
+
 var timeScales = [
   { 'minute(s)': 60 },
   { 'hour(s)': 60 },

programming_language: JavaScript
filename: security.api.tests.js
change_type: ModificationType.MODIFY
diff: @@ -12,7 +12,65 @@ const loginAs = promisify(apiClient.loginAs);
 before(cleanup);
 
 describe('security', () => {
-  describe('Open Redirect', () => {
+  describe('Open Redirect from /login', () => {
+    it('should allow redirect to /stream', async () => {
+      const target = `/stream`;
+      const { response } = await postRaw(null, `/login`, {
+        action: 'login',
+        email: ADMIN_USER.email,
+        md5: ADMIN_USER.md5,
+        redirect: target,
+      });
+      assert(
+        response.body.includes(`window.location.href="${target}"`) === true,
+        `page body should include redirect to ${target}`
+      );
+    });
+
+    it('should NOT allow redirect to other domain', async () => {
+      const target = `https://google.com`;
+      const { response } = await postRaw(null, `/login`, {
+        action: 'login',
+        email: ADMIN_USER.email,
+        md5: ADMIN_USER.md5,
+        redirect: target,
+      });
+      assert(
+        response.body.includes(`window.location.href="${target}"`) === false,
+        `page body should NOT include redirect to ${target}`
+      );
+    });
+
+    it('should NOT allow javascript in redirect URL', async () => {
+      const target = `javascript:alert()`;
+      const { response } = await postRaw(null, `/login`, {
+        action: 'login',
+        email: ADMIN_USER.email,
+        md5: ADMIN_USER.md5,
+        redirect: target,
+      });
+      assert(
+        response.body.includes(`window.location.href="${target}"`) === false,
+        `page body should NOT include redirect to ${target}`
+      );
+    });
+
+    it('should NOT allow script element in redirect URL', async () => {
+      const target = `<script>alert(document.cookie)</script>`;
+      const { response } = await postRaw(null, `/login`, {
+        action: 'login',
+        email: ADMIN_USER.email,
+        md5: ADMIN_USER.md5,
+        redirect: target,
+      });
+      assert(
+        response.body.includes(`window.location.href="${target}"`) === false,
+        `page body should NOT include redirect to ${target}`
+      );
+    });
+  });
+
+  describe('Open Redirect from /consent', () => {
     it('should allow redirect to /stream', async () => {
       const target = `/stream`;
       const { jar } = await loginAs(ADMIN_USER);

programming_language: TypeScript