CVE-2026-3531

No items found.
Affects
OpenID Connect
in
Drupal 7
No items found.
Versions
>=7.1.0 <=7.1.3
Exclamation circle icon
Patch Available

This Vulnerability has been fixed in the Never-Ending Support (NES) version offered by HeroDevs.

Overview

Drupal is an open-source content management system known for its flexibility, robust features, and strong community support. Organizations of all sizes use it to build and manage dynamic websites and web applications.

A user with an active session at the Identity Provider can revisit the OpenID Connect login flow and be silently reauthenticated without re-entering credentials. If Drupal denies access during first-time login or a custom access check fails, that existing IdP session may still be reused on a later attempt, allowing unauthorized access, particularly on shared devices.

In general, an authorization bypass vulnerability occurs when a system fails to properly enforce access controls, allowing an attacker to gain access to resources or perform actions that they are not supposed to have permission to access. This can happen due to flaws in the design or implementation of the authentication or authorization mechanisms.

  • Authorization bypass is a critical security risk because it can lead to severe consequences, including:
  • Unauthorized data access
  • Large-scale data breaches
  • Account takeovers, and
  • System compromise.

This issue affects OpenID Connect versions 7.1.0 through 7.1.3.

Details

Module Info

Vulnerability Info

This medium-severity vulnerability affects OpenID Connect versions 7.1.0 through 7.1.3 for Drupal 7.

A user successfully authenticates with the external OpenID Connect Identity Provider but Drupal does not complete the local login process. This can happen when custom authorization logic denies access, account provisioning fails, or a server-side error interrupts the request. Even though Drupal blocks that attempt, the authenticated session at the Identity Provider remains active in the browser. 

On a later visit to the login endpoint, the module can silently reuse that existing IdP session and continue the authentication flow without forcing the user to enter credentials again.

In practice, this means a different person using the same browser session, especially on a shared device, may inherit the prior user’s authenticated IdP state.

If the login flow does not force re-authentication, for example because no prompt=login is sent and the IdP permits silent reuse of an existing session, Drupal may accept the reused existing session and grant access that should have been denied or revalidated.

The result is an authorization bypass caused by unintended reuse of a persistent external authentication session.

Steps To Reproduce

1. Create a Drupal 7 installation and install a vulnerable version of the OpenID Connect module, such as 7.1.3. 

2. Enable the OpenID Connect module.

3. Prepare the mock OpenID Connect provider; write this file into /tmp/mock_oidc.php:


<?php

session_name('MOCKOIDC');
session_start();

function mock_b64url($value) {
  return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
}

function mock_hidden($name, $value) {
  return '<input type="hidden" name="' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '" value="' . htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . '">';
}

function mock_redirect_with_code($redirect_uri, $state) {
  $separator = strpos($redirect_uri, '?') === FALSE ? '?' : '&';
  header('Location: ' . $redirect_uri . $separator . 'code=mock-code&state=' . rawurlencode($state));
  exit;
}

function mock_render_login_form($redirect_uri, $state, $prompt) {
  header('Content-Type: text/html; charset=UTF-8');
  ?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Mock OIDC Login</title>
  <style>
    body { font-family: sans-serif; margin: 2rem; max-width: 48rem; }
    code { background: #f4f4f4; padding: 0.1rem 0.3rem; }
    .box { border: 1px solid #ccc; padding: 1rem 1.25rem; border-radius: 0.5rem; }
    .meta { color: #444; font-size: 0.95rem; }
    button { padding: 0.6rem 1rem; }
  </style>
</head>
<body>
  <div class="box">
    <h1>Mock OpenID Connect Provider</h1>
    <p>This page simulates a fresh IdP login challenge.</p>
    <p class="meta">Current session: <strong><?php print empty($_SESSION['mock_authenticated']) ? 'not authenticated' : 'authenticated'; ?></strong></p>
    <p class="meta">Received <code>prompt</code>: <strong><?php print htmlspecialchars($prompt, ENT_QUOTES, 'UTF-8'); ?></strong></p>
    <form method="post" action="/authorize">
      <?php print mock_hidden('redirect_uri', $redirect_uri); ?>
      <?php print mock_hidden('state', $state); ?>
      <?php print mock_hidden('prompt', $prompt); ?>
      <button type="submit" name="mock_login" value="1">Log in as Mock User</button>
    </form>
  </div>
</body>
</html>
<?php
  exit;
}

$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

if ($path === '/authorize') {
  $is_post = $_SERVER['REQUEST_METHOD'] === 'POST';
  $request = $is_post ? $_POST : $_GET;
  $redirect_uri = isset($request['redirect_uri']) ? $request['redirect_uri'] : '';
  $state = isset($request['state']) ? $request['state'] : '';
  $prompt = isset($request['prompt']) ? trim($request['prompt']) : '';

  if ($redirect_uri === '') {
    http_response_code(400);
    header('Content-Type: text/plain');
    echo "missing redirect_uri\n";
    exit;
  }

  if ($is_post && !empty($_POST['mock_login'])) {
    $_SESSION['mock_authenticated'] = TRUE;
    $_SESSION['mock_user'] = array(
      'sub' => 'mock-user-1',
      'email' => 'mock@example.com',
      'name' => 'Mock User',
    );
    mock_redirect_with_code($redirect_uri, $state);
  }

  $force_login = strpos(' ' . $prompt . ' ', ' login ') !== FALSE;
  $has_session = !empty($_SESSION['mock_authenticated']);

  if ($has_session && !$force_login) {
    mock_redirect_with_code($redirect_uri, $state);
  }

  mock_render_login_form($redirect_uri, $state, $prompt);
}

if ($path === '/token') {
  header('Content-Type: application/json');
  $claims = array(
    'sub' => 'mock-user-1',
    'email' => 'mock@example.com',
  );
  $id_token = mock_b64url(json_encode(array('alg' => 'none', 'typ' => 'JWT'))) . '.'
    . mock_b64url(json_encode($claims)) . '.x';

  echo json_encode(array(
    'access_token' => 'mock-access-token',
    'id_token' => $id_token,
    'expires_in' => 3600,
    'token_type' => 'Bearer',
  ));
  exit;
}

if ($path === '/UserInfo') {
  header('Content-Type: application/json');
  $user = !empty($_SESSION['mock_user']) ? $_SESSION['mock_user'] : array(
    'sub' => 'mock-user-1',
    'email' => 'mock@example.com',
    'name' => 'Mock User',
  );
  echo json_encode($user);
  exit;
}

if ($path === '/logout') {
  $_SESSION = array();
  if (ini_get('session.use_cookies')) {
    $params = session_get_cookie_params();
    setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
  }
  session_destroy();
  header('Content-Type: text/plain');
  echo "logged out\n";
  exit;
}

if ($path === '/') {
  header('Content-Type: text/html; charset=UTF-8');
  ?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Mock OIDC Provider</title>
</head>
<body>
  <h1>Mock OIDC Provider</h1>
  <p>Session state: <strong><?php print empty($_SESSION['mock_authenticated']) ? 'not authenticated' : 'authenticated'; ?></strong></p>
  <ul>
    <li><a href="/logout">Log out of mock IdP</a></li>
    <li>Authorize endpoint: <code>/authorize</code></li>
    <li>Token endpoint: <code>/token</code></li>
    <li>UserInfo endpoint: <code>/UserInfo</code></li>
  </ul>
</body>
</html>
<?php
  exit;
}

http_response_code(404);
header('Content-Type: text/plain');
echo "not found\n";

4. Start the mock IdP.

php -S 0.0.0.0:8008 /tmp/mock_oidc.php

5. At admin/config/services/openid-connect, configure Drupal’s Generic OpenID Connect client to use the mock.

If using DDEV, select endpoints reachable from both DDEV and your browser.

Click Save configuration.

6. Create a custom module with a temporary Drupal denial hook so that Drupal rejects the user after IdP authentication. Enable the module.

7. Create a custom module with a temporary Drupal denial hook so that Drupal rejects the user after IdP authentication. Enable the module.

name = Mock OIDC Deny
description = Temporarily denies OpenID Connect authorization.
core = 7.x
package = Testing
dependencies[] = openid_connect

----------
<?php

function mock_oidc_deny_openid_connect_pre_authorize($tokens, $account, $userinfo, $client_name) {
  return FALSE;
}

8. Clear Drupal caches.

drush cc all

9. Reset the mock IdP session (created by the php file in /tmp) in the same browser you will use for testing.Open:

http://127.0.0.1:8008/logout

10. Start the OpenID Connect login flow from Drupal, typically at /user/login, by clicking the Generic button. You will see a page with Mock OpenID Connect Provider as the title.

11. At the mock IdP, log in by clicking the button on the /authorize page.
The mock IdP now has an authenticated browser session.

12. Drupal receives the callback and denies access via the temporary hook.
This simulates the advisory condition: IdP login succeeded, but Drupal rejected the user. You should see "Logging in with Generic could not be completed due to an error."

13. Without closing the browser or logging out of the mock IdP, start the Drupal OpenID Connect login flow again by clicking the Generic button.

14. Observe the vulnerable behavior on the unpatched build:

  • Drupal does not send prompt=login
  • the mock IdP sees an existing authenticated session
  • /authorize silently redirects back without showing the login page again.

15. Apply the patch and repeat from the mock-IdP session reset step.

16. Observe the patched behavior on the patched build:

  • Drupal sends prompt=login
  • the mock IdP honors that and shows the login page again
  • silent session reuse no longer happens.

Addressing the Issue

Users of the affected module should apply one of the following mitigations:

  • Configure OpenID Connect clients to send prompt=login so the Identity Provider re-prompts for authentication instead of silently reusing an existing session.
  • Avoid configurations that omit the prompt parameter or set prompt=none, unless the security implications are fully understood and accepted.
  • Limit use of this module to trusted identity providers whose session behavior and prompt handling you understand and control.
  • Restrict who can configure or modify OpenID Connect client settings, especially authorization-request parameters that affect login behavior.
  • Sign up for post-EOL security support; HeroDevs customers get immediate access to a patched version of this module.

Credits

Vulnerability Details
Severity
Level
CVSS Assessment
Low
>=0 <4
Medium
>=4 <6
High
>=6 <8
Critical
>=8 <10
Medium
ID
CVE-2026-3531
PROJECT Affected
OpenID Connect
Versions Affected
>=7.1.0 <=7.1.3
NES Versions Affected
Published date
April 16, 2026
≈ Fix date
April 14, 2026
Category
No items found.
Vex Document
Download VEXHow do I use it?
Sign up for the latest vulnerability alerts fixed in
NES for Drupal 7
Rss feed icon
Subscribe via RSS
or

By clicking “submit” I acknowledge receipt of our Privacy Policy.

Thanks for signing up for our Newsletter! We look forward to connecting with you.
Oops! Something went wrong while submitting the form.