Actual has Privilege Escalation via 'change-password' Endpoint on OpenID-Migrated Servers
Date: 2026-04
Affected: npm/@actual-app/sync-server
Attack vector: auth-bypass
Category: vulnerability-disclosure
Severity: High
CVE: CVE-2026-33318
CWE: CWE-284
CWE-862
CVSS: 8.8 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

### Summary Any authenticated user (including `BASIC` role) can escalate to `ADMIN` on servers migrated from password authentication to OpenID Connect. Three weaknesses combine: `POST /account/change-password` has no authorization check, allowing any session to overwrite the password hash; the inactive password `auth` row is never removed on migration; and the login endpoint accepts a client-supplied `loginMethod` that bypasses the server's active auth configuration. Together these allow an attacker to set a known password and authenticate as the anonymous admin account created during the multiuser migration. --- ### Details **`packages/sync-server/src/app-account.js:120-132`** — the `/account/change-password` route validates only that a session exists. No admin role check is performed ```js app.post('/change-password', (req, res) => { const session = validateSession(req, res); // only checks token validity if (!session) return; const { error } = changePassword(req.body.password); // no isAdmin() check ``` **`packages/sync-server/src/accounts/password.js:113-125`** — `changePassword()` updates the hash with no current-password confirmation: ```js export function changePassword(newPassword) { accountDb.mutate("UPDATE auth SET extra_data = ? WHERE method = 'password'", [hashed]); } ``` **`packages/sync-server/src/accounts/password.js:56-62`** — `loginWithPassword()` always authenticates as the user with `user_name = ''`, which is created by the multiuser migrati


Mitigations:

Architectural layers (MAESTRO):