Compare commits

..

3 Commits

Author SHA1 Message Date
Matiss Janis Aboltins
2854e049d2 Fix privilege escalation issue in change-password endpoint 2026-03-07 21:22:57 +00:00
Matiss Janis Aboltins
d99b19b911 Update upcoming-release-notes/7155.md
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-07 21:11:59 +00:00
Matiss Janis Aboltins
67d1abda52 [AI] Fix privilege escalation in sync-server /change-password and getLoginMethod
Made-with: Cursor
2026-03-07 21:01:15 +00:00
34 changed files with 214 additions and 267 deletions

View File

@@ -1,74 +0,0 @@
---
description: Rules for AI-generated commits and pull requests
globs:
alwaysApply: true
---
# PR and Commit Rules for AI Agents
Canonical source: `.github/agents/pr-and-commit-rules.md`
## Commit Rules
### [AI] Prefix Requirement
**ALL commit messages MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
**Examples:**
- `[AI] Fix type error in account validation`
- `[AI] Add support for new transaction categories`
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
- `Add support for new transaction categories` (MISSING PREFIX - NOT ALLOWED)
### Git Safety Rules
- **Never** update git config
- **Never** run destructive git operations (force push, hard reset) unless the user explicitly requests it
- **Never** skip hooks (`--no-verify`, `--no-gpg-sign`)
- **Never** force push to `main`/`master`
- **Never** commit unless explicitly asked by the user
## Pre-Commit Quality Checklist
Before committing, ensure all of the following:
- [ ] Commit message is prefixed with `[AI]`
- [ ] `yarn typecheck` passes
- [ ] `yarn lint:fix` has been run
- [ ] Relevant tests pass
- [ ] User-facing strings are translated
- [ ] Code style conventions followed (see `AGENTS.md` for full style guide)
## Pull Request Rules
### [AI] Prefix Requirement
**ALL pull request titles MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
**Examples:**
- `[AI] Fix type error in account validation`
- `[AI] Add support for new transaction categories`
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
### Labels
Add the **"AI generated"** label to all AI-created pull requests.
### PR Template: Do Not Fill In
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all blank spaces and placeholder comments as-is.
- **Exception**: If a human **explicitly asks** you to fill out the PR template, then fill it out **in Chinese** (简体中文).
## Quick-Reference Workflow
1. Make your changes
2. Run `yarn typecheck` — fix any errors
3. Run `yarn lint:fix` — fix any remaining lint errors
4. Run relevant tests (`yarn test` for all, or workspace-specific)
5. Stage files and commit with `[AI]` prefix — do not skip hooks
6. When creating a PR:
- Use `[AI]` prefix in the title
- Add the `"AI generated"` label
- Leave the PR template blank (do not fill it in)

View File

@@ -1,14 +1,14 @@
#!/usr/bin/env node
// overview:
// 1. Identify the migrations in packages/loot-core/migrations/* on `master` and HEAD
// 2. Make sure that any new migrations on HEAD are dated after the latest migration on `master`.
import { spawnSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
const { spawnSync } = require('child_process');
const path = require('path');
const migrationsDir = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
__dirname,
'..',
'..',
'packages',
@@ -16,7 +16,7 @@ const migrationsDir = path.join(
'migrations',
);
function readMigrations(ref: string) {
function readMigrations(ref) {
const { stdout } = spawnSync('git', [
'ls-tree',
'--name-only',

View File

@@ -1,70 +0,0 @@
# PR and Commit Rules for AI Agents
This is the single source of truth for all commit and pull request rules that AI agents must follow when working with Actual Budget.
## Commit Rules
### [AI] Prefix Requirement
**ALL commit messages MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
**Examples:**
- `[AI] Fix type error in account validation`
- `[AI] Add support for new transaction categories`
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
- `Add support for new transaction categories` (MISSING PREFIX - NOT ALLOWED)
### Git Safety Rules
- **Never** update git config
- **Never** run destructive git operations (force push, hard reset) unless the user explicitly requests it
- **Never** skip hooks (`--no-verify`, `--no-gpg-sign`)
- **Never** force push to `main`/`master`
- **Never** commit unless explicitly asked by the user
## Pre-Commit Quality Checklist
Before committing, ensure all of the following:
- [ ] Commit message is prefixed with `[AI]`
- [ ] `yarn typecheck` passes
- [ ] `yarn lint:fix` has been run
- [ ] Relevant tests pass
- [ ] User-facing strings are translated
- [ ] Code style conventions followed (see `AGENTS.md` for full style guide)
## Pull Request Rules
### [AI] Prefix Requirement
**ALL pull request titles MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
**Examples:**
- `[AI] Fix type error in account validation`
- `[AI] Add support for new transaction categories`
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
### Labels
Add the **"AI generated"** label to all AI-created pull requests. This helps maintainers understand the nature of the contribution.
### PR Template: Do Not Fill In
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all blank spaces and placeholder comments as-is. Humans are expected to fill in the Description, Related issue(s), Testing, and Checklist sections.
- **Exception**: If a human **explicitly asks** you to fill out the PR template, then fill it out **in Chinese**, using Chinese characters (简体中文) for all content you add.
## Quick-Reference Workflow
Follow these steps when committing and creating PRs:
1. Make your changes
2. Run `yarn typecheck` — fix any errors
3. Run `yarn lint:fix` — fix any remaining lint errors
4. Run relevant tests (`yarn test` for all, or workspace-specific)
5. Stage files and commit with `[AI]` prefix — do not skip hooks
6. When creating a PR:
- Use `[AI]` prefix in the title
- Add the `"AI generated"` label
- Leave the PR template blank (do not fill it in)

View File

@@ -60,9 +60,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
uses: ./.github/actions/setup
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
download-translations: 'false'
node-version: 22
- name: Check migrations
run: yarn workspace @actual-app/ci-actions tsx bin/check-migrations.ts
run: node ./.github/actions/check-migrations.js

View File

@@ -44,9 +44,25 @@ yarn start:desktop
### ⚠️ CRITICAL REQUIREMENT: AI-Generated Commit Messages and PR Titles
**ALL commit messages and PR titles MUST be prefixed with `[AI]`.** No exceptions.
**THIS IS A MANDATORY REQUIREMENT THAT MUST BE FOLLOWED WITHOUT EXCEPTION:**
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for the full specification, including git safety rules, pre-commit checklist, and PR workflow.
- **ALL commit messages MUST be prefixed with `[AI]`**
- **ALL pull request titles MUST be prefixed with `[AI]`**
**Examples:**
-`[AI] Fix type error in account validation`
-`[AI] Add support for new transaction categories`
-`Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
-`Add support for new transaction categories` (MISSING PREFIX - NOT ALLOWED)
**This requirement applies to:**
- Every single commit message created by AI agents
- Every single pull request title created by AI agents
- No exceptions are permitted
**This is a hard requirement that agents MUST follow. Failure to include the `[AI]` prefix is a violation of these instructions.**
### Task Orchestration with Lage
@@ -345,7 +361,13 @@ Always maintain newlines between import groups.
**Git Commands:**
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete git safety rules, commit message requirements, and PR workflow.
- **MANDATORY: ALL commit messages MUST be prefixed with `[AI]`** - This is a hard requirement with no exceptions
- **MANDATORY: ALL pull request titles MUST be prefixed with `[AI]`** - This is a hard requirement with no exceptions
- Never update git config
- Never run destructive git operations (force push, hard reset) unless explicitly requested
- Never skip hooks (--no-verify, --no-gpg-sign)
- Never force push to main/master
- Never commit unless explicitly asked
## File Structure Patterns
@@ -544,7 +566,7 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
Before committing changes, ensure:
- [ ] Commit and PR rules followed (see [PR and Commit Rules](.github/agents/pr-and-commit-rules.md))
- [ ] **MANDATORY: Commit message is prefixed with `[AI]`** - This is a hard requirement with no exceptions
- [ ] `yarn typecheck` passes
- [ ] `yarn lint:fix` has been run
- [ ] Relevant tests pass
@@ -557,7 +579,17 @@ Before committing changes, ensure:
## Pull Request Guidelines
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete PR creation rules, including title prefix requirements, labeling, and PR template handling.
When creating pull requests:
- **MANDATORY PREFIX REQUIREMENT**: **ALL pull request titles MUST be prefixed with `[AI]`** - This is a hard requirement that MUST be followed without exception
- ✅ Correct: `[AI] Fix type error in account validation`
- ❌ Incorrect: `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
- **AI-Generated PRs**: If you create a PR using AI assistance, add the **"AI generated"** label to the pull request. This helps maintainers understand the nature of the contribution.
### PR Template: Do Not Fill In
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all blank spaces and placeholder comments as-is. We expect **humans** to fill in the Description, Related issue(s), Testing, and Checklist sections.
- **Exception**: If a human **explicitly asks** you to fill out the PR template, then fill it out **in Chinese**, using Chinese characters (简体中文) for all content you add.
## Code Review Guidelines

View File

@@ -1,2 +1 @@
@AGENTS.md
@.github/agents/pr-and-commit-rules.md

View File

@@ -3,18 +3,9 @@
"private": true,
"type": "module",
"scripts": {
"tsx": "node --import=extensionless/register --experimental-strip-types",
"test": "vitest --run",
"typecheck": "tsc --noEmit"
"test": "vitest --run"
},
"devDependencies": {
"extensionless": "^2.0.6",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
"extensionless": {
"lookFor": [
"ts"
]
}
}

View File

@@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [],
"module": "nodenext",
"moduleResolution": "nodenext",
"skipLibCheck": true,
"strict": true,
"types": ["node"],
"outDir": "dist",
"rootDir": "."
},
"include": ["src/**/*", "bin/**/*"],
"exclude": ["node_modules"]
}

View File

@@ -34,7 +34,10 @@ import type { AccountEntity, TransactionEntity } from 'loot-core/types/models';
import { lookupName, Status } from './TransactionEdit';
import { makeAmountFullStyle } from '@desktop-client/components/budget/util';
import {
makeAmountFullStyle,
makeBalanceAmountStyle,
} from '@desktop-client/components/budget/util';
import { useAccount } from '@desktop-client/hooks/useAccount';
import { useCachedSchedules } from '@desktop-client/hooks/useCachedSchedules';
import { useCategories } from '@desktop-client/hooks/useCategories';
@@ -280,11 +283,7 @@ export function TransactionListItem({
<Text
style={{
...styles.tnum,
...makeAmountFullStyle(amount, {
positiveColor: theme.tableText,
negativeColor: theme.tableText,
zeroColor: theme.numberNeutral,
}),
...makeAmountFullStyle(amount),
...textStyle,
}}
>
@@ -296,11 +295,7 @@ export function TransactionListItem({
fontSize: 11,
fontWeight: '400',
...styles.tnum,
...makeAmountFullStyle(runningBalance, {
positiveColor: theme.numberPositive,
negativeColor: theme.numberNegative,
zeroColor: theme.numberNeutral,
}),
...makeBalanceAmountStyle(runningBalance),
}}
>
{integerToCurrency(runningBalance)}

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { send } from 'loot-core/platform/client/connection';
import { computeSchedulePreviewTransactions } from 'loot-core/shared/schedules';
@@ -47,9 +47,18 @@ export function usePreviewTransactions({
} = useCachedSchedules();
const [isLoading, setIsLoading] = useState(isSchedulesLoading);
const [error, setError] = useState<Error | undefined>(undefined);
const [runningBalances, setRunningBalances] = useState<
Map<TransactionEntity['id'], IntegerAmount>
>(new Map());
const [upcomingLength] = useSyncedPref('upcomingScheduledTransactionLength');
// We don't want to re-render if options changes.
// Putting options in a ref will prevent that and
// allow us to use the latest options on next render.
const optionsRef = useRef(options);
optionsRef.current = options;
const scheduleTransactions = useMemo(() => {
if (isSchedulesLoading) {
return [];
@@ -100,6 +109,21 @@ export function usePreviewTransactions({
const ungroupedTransactions = ungroupTransactions(withDefaults);
setPreviewTransactions(ungroupedTransactions);
if (optionsRef.current?.calculateRunningBalances) {
setRunningBalances(
// We always use the bottom up calculation for preview transactions
// because the hook controls the order of the transactions. We don't
// need to provide a custom way for consumers to calculate the running
// balances, at least as of writing.
calculateRunningBalancesBottomUp(
ungroupedTransactions,
// Preview transactions are behaves like 'all' splits
'all',
optionsRef.current?.startingBalance,
),
);
}
setIsLoading(false);
}
})
@@ -115,24 +139,6 @@ export function usePreviewTransactions({
};
}, [scheduleTransactions, schedules, statuses, upcomingLength]);
const runningBalances = useMemo(() => {
if (!options?.calculateRunningBalances) {
return new Map<TransactionEntity['id'], IntegerAmount>();
}
// We always use the bottom up calculation for preview transactions
// because the hook controls the order of the transactions.
return calculateRunningBalancesBottomUp(
previewTransactions,
'all',
options?.startingBalance,
);
}, [
previewTransactions,
options?.calculateRunningBalances,
options?.startingBalance,
]);
const returnError = error || scheduleQueryError;
return {
previewTransactions,

View File

@@ -72,15 +72,6 @@ describe('utility functions', () => {
expect(looselyParseAmount('(1 500.99)')).toBe(-1500.99);
});
test('looseParseAmount handles trailing whitespace', () => {
expect(looselyParseAmount('1055 ')).toBe(1055);
expect(looselyParseAmount('$1,055 ')).toBe(1055);
expect(looselyParseAmount('$1,055.00 ')).toBe(1055);
expect(looselyParseAmount(' $1,055 ')).toBe(1055);
expect(looselyParseAmount('3.45 ')).toBe(3.45);
expect(looselyParseAmount(' 3.45 ')).toBe(3.45);
});
test('number formatting works with comma-dot format', () => {
setNumberFormat({ format: 'comma-dot', hideFraction: false });
let formatter = getNumberFormat().formatter;

View File

@@ -550,8 +550,6 @@ export function looselyParseAmount(amount: string) {
return v.replace(/[^0-9-]/g, '');
}
amount = amount.trim();
if (amount.startsWith('(') && amount.endsWith(')')) {
// Remove Unicode minus inside parentheses before converting to ASCII minus
amount = amount.replace(/\u2212/g, '');

View File

@@ -59,7 +59,12 @@ export function getLoginMethod(req) {
(req.body || { loginMethod: null }).loginMethod &&
config.get('allowedLoginMethods').includes(req.body.loginMethod)
) {
return req.body.loginMethod;
const accountDb = getAccountDb();
const activeRow = accountDb.first(
'SELECT method FROM auth WHERE method = ? AND active = 1',
[req.body.loginMethod],
);
if (activeRow) return req.body.loginMethod;
}
//BY-PASS ANY OTHER CONFIGURATION TO ENSURE HEADER AUTH

View File

@@ -121,6 +121,15 @@ app.post('/change-password', (req, res) => {
const session = validateSession(req, res);
if (!session) return;
if (getActiveLoginMethod() !== 'password') {
res.status(403).send({
status: 'error',
reason: 'forbidden',
details: 'password-auth-not-active',
});
return;
}
const { error } = changePassword(req.body.password);
if (error) {

View File

@@ -1,7 +1,8 @@
import request from 'supertest';
import { v4 as uuidv4 } from 'uuid';
import { getAccountDb, getServerPrefs } from './account-db';
import { getAccountDb, getLoginMethod, getServerPrefs } from './account-db';
import { bootstrapPassword } from './accounts/password';
import { handlers as app } from './app-account';
const ADMIN_ROLE = 'ADMIN';
@@ -33,6 +34,119 @@ const clearServerPrefs = () => {
getAccountDb().mutate('DELETE FROM server_prefs');
};
const insertAuthRow = (method, active, extraData = null) => {
getAccountDb().mutate(
'INSERT INTO auth (method, display_name, extra_data, active) VALUES (?, ?, ?, ?)',
[method, method, extraData, active],
);
};
const clearAuth = () => {
getAccountDb().mutate('DELETE FROM auth');
};
describe('/change-password', () => {
let userId, sessionToken;
beforeEach(() => {
userId = uuidv4();
sessionToken = generateSessionToken();
createUser(userId, 'testuser', ADMIN_ROLE);
createSession(userId, sessionToken);
});
afterEach(() => {
deleteUser(userId);
clearAuth();
});
it('should return 401 if no session token is provided', async () => {
const res = await request(app).post('/change-password').send({
password: 'newpassword',
});
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('status', 'error');
expect(res.body).toHaveProperty('reason', 'unauthorized');
});
it('should return 403 when active auth method is openid', async () => {
insertAuthRow('openid', 1);
const res = await request(app)
.post('/change-password')
.set('x-actual-token', sessionToken)
.send({ password: 'newpassword' });
expect(res.statusCode).toEqual(403);
expect(res.body).toEqual({
status: 'error',
reason: 'forbidden',
details: 'password-auth-not-active',
});
});
it('should return 400 when active method is password but password is empty', async () => {
bootstrapPassword('oldpassword');
const res = await request(app)
.post('/change-password')
.set('x-actual-token', sessionToken)
.send({ password: '' });
expect(res.statusCode).toEqual(400);
expect(res.body).toEqual({ status: 'error', reason: 'invalid-password' });
});
it('should return 200 when active method is password and new password is valid', async () => {
bootstrapPassword('oldpassword');
const res = await request(app)
.post('/change-password')
.set('x-actual-token', sessionToken)
.send({ password: 'newpassword' });
expect(res.statusCode).toEqual(200);
expect(res.body).toEqual({ status: 'ok', data: {} });
});
});
describe('getLoginMethod()', () => {
afterEach(() => {
clearAuth();
});
it('returns the active DB method when no req is provided', () => {
insertAuthRow('password', 1);
expect(getLoginMethod(undefined)).toBe('password');
});
it('honors a client-requested method when it is active in DB', () => {
insertAuthRow('openid', 1);
const req = { body: { loginMethod: 'openid' } };
expect(getLoginMethod(req)).toBe('openid');
});
it('ignores a client-requested method that is inactive in DB', () => {
insertAuthRow('openid', 1);
insertAuthRow('password', 0);
const req = { body: { loginMethod: 'password' } };
expect(getLoginMethod(req)).toBe('openid');
});
it('ignores a client-requested method that is not in DB', () => {
insertAuthRow('openid', 1);
const req = { body: { loginMethod: 'password' } };
expect(getLoginMethod(req)).toBe('openid');
});
it('falls back to config default when auth table is empty and no req', () => {
// auth table is empty — getActiveLoginMethod() returns undefined
// config default for loginMethod is 'password'
expect(getLoginMethod(undefined)).toBe('password');
});
});
describe('/server-prefs', () => {
describe('POST /server-prefs', () => {
let adminUserId, basicUserId, adminSessionToken, basicSessionToken;

View File

@@ -1,6 +0,0 @@
---
category: Bugfixes
authors: [LeviBorodenko]
---
[Mobile] Show running balance on upcoming transactions when respective setting is toggled

View File

@@ -1,6 +0,0 @@
---
category: Bugfixes
authors: [Juulz]
---
Make mobile account page colors more consistent

View File

@@ -1,6 +0,0 @@
---
category: Maintenance
authors: [jfdoming]
---
Move migrations CI script to typescript + ci-actions

View File

@@ -1,6 +0,0 @@
---
category: Bugfixes
authors: [mibragimov]
---
Fix CSV import incorrectly parsing transaction amounts that contain trailing whitespace (e.g. amounts from Excel-saved CSV files).

View File

@@ -1,6 +0,0 @@
---
category: Maintenance
authors: [MatissJanis]
---
Establish centralized AI governance documentation for commit and pull request standards.

View File

@@ -0,0 +1,6 @@
---
category: Bugfixes
authors: [MatissJanis]
---
Fixed a privilege escalation issue affecting password changes

View File

@@ -39,8 +39,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "@actual-app/ci-actions@workspace:packages/ci-actions"
dependencies:
extensionless: "npm:^2.0.6"
typescript: "npm:^5.9.3"
vitest: "npm:^4.0.18"
languageName: unknown
linkType: soft
@@ -16033,13 +16031,6 @@ __metadata:
languageName: node
linkType: hard
"extensionless@npm:^2.0.6":
version: 2.0.6
resolution: "extensionless@npm:2.0.6"
checksum: 10/4a264600d9ff811534b35a66ff59eb075ca5c1ae4f25213bfa71d26a5e28ba5188a0d743f4e1dc8255cf9739258d5d374c6f757957faf4fe0d4ec5f57f51034f
languageName: node
linkType: hard
"extract-zip@npm:^2.0.1":
version: 2.0.1
resolution: "extract-zip@npm:2.0.1"