mirror of
https://github.com/actualbudget/actual.git
synced 2026-11-09 10:45:04 -06:00
* [AI] Reject invalid password inputs before hashing - Require non-empty strings at password validation, hashing and verification boundaries so JSON objects, arrays and other scalar types cannot reach Argon2's buffer conversion. - Add regressions for malformed passwords on fresh and configured servers, including assertions that rejected input never reaches Argon2. Advisory: https://github.com/actualbudget/actual/security/advisories/GHSA-3jjw-2hq7-mmm2 * [AI] Require admin permission for EnableBanking configuration - Require administrator permission to configure EnableBanking, even when credentials have not been configured yet. - Check permission before validating credentials with the provider or persisting secrets, and disable configuration controls for non-admins. - Test unauthenticated, BASIC and ADMIN requests against empty and existing configuration, preserving secrets and avoiding provider calls on denial. Advisories: https://github.com/actualbudget/actual/security/advisories/GHSA-hfxx-qc34-gqcw * [AI] Neutralize BOM-prefixed CSV formulas - Detect spreadsheet formula triggers after leading BOM characters and prepend an apostrophe before the original text. - Protect textual CSV headers and scalar values as well as ordinary strings. - Preserve numeric output, including negative amounts, and cover repeated BOMs, formula triggers, quoting and Unicode text in regression tests. Advisories: https://github.com/actualbudget/actual/security/advisories/GHSA-7jvc-qfpw-85g2 * [AI] Remove ambiguous Commerzbank payee matching - Split Commerzbank payees into non-empty whitespace/slash-delimited tokens and escape their literal text before constructing the cleanup expression. - Use consuming separators that cannot overlap the literal tokens, avoiding ambiguous matching on long runs of spaces or slashes. - Require separators between payee tokens: concatenated words in notes are no longer stripped. Allow 20 seconds for the hostile-case subprocess, including Node and Vite startup on slower CI runners. - Skip matching for empty payees while preserving normal note cleanup. - Exercise hostile payees, including the report's spaced-word reproducer, in a subprocess with a hard timeout; retain the existing bank fixtures. Advisories: https://github.com/actualbudget/actual/security/advisories/GHSA-p34v-m2gp-w3jm * [AI] Reject unsafe IPv6 address classes in SSRF checks - Allow public unicast addresses by default. The private-network option additionally permits private, loopback, unique-local and carrier-grade NAT addresses. - Permit the well-known NAT64 prefix only for embedded public IPv4 destinations; private and metadata addresses remain blocked in both modes. - Reject other IPv6 transition, multicast, reserved and link-local classes in both modes, and classify IPv4-mapped addresses by the embedded IPv4 address. - Cover literal URLs, DNS answers, redirects and both SimpleFIN fetch paths, including NAT64, 6to4 and Teredo addresses, with synthetic upstreams. Advisories: https://github.com/actualbudget/actual/security/advisories/GHSA-97j5-6g5r-42v5 * [AI] Exclude callback secrets from ordinary server logs - Log request method, pathname and response status without query strings, and restrict serialized metadata to the same safe fields. - Keep full OpenID and banking provider messages, response bodies, session identifiers and error stacks behind existing actual-sensitive namespaces. - Apply the same opt-in handling to shared banking errors, proxy target URLs and unhandled rejections; do not forward raw streaming errors to Express's ordinary error logger. - Stop logging stored secret values, including with debugging enabled. - Document targeted sensitive debugging and private support exchanges. - Test ordinary and opt-in output, query and serialized metadata leaks, provider failures, session URLs, streaming errors and secret storage. - Preserve provider format sequences by passing SimpleFIN response text as a debug format argument, rather than part of the format string. Advisories: https://github.com/actualbudget/actual/security/advisories/GHSA-ppcp-hx2h-fm35 * [AI] Revoke password sessions when credentials change - Update the password hash and revoke all password-authenticated sessions in one transaction, including password changes through the local CLI. - Issue a fresh token for each successful password login instead of reusing an existing session token. - Recheck the verified password hash before creating a session so an in-flight login cannot restore access after a credential change. - Correct the UI's claim that other sessions remain signed in, and test multiple sessions, CLI resets, transaction rollback and login/reset races. This addresses session revocation and token reuse. Requiring the current password for remote changes is covered by the following commit. Advisories: https://github.com/actualbudget/actual/security/advisories/GHSA-7363-3qq5-2frj https://github.com/actualbudget/actual/security/advisories/GHSA-4wgp-48c6-v9w3 * [AI] Require the current password for remote password changes - Require currentPassword alongside the replacement password for remote changes, preserving the administrator and password-authentication checks. - Verify the current credential, rate-limit attempts and reject a change if the verified password hash changes before the update. - Collect the current password in the client and handle failure to sign in after a successful change. Keep the local reset command available without the old password. - Test that a session token alone cannot change credentials and that failed requests preserve the existing password and sessions. - Clear the redundant success notice when signing in with the new password fails. Advisories: https://github.com/actualbudget/actual/security/advisories/GHSA-4wgp-48c6-v9w3 * [AI] Restrict Electron protocol launches and network file reveals - Allow only parsed HTTP/HTTPS URLs through external-opening IPC, new-window handling and navigation, addressing GHSA-7pf2-w24j-p6h2. - Also reject network/UNC paths in the separate file-reveal handler. A note such as [View receipt](//host/share/invoice.pdf) reaches showItemInFolder without passing through openExternal. On Windows, resolving that share can disclose NTLM authentication responses to an attacker-controlled server where network and authentication policies permit it. This additional flow was not demonstrated in the advisory. - Accept native absolute local paths without adding file-URL conversion. - Test protocol filtering and network-path rejection without invoking OS handlers. The note-to-file-reveal flow is code-confirmed; an actual Windows authentication exchange has not been reproduced. The following commit addresses the advisory's separate internal-origin classification finding. Advisory: https://github.com/actualbudget/actual/security/advisories/GHSA-7pf2-w24j-p6h2 Additional finding references: https://github.com/electron/electron/blob/v43.4.0/shell/common/platform_util_win.cc#L270-L340 https://learn.microsoft.com/en-us/windows-server/storage/file-server/smb-ntlm-blocking - Exercise Windows path semantics on all test platforms and exclude generated Electron tests and Vitest configuration from the packaged application. * [AI] Classify Electron app URLs exactly and block redirects - Replace substring checks with exact app://actual scheme/host matching and the existing development origin, http://localhost:3001. - Use the classification for browser handoffs and focus/log script execution so remote URLs containing app:// or localhost: are not mistaken for app pages. - Preserve the existing global renderer-navigation block and add redirect blocking there, without duplicating it in the main-window handlers. - Keep ordinary IPC registration unchanged; broad sender validation is outside this URL-classification fix. - Test misleading hosts, ports and URL substrings. Exercise ordinary clicks, new-window links and explicit browser IPC in Electron, asserting the exact HTTP/HTTPS URLs handed to the browser without opening real OS handlers. Exercise a local HTTP redirect to check the app stays put. Advisory: https://github.com/actualbudget/actual/security/advisories/GHSA-7pf2-w24j-p6h2 - Retain startup logs until an internal page finishes loading, then forward them to DevTools; cover startup delivery in the Electron navigation suite. * [AI] Choose import destinations from trusted local context - Allocate fresh local IDs for cloud downloads and local archive imports, ignoring destination IDs supplied in archived metadata. - Pass an explicit local destination for replacement downloads and verify its cloud-file association. Do not require matching sync group IDs, so downloading the reset version of the same cloud file remains possible. - Reject server metadata describing a different cloud file and close the budget before an authorized replacement. - Retain existing import metadata compatibility, collision error handling, nested archive layouts and backups; do not require a budget name. - Test targeted collisions, local recovery imports with obsolete IDs or missing optional metadata, mismatched cloud associations and replacement after a sync ID reset using real local files and databases. Advisory: https://github.com/actualbudget/actual/security/advisories/GHSA-7fr5-fjj8-hwjq - Reuse the matching local budget for API downloads after a sync reset, report cloud-file mismatches in API/CLI errors, and document local IDs. * [AI] Revoke sessions on sign-out and API shutdown Revoke only the supplied session on explicit sign-out. Clear local credentials if the server cannot be reached, and revoke password-created API sessions during shutdown without revoking caller-supplied tokens. Cover isolated and repeated revocation, invalid tokens, offline sign-out, borrowed API tokens and shutdown after a close failure. Session expiry policy is unchanged. Follow-up to: https://github.com/actualbudget/actual/security/advisories/GHSA-7363-3qq5-2frj
95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
import { mkdtemp, rm } from 'node:fs/promises';
|
|
import { createServer } from 'node:http';
|
|
import type { Server } from 'node:http';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
|
|
import * as api from './index';
|
|
|
|
let server: Server;
|
|
let serverURL: string;
|
|
let dataDir: string;
|
|
let logouts: unknown[];
|
|
|
|
beforeEach(async () => {
|
|
global.IS_TESTING = true;
|
|
dataDir = await mkdtemp(join(tmpdir(), 'actual-session-'));
|
|
logouts = [];
|
|
server = createServer(async (req, res) => {
|
|
let body = '';
|
|
for await (const chunk of req) body += chunk;
|
|
if (req.url === '/account/logout') logouts.push(JSON.parse(body));
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(
|
|
JSON.stringify({
|
|
status: 'ok',
|
|
data: req.url === '/account/login' ? { token: 'owned-session' } : {},
|
|
}),
|
|
);
|
|
});
|
|
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
const address = server.address();
|
|
if (!address || typeof address === 'string') {
|
|
throw new Error('No test server address');
|
|
}
|
|
serverURL = `http://127.0.0.1:${address.port}`;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.restoreAllMocks();
|
|
await api.shutdown();
|
|
await new Promise<void>(resolve => server.close(() => resolve()));
|
|
await rm(dataDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it.each([false, true])(
|
|
'revokes the API-created session on shutdown (close failure: %s)',
|
|
async failClose => {
|
|
const instance = await api.init({
|
|
dataDir,
|
|
serverURL,
|
|
password: 'password',
|
|
});
|
|
if (failClose) {
|
|
const send = instance.send;
|
|
vi.spyOn(instance, 'send').mockImplementation((name, args) => {
|
|
if (name === 'close-budget') {
|
|
return Promise.reject(new Error('close failed'));
|
|
}
|
|
return send(name, args);
|
|
});
|
|
await expect(api.shutdown()).rejects.toThrow('close failed');
|
|
} else {
|
|
await api.shutdown();
|
|
}
|
|
await api.shutdown();
|
|
expect(logouts).toEqual([{ token: 'owned-session' }]);
|
|
},
|
|
);
|
|
|
|
it('does not revoke a caller-supplied token', async () => {
|
|
await api.init({ dataDir, serverURL, sessionToken: 'borrowed-session' });
|
|
await api.shutdown();
|
|
expect(logouts).toEqual([]);
|
|
});
|
|
|
|
it('revokes the current token on explicit sign-out', async () => {
|
|
const instance = await api.init({
|
|
dataDir,
|
|
serverURL,
|
|
sessionToken: 'borrowed-session',
|
|
});
|
|
await instance.send('subscribe-sign-out');
|
|
expect(logouts).toEqual([{ token: 'borrowed-session' }]);
|
|
expect(await instance.send('subscribe-get-user')).toBeNull();
|
|
});
|
|
|
|
it('clears local credentials when the server is offline', async () => {
|
|
const instance = await api.init({ dataDir, serverURL, password: 'password' });
|
|
await new Promise<void>(resolve => server.close(() => resolve()));
|
|
await expect(instance.send('subscribe-sign-out')).resolves.toBe('ok');
|
|
expect(await instance.send('subscribe-get-user')).toBeNull();
|
|
});
|