Feb 27, 2026·5 min read·5 visits
Actual Budget versions prior to 26.2.1 fail to verify file ownership on synchronization endpoints. Any authenticated user can read or overwrite another user's financial data by supplying the target's budget ID in API requests.
A critical authorization flaw exists in Actual Budget's synchronization server, specifically affecting multi-user deployments. The vulnerability allows authenticated users to access, modify, or delete budget files belonging to other users due to missing ownership verification checks in the sync endpoints. This effectively constitutes an Insecure Direct Object Reference (IDOR) where knowledge of a file's UUID is sufficient to grant full access, bypassing intended isolation between users.
Actual Budget is a local-first personal finance application that supports a server component for syncing data across devices. In multi-user configurations, the server acts as a central repository for multiple distinct user accounts. CVE-2026-27638 identifies a Missing Authorization vulnerability (CWE-862) within this synchronization layer.
The core issue lies in the API endpoints responsible for handling budget file operations, such as downloading, uploading, and syncing changes. While the application correctly enforces authentication—requiring a valid session token to interact with the API—it failed to implement resource-level authorization. Consequently, the server did not verify that the authenticated user was the owner of the budget file specified in the request.
The vulnerability stems from a reliance on authentication middleware without complementary object-level permission checks. The server utilizes validateSessionMiddleware to ensure that incoming requests originate from a logged-in user. However, once this check passed, the endpoints trustingly accepted the fileId (typically provided via the X-Actual-File-Id header or JSON body) as the target resource.
In packages/sync-server/src/app-sync.ts, handlers for routes like /sync, /download-user-file, and /upload-user-file would retrieve file metadata from the database based on the provided ID. The logic proceeded to perform the requested operation (read/write) immediately after retrieval. Crucially, the code did not compare the owner field of the retrieved file record against the userId associated with the active session. This omission created a direct path for Insecure Direct Object Reference (IDOR), where the identifier alone acted as the authorization token.
The following comparison highlights the absence of authorization logic in the vulnerable version and the explicit checks introduced in the patch.
Vulnerable Logic (Conceptual Representation):
Before the fix, endpoints would look up a file and immediately return it or process it, assuming that possession of the ID implied permission.
// Vulnerable handler example
app.post('/download-user-file', async (req, res) => {
const fileId = req.headers['x-actual-file-id'];
// DANGER: No check to see if req.userId owns fileId
const file = await db.getFile(fileId);
if (file) {
res.send(file.content);
}
});Patched Logic (Commit 9966c024):
The fix introduces a centralized helper function requireFileAccess which is now invoked at the start of every sync endpoint. This function explicitly validates ownership or administrative privileges.
// New authorization helper
function requireFileAccess(file, userId) {
// 1. Direct ownership check
if (file.owner === userId) {
return null; // Access granted
}
// 2. Admin override for system tasks
if (isAdmin(userId)) {
return null;
}
// 3. Shared access check (new feature support)
if (UserService.countUserAccess(file.id, userId) > 0) {
return null;
}
// Default deny
return 'file-access-not-allowed';
}
// Patched handler usage
app.post('/download-user-file', async (req, res) => {
const fileId = req.headers['x-actual-file-id'];
const file = await db.getFile(fileId);
// Explicit authorization gate
const accessError = requireFileAccess(file, req.userId);
if (accessError) {
return res.status(403).send(accessError);
}
res.send(file.content);
});This change ensures that even if an attacker guesses a valid fileId, the server rejects the request if the database record for that file lists a different owner.
To exploit this vulnerability, an attacker requires a valid account on the target Actual Budget instance. The attack does not require administrative privileges, only a standard user session.
Step 1: Reconnaissance
The attacker must obtain the fileId (a UUID) of a target budget. While UUIDs are generally not enumerable, they may be leaked through shared logs, exposed in client-side errors, or obtained via social engineering. In some configurations or older versions, file IDs might be predictable or sequentially generated (though Actual uses UUIDs by default).
Step 2: Execution The attacker crafts a malicious HTTP request to the synchronization API. For example, to exfiltrate a victim's budget:
POST /download-user-file HTTP/1.1
Host: budget.example.com
X-Actual-Token: <attacker-session-token>
X-Actual-File-Id: <victim-budget-uuid>
Content-Type: application/json
{}Step 3: Impact The server processes the request using the attacker's valid session but the victim's file ID. Without the ownership check, the server returns the victim's full budget database. Conversely, an attacker could upload a corrupted database to the same ID, destroying the victim's data.
The impact of this vulnerability is significant for multi-user deployments, affecting the confidentiality and integrity of financial data.
Confidentiality: An attacker can download complete budget files, exposing sensitive transaction histories, bank balances, and financial habits of other users.
Integrity: An attacker can modify or overwrite budget files. This allows for the injection of fraudulent transactions, deletion of valid records, or complete corruption of the budget database, rendering it unusable for the legitimate owner.
Availability: The /delete-user-file endpoint is also affected. An attacker could permanently delete other users' budget files, causing data loss if no backups exist.
The CVSS v3.1 score is 7.1 (High), driven by the high Integrity impact and the low complexity required to execute the attack once a file ID is known.
The primary remediation is to update the Actual server software.
Patching:
Update to Actual version 26.2.1 or later. This version includes the requireFileAccess logic and database migrations to ensure all existing files have correctly assigned owners.
Configuration Workarounds: If immediate patching is not feasible, administrators should restrict the instance to single-user mode. This vulnerability specifically impacts multi-user configurations where isolation between accounts is assumed. By disabling open registration and ensuring only trusted users have accounts, the risk is mitigated, although the underlying code flaw remains.
Database Audit:
Post-update, administrators should verify that the migration script (1763873600000-backfill-files-owner.js) successfully populated the owner column for all records in the files table. Orphaned files (files with NULL owners) should be manually assigned or audited.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Actual Actual Budget | < 26.2.1 | 26.2.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-862 |
| Vulnerability Type | Missing Authorization / IDOR |
| CVSS v3.1 | 7.1 (High) |
| Attack Vector | Network |
| Privileges Required | Low (Authenticated User) |
| Impact | Data Exfiltration & Integrity Loss |