GitHub Security Advisory: GHSA-v9xm-ffx2-7h35
Package: apostrophe (NPM)
Severity: HIGH (CVSS 8.1)
CWE: CWE-287: Improper Authentication, CWE-305: Authentication Bypass by Primary Weakness
Vulnerable versions: <= 4.27.1
Patched in: 4.28.0

Summary: ApostropheCMS MFA/TOTP Bypass via Incorrect MongoDB Query in Bearer Token Middleware

Details:
# MFA/TOTP Bypass via Incorrect MongoDB Query in Bearer Token Middleware

## Summary

The bearer token authentication middleware in `@apostrophecms/express/index.js` (lines 386-389) contains an incorrect MongoDB query that allows incomplete login tokens — where the password was verified but TOTP/MFA requirements were NOT — to be used as fully authenticated bearer tokens. This completely bypasses multi-factor authentication for any ApostropheCMS deployment using `@apostrophecms/login-totp` or any custom `afterPasswordVerified` login requirement.

## Severity

The AC is High because the attacker must first obtain the victim's password. However, the entire purpose of MFA is to protect accounts when passwords are compromised (credential stuffing, phishing, database breaches), so this bypass negates the security control entirely.

## Affected Versions

All versions of ApostropheCMS from 3.0.0 to 4.27.1, when used with `@apostrophecms/login-totp` or any custom `afterPasswordVerified` requirement.

## Root Cause

In `packages/apostrophe/modules/@apostrophecms/express/index.js`, the `getBearer()` function (line 377) queries MongoDB for valid bearer tokens. The query at lines 386-389 is intended to only match tokens where the `requirementsToVerify` array is either absent (no MFA configured) or empty (all MFA requirements completed):

```javascript
async function getBearer() {
    const bearer = await self.apos.login.bearerTokens.findOne({
        _id: req.token,
        expires: { $gte: new Date() },
        // requirementsToVerify array should be empty or inexistant
        // for the token to be usable to log in.
        $or: [
            { requirementsToVerify: { $exists: false } },
            { requirementsToVerify: { $ne: [] } }  // BUG
        ]
    });
    return bearer && bearer.userId;
}
```

The comment correctly states the intent: the array should be "empty or inexistant." However, the MongoDB operator `$ne: []` matches documents where `requirementsToVerify` is **NOT** an empty array — meaning it matches tokens that still have **unverified requirements**. This is the exact opposite of the intended behavior.

| Token State | `requirementsToVerify` | `$ne: []` result | Should match? |
|---|---|---|---|
| No MFA configured | *(field absent)* | N/A (`$exists: false` matches) | Yes |
| TOTP pending | `["AposTotp"]` | `true` (BUG!) | **No** |
| All verified | `[]` | `false` (BUG!) | **Yes** |
| Field removed (`$unset`) | *(field absent)* | N/A (`$exists: false` matches) | Yes |

## Attack Scenario

### Prerequisites
- ApostropheCMS instance with `@apostrophecms/login-totp` enabled
- Attacker knows the victim's username and password (e.g., from credential stuffing, phishing, or a database breach)
- Attacker does NOT know the victim's TOTP secret/code

### Steps

1. **Authenticate with password only:**
   ```
   POST /api/v1/@apostrophecms/login/login
   Content-Type: application/json

   {"username": "admin", "password": "correct_password", "session