Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain
-
AccessAuthenticate with any valid session token
-
DeliveryPOST /account/change-password to overwrite dormant admin hash
-
ExploitPOST /account/login with loginMethod=password
-
ExecutionReceive token for empty-username ADMIN account
-
ImpactFull admin control of users, budgets, and config
Vulnerability Assessment
| Weakness |
CWE
OWASP Top 10:2025
A01:2025 - Broken Access Control
ASVS 5.0
V8 - Authorization Access control enforcement
MITRE ATT&CK
1 technique across 1 tactic: Defense Evasion The technique list with ATT&CK links and the kill-chain view is part of the Basic plan and up. |
| Exploitation Conditions | Exploitation requires a specific deployment history: the server must have been originally bootstrapped with password authentication and then switched to OpenID Connect via enableOpenID() (which leaves the password auth row present but active = 0), and the server must retain a default allowedLoginMethods configuration that still includes 'password'. The attacker must also already possess a valid session token of any role (PR:L - a BASIC OpenID user suffices), so this is not fully unauthenticated. Limiting factors that reduce realistic exposure: OpenID-only servers (no password row ever created) and unmigrated password-only servers are not exploitable; removing 'password' from allowedLoginMethods or deleting the orphaned auth row also breaks the chain. No user interaction is required. |
| Impact | The supplied CVSS 3.1 vector (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H, 8.8) reflects network-reachable exploitation by an already-authenticated low-privilege user with total confidentiality, integrity, and availability impact over the application, consistent with SSVC Technical Impact: total. However, the signals diverge on real-world urgency: EPSS is very low (0.07%, 21st percentile) and SSVC Automatable: no, because exploitation requires a specific, non-default precondition - the server must have been bootstrapped with password auth and later switched to OpenID while retaining the default allowedLoginMethods that includes password. This is a genuine, fully-chained privilege escalation with public PoC, so for any deployment matching that migration history it is a real, high-priority issue; for OpenID-only or password-only deployments it is not exploitable at all. The 8.8 base score arguably overstates broad risk because AC:L does not capture the deployment-state prerequisite (an argument for AC:H), so treat CVSS as the ceiling and gate prioritization on whether a given server was migrated. No KEV listing and low EPSS suggest no observed mass exploitation. |
| Exploit ScenarioDerived | On an Actual Budget server migrated from password auth to OpenID, an attacker holding any valid session token (even a BASIC-role user) sends POST /account/change-password with a password of their choice, silently overwriting the dormant admin credential. They then POST to /account/login with loginMethod: 'password' and that password, receiving a token for the user_name = '' ADMIN account, which grants control over all users, budget files, and server configuration. A public curl-based PoC for the full chain is included in the GitHub advisory. |
Get an e-mail when this CVE is exploited, patched or re-scored. Free account.
Alert meAnalysisAIv1 – Jul 24, 2026
Privilege escalation in Actual Budget sync-server (npm @actual-app/sync-server before 26.4.0) allows any authenticated user, including a low-privilege BASIC-role account, to gain full ADMIN control on servers that were migrated from password authentication to OpenID Connect. The flaw chains an unauthorized /account/change-password endpoint, an orphaned password auth row left active after migration, and a client-controlled loginMethod override, letting an attacker set a known password and log in as the anonymous ADMIN account. Publicly available exploit code exists (SSVC PoC) with a working curl-based chain, though EPSS is low (0.07%) and it is not listed in CISA KEV.
RemediationAI
Vendor-released patch: upgrade to Actual Budget 26.4.0 or later (release notes at https://actualbudget.org/blog/release-26.4.0; advisory GHSA-prp4-2f49-fcgp). The fix restricts change-password to password-authenticated sessions, requires current-password confirmation, and determines the login method server-side from active = 1 rows instead of trusting client input. As an immediate mitigation for servers already fully migrated to OpenID that do not need password auth, administrators can remove the orphaned credential directly with the SQL statement DELETE FROM auth WHERE method = 'password'; this closes the exploit by eliminating the target row, with the side effect that no password-based login will be possible thereafter (acceptable for OpenID-only servers). Where DB edits are undesirable before patching, restrict network access to the sync-server admin/account endpoints and review the allowedLoginMethods configuration to drop 'password' where feasible, accepting that this may break legitimate password logins on mixed-mode servers.
Affected VersionsENISA
Affected Packages
Source: OSV (Open Source Vulnerabilities)
Affects your stack
Affects 2 packages in npm, euvd
Checking this CVE against your own stacks and servers is part of the Basic plan and up.
Technical ContextAI · show details
Actual Budget is a self-hosted, open-source personal finance/budgeting application; the affected component is its Node.js sync-server (distributed via npm as @actual-app/sync-server), which handles authentication and multi-user access to budget files. The root cause is CWE-284 (Improper Access Control): the Express route POST /account/change-password in app-account.js only calls validateSession() and omits any isAdmin() or active-auth-method check, so changePassword() in accounts/password.js rewrites the stored bcrypt hash for the 'password' method without confirming the current password. This is compounded by the multiuser migration (1719409568000-multiuser.js), which creates an ADMIN account with user_name = '' that loginWithPassword() always resolves to, and by getLoginMethod() in account-db.js trusting a client-supplied loginMethod as long as it appears in allowedLoginMethods while never checking the auth table's active column. When enableOpenID() migrates a server, it sets the password row to active = 0 but never deletes it, leaving a reactivatable credential path.
Know when the next one hits your stack
- Watch products and get an email when something new is published
- Check this CVE against your own stack and servers
- Exploit and PoC references, threat intelligence from 22 sources
No credit card · 7-day full trial
Severity by source
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Network-reachable and needs a low-priv session (PR:L), but the required password-to-OpenID migration state is a non-default precondition beyond attacker control, warranting AC:H; full app compromise gives C:H/I:H/A:H.
Primary rating from Vendor (https://github.com/actualbudget/actual).
CVSS VectorVendor: https://github.com/actualbudget/actual
Lifecycle Timeline
2DescriptionCVE.org
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
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() checkpackages/sync-server/src/accounts/password.js:113-125 - changePassword() updates the hash with no current-password confirmation:
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 migration with role = 'ADMIN':
const sessionRow = accountDb.first(
'SELECT * FROM sessions WHERE auth_method = ?', ['password']
);packages/sync-server/src/account-db.js:56-63 - a client can force the password login method regardless of server configuration by sending loginMethod in the request body:
if (req.body.loginMethod && config.get('allowedLoginMethods').includes(req.body.loginMethod)) {
return req.body.loginMethod;
}When a server is migrated from password → OpenID via enableOpenID(), the password row is set to active = 0 but never deleted, leaving it available for exploitation.
---
PoC
Prerequisites: Server originally bootstrapped with password auth, then switched to OpenID. Default allowedLoginMethods configuration (includes password). Attacker has any valid OpenID session token (any role).
# Step 1 - overwrite the password hash using a BASIC-role session
curl -s -X POST https://<host>/account/change-password \
-H "Content-Type: application/json" \
-H "X-Actual-Token: <any_valid_session_token>" \
-d '{"password": "attacker123"}'
# → {"status":"ok","data":{}}
# Step 2 - log in via password method to obtain an ADMIN session
curl -s -X POST https://<host>/account/login \
-H "Content-Type: application/json" \
-d '{"loginMethod": "password", "password": "attacker123"}'
# → {"status":"ok","data":{"token":"<admin_token>"}}The returned token belongs to the user_name = '' admin account (created by 1719409568000-multiuser.js).
Verify admin access:
curl -s https://<host>/account/validate \
-H "X-Actual-Token: <admin_token>"
# → {"status":"ok","data":{"permission":"ADMIN", ...}}---
Impact
Privilege escalation - any authenticated user can gain full ADMIN access on affected deployments. An ADMIN can manage all users, access all budget files regardless of ownership, modify file access controls, and change server configuration.
Affected deployments: multi-user servers running OpenID Connect that were previously configured with password authentication. Servers bootstrapped exclusively with OpenID from initial setup are not affected (no password row exists in the auth table).
---
Recommendations
1. Restrict POST /account/change-password to password-authenticated sessions only (packages/sync-server/src/app-account.js)
The endpoint should reject requests from sessions that were authenticated via OpenID. The active auth method can be checked against the session's auth_method field or by querying the auth table for the currently active method. If the server is running in OpenID mode, this endpoint should return 403 Forbidden.
2. Require current-password confirmation before accepting a new password (packages/sync-server/src/accounts/password.js)
changePassword() should accept the current password as a parameter and verify it against the stored hash before applying the update. This prevents any session (even a legitimate password session) from silently overwriting the credential without proving possession of the existing one.
3. Enforce active status and remove client control over login method selection (packages/sync-server/src/account-db.js - getLoginMethod())
Two issues exist in getLoginMethod(): (a) a client can supply loginMethod in the request body to select any method listed in allowedLoginMethods, regardless of whether it is currently active on the server; (b) the function does not check the active column, so an administratively disabled method (e.g. password after migrating to OpenID) remains accessible. The fix is to determine the permitted method server-side from WHERE active = 1 in the auth table and ignore any client-supplied loginMethod override entirely. Servers intentionally running both methods simultaneously can be supported by allowing multiple active = 1 rows rather than relying on client input.
4. Immediate mitigation for existing deployments (OpenID-only servers)
Administrators who have fully migrated to OpenID and do not need password auth can remove the orphaned row:
DELETE FROM auth WHERE method = 'password';####
The three weaknesses form a single, sequential exploit chain - none produces privilege escalation on its own:
Missing authorization on POST /change-password - allows overwriting a password hash, but only matters if there is an orphaned row to target. Orphaned password row persisting after migration - provides the target row, but is harmless without the ability to authenticate using it. Client-controlled loginMethod: "password" - allows forcing password-based auth, but is useless without a known hash established by step 1.
All three must be chained in sequence to achieve the impact. No single weakness independently results in privilege escalation, which under CVE CNA rule 4.1.2 means they should not each be treated as standalone vulnerabilities. The single root cause is the missing authorization check on /change-password; the other two are preconditions that make it exploitable. A single CVE reflecting that root cause is the appropriate representation - splitting them would falsely imply each carries independent risk.
Executive ViewAI
Actual Budget sync-server versions before 26.4.0 contain a critical privilege escalation vulnerability affecting instances migrated to OpenID Connect. Users with basic-level access can gain full administrative control, compromising data confidentiality and system integrity. Exploitation is straightforward with publicly available code, making prompt remediation essential for organizations running affected versions.
Apply vendor patch.
Threat Intelligence Overview
How this decision was reached
The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.
What every action level means
SSVC AssessmentENISA
Priority Score
Attack Technique
Authentication bypass attacks exploit flaws in the verification mechanisms that control access to systems and applications.
Privilege escalation occurs when an attacker leverages flaws in access control mechanisms to gain permissions beyond what they were originally granted.
EUVD-2026-25380
GHSA-prp4-2f49-fcgp
NVD
CVE.org
EPSS
Vulnrichment
VulDB
Exploit-DB