Apr 24, 2026·5 min read·6 visits
A privilege escalation vulnerability in Actual's sync-server allows authenticated users with basic roles to obtain administrative privileges. The issue stems from missing authorization checks on password change endpoints, blind credential overwrites, and improper handling of client-controlled authentication methods during OIDC migration.
Actual versions prior to 26.4.0 contain a critical privilege escalation vulnerability within the sync-server component. The flaw affects environments migrating to OpenID Connect (OIDC) and allows an authenticated user with a basic role to hijack an administrative account via an orphaned password record.
The Actual sync-server component prior to version 26.4.0 is vulnerable to a privilege escalation attack. This vulnerability affects deployments that have migrated from legacy password-based authentication to OpenID Connect (OIDC). The issue is formally tracked as CVE-2026-33318 and GHSA-prp4-2f49-fcgp.
The vulnerability is classified under CWE-284 (Improper Access Control) and CWE-862 (Missing Authorization). The core issue stems from multiple architectural flaws interacting within the sync-server authentication flow. These flaws interact sequentially, allowing a low-privileged user to hijack the primary administrative account.
Exploitation requires the system to have undergone an OIDC migration, which leaves an orphaned password database record. The attacker must possess an active session token, even with the lowest privilege level (BASIC), to initiate the exploit chain. No further interaction from the administrator is required.
The vulnerability is a sequential exploit chain relying on four distinct architectural flaws. First, the POST /account/change-password endpoint in packages/sync-server/src/app-account.js lacks proper authorization checks. It validates the presence of a session token but fails to verify the user's role, allowing BASIC role users to invoke it.
Second, the changePassword() function in packages/sync-server/src/accounts/password.js performs a blind credential overwrite. It updates the password hash via an unconditional SQL UPDATE on the auth table where method = 'password', without requiring the current password. This allows an attacker to alter credentials for an account they do not own.
Third, migration scripts introduced ghost records. The script 1719409568000-multiuser.js created an administrative account with an empty user_name (''). When migrating to OIDC, the password row in the auth table was marked active = 0 but not deleted. This left an orphaned, inactive password row tied to the admin account.
Fourth, the getLoginMethod() function in packages/sync-server/src/account-db.js trusts client input. It prioritizes the loginMethod parameter from the HTTP request body over server configuration. Crucially, it fails to check the active flag in the database, permitting the client to force the legacy password flow even when the server expects OIDC authentication.
Exploitation requires network access and a valid OIDC session with a BASIC role. The attacker initiates the chain by obtaining a standard session token from the OIDC provider. This token fulfills the basic authentication requirement for subsequent API calls to the sync-server.
The attacker sends a crafted POST request to /account/change-password containing a new password payload. The server processes this request without validating the user's role. The SQL UPDATE statement executes against the orphaned administrative row (where method = 'password'), overwriting the admin's password hash with the attacker's supplied value.
Next, the attacker sends a POST request to the login endpoint, explicitly setting the payload {"loginMethod": "password"} alongside the newly set credentials. The getLoginMethod() function honors this client-controlled parameter, bypassing the intended OIDC flow entirely.
The server authenticates the attacker using the legacy method, disregarding the active = 0 status of the database row. The server issues a new session token with ADMIN privileges, completing the privilege escalation.
The vulnerability carries a CVSS v3.1 base score of 8.8. The confidentiality, integrity, and availability impacts are all rated as High. Successful exploitation grants the attacker full administrative access to the Actual instance.
With administrative privileges, the attacker can view and exfiltrate all financial data stored within the system. They can manipulate budgets, modify account balances, and alter transaction histories. The integrity of the financial records is entirely compromised.
Furthermore, the attacker can disrupt the availability of the service. They can lock out legitimate users by changing account credentials, alter server configurations, or execute data deletion operations. The vulnerability bypasses the intended security boundary of the OIDC migration.
The vulnerability is fully resolved in Actual version 26.4.0. Administrators must update the sync-server component immediately. As a defense-in-depth measure, administrators of OIDC-migrated servers should manually inspect the auth table and delete orphaned rows where method = 'password'.
The patch was implemented across multiple pull requests. PR #7155 resolved the initial privilege escalation by enforcing role checks in the /change-password endpoint. It also hardened getLoginMethod() to ignore client-supplied overrides, forcing the server to dictate the authentication flow based on backend configuration.
PR #7207 introduced strict "admin-only" role requirements for password changes. It also required that sessions attempting to change passwords must have been authenticated via the password method initially. This severs the attack path between OIDC-authenticated sessions and legacy password modification.
Finally, PR #7334 addressed the ghost record issue. The patch enforces a strict check on the active status of authentication methods during login. If an account is marked active = 0, the server refuses the authentication attempt, neutralizing the client-side loginMethod override vector.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Actual Actual Budget | < 26.4.0 | 26.4.0 |
| Attribute | Detail |
|---|---|
| CWE | CWE-284, CWE-862 |
| Attack Vector | Network (AV:N) |
| CVSS Score | 8.8 (High) |
| Privileges Required | Low (PR:L) |
| User Interaction | None (UI:N) |
| Exploit Status | weaponized |
Improper Access Control