GitHub Advisoryadvisory · npm

GHSA-wrvh-rcmr-9qfc @strapi/plugin-users-permissions leaks 3rd party authentication tokens and authentication bypass

HIGH Published 2024-06-12 Modified 2024-06-12 Official page ↗
Aliases
CVE-2024-34065
Ecosystem
npm
Packages
@strapi/plugin-users-permissions
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N
Weaknesses
CWE-294, CWE-601
Reviewed
2024-06-12

Affected packages

• npm @strapi/plugin-users-permissions: < 4.24.2 → fixed in 4.24.2

Description

Summary By combining two vulnerabilities (an `Open Redirect` and `session token sent as URL query parameter`) in Strapi framework is its possible of an unauthenticated attacker to bypass authentication mechanisms and retrieve the 3rd party tokens. The attack requires user interaction (one click). Impact Unauthenticated attackers can leverage two vulnerabilities to obtain an 3rd party token and the bypass authentication of Strapi apps. Technical details Vulnerability 1: Open Redirect Description Open redirection vulnerabilities arise when an application incorporates user-controllable data into the target of a redirection in an unsafe way. An attacker can construct a URL within the application that causes a redirection to an arbitrary external domain. In the specific context of Strapi, this vulnerability allows the SSO token to be stolen, allowing an attacker to authenticate himself within the application. Remediation If possible, applications should avoid incorporating user-controllable data into redirection targets. In many cases, this behavior can be avoided in two ways: • Remove the redirection function from the application, and replace links to it with direct links to the relevant target URLs. • Maintain a server-side list of all URLs that are permitted for redirection. Instead of passing the target URL as a parameter to the redirector, pass an index into this list. If it is considered unavoidable for the redirection function to receive user-controllable input and incorporate this into the redirection target, one of the following measures should be used to minimize the risk of redirection attacks: • The application should use relative URLs in all of its redirects, and the redirection function should strictly validate that the URL received is a relative URL. • The application should use URLs relative to the web root for all of its redirects, and the redirection function should validate that the URL received starts with a slash character. It should then prepend http://yourdomainname.com to the URL before issuing the redirect. Example 1: Open Redirect in /api/connect/microsoft via `$_GET["callback"]` • Path: /api/connect/microsoft • Parameter: `$_GET["callback"]` Payload: ```plaintext https://google.fr/ ``` Final payload: ```plaintext https:///api/connect/microsoft?callback=https://google.fr/ ``` User clicks on the link: Look at the intercepted request in Burp and see the redirect to Microsoft: Microsoft check the cookies and redirects to the original domain (and route) but with different GET parameters. Then, the page redirects to the domain controlled by the attacker (and a token is added to controlled the URL): The domain originally specified (https://google.fr) as `$_GET["callback"]` parameter is present in the cookies. So \ is using the cookies (`koa.sess`) to redirect. `koa.sess` cookie: ```base64 eyJncmFudCI6eyJwcm92aWRlciI6Im1pY3Jvc29mdCIsImR5bmFtaWMiOnsiY2FsbGJhY2siOiJodHRwczovL2dvb2dsZS5mci8ifX0sIl9leHBpcmUiOjE3MDAyMzQyNDQyNjMsIl9tYXhBZ2UiOjg2NDAwMDAwfQ== ``` ```json {"grant":{"provider":"microsoft","dynamic":{"callback":"https://google.fr/"}},"_expire":1700234244263,"_maxAge":86400000} ``` The vulnerability seems to come from the application's core: File: packages/plugins/users-permissions/server/controllers/auth.js ```js 'use strict'; /** • Auth.js controller * • @description: A set of functions called "actions" for managing `Auth`. */ /* eslint-disable no-useless-escape */ const crypto = require('crypto'); const _ = require('lodash'); const { concat, compact, isArray } = require('lodash/fp'); const utils = require('@strapi/utils'); const { contentTypes: { getNonWritableAttributes }, } = require('@strapi/utils'); const { getService } = require('../utils'); const { validateCallbackBody, validateRegisterBody, validateSendEmailConfirmationBody, validateForgotPasswordBody, validateResetPasswordBody, validateEmailConfirmationBody, validateChangePasswordBody, } = require('./validation/auth'); const { getAbsoluteAdminUrl, getAbsoluteServerUrl, sanitize } = utils; const { ApplicationError, ValidationError, ForbiddenError } = utils.errors; const sanitizeUser = (user, ctx) => { const { auth } = ctx.state; const userSchema = strapi.getModel('plugin::users-permissions.user'); return sanitize.contentAPI.output(user, userSchema, { auth }); }; module.exports = { async callback(ctx) { const provider = ctx.params.provider || 'local'; const params = ctx.request.body; const store = strapi.store({ type: 'plugin', name: 'users-permissions' }); const grantSettings = await store.get({ key: 'grant' }); const grantProvider = provider === 'local' ? 'email' : provider; if (!_.get(grantSettings, [grantProvider, 'enabled'])) { throw new ApplicationError('This provider is disabled'); } if (provider === 'local') { await validateCallbackBody(params); const { identifier } = params; // Check if the user exists. const user = await strapi.query('plugin::users-permissions.user').findOne({ where: { provider, $or: [{ email: identifier.toLowerCase() }, { username: identifier }], }, }); if (!user) { throw new ValidationError('Invalid identifier or password'); } if (!user.password) { throw new ValidationError('Invalid identifier or password'); } const validPassword = await getService('user').validatePassword( params.password, user.password ); if (!validPassword) { throw new ValidationError('Invalid identifier or password'); } const advancedSettings = await store.get({ key: 'advanced' }); const requiresConfirmation = _.get(advancedSettings, 'email_confirmation'); if (requiresConfirmation && user.confirmed !== true) { throw new ApplicationError('Your account email is not confirmed'); } if (user.blocked === true) { throw new ApplicationError('Your account has been blocked by an administrator'); } return ctx.send({ jwt: getService('jwt').issue({ id: user.id }), user: await sanitizeUser(user, ctx), }); } // Connect the user with the third-party provider. try { const user = await getService('providers').connect(provider, ctx.query); if (user.blocked) { throw new ForbiddenError('Your account has been blocked by an administrator'); } return ctx.send({ jwt: getService('jwt').issue({ id: user.id }), user: await sanitizeUser(user, ctx), }); } catch (error) { throw new ApplicationError(error.message); } }, //... async connect(ctx, next) { const grant = require('grant-koa'); const providers = await strapi .store({ type: 'plugin', name: 'users-permissions', key: 'grant' }) .get(); const apiPrefix = strapi.config.get('api.rest.prefix'); const grantConfig = { defaults: { prefix: `${apiPrefix}/connect`, }, ...providers, }; const [requestPath] = ctx.request.url.split('?'); const provider = requestPath.split('/connect/')[1].split('/')[0]; if (!_.get(grantConfig[provider], 'enabled')) { throw new ApplicationError('This provider is disabled'); } if (!strapi.config.server.url.startsWith('…

Related

Alias · CVE (1)

  • CVE-2024-34065 Strapi is an open-source content management system. vulnerability

Weakness · CWE (2)

  • CWE-294 Authentication Bypass by Capture-replay weakness
  • CWE-601 URL Redirection to Untrusted Site weakness

References