mirror of
https://github.com/actualbudget/actual.git
synced 2026-05-06 07:01:45 -05:00
Add rate limiting to authentication endpoints (#7432)
* [AI] Add rate limiting to authentication endpoints Add strict rate limiting (5 attempts per 15 minutes) to /account/login, /account/bootstrap, and /account/change-password endpoints to prevent brute-force password attacks. Uses express-rate-limit as route-level middleware on auth-sensitive routes only. https://claude.ai/code/session_017SHnNCn93RzxpvEEPJAZUZ * [AI] Add release notes and remove rate limit from /change-password Add upcoming release notes file for the auth rate limiting feature. Remove rate limiting from /change-password since it already requires a valid admin session token. https://claude.ai/code/session_017SHnNCn93RzxpvEEPJAZUZ --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
4efa8bba04
commit
20ba076a51
@@ -1,4 +1,5 @@
|
||||
import express from 'express';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
import {
|
||||
bootstrap,
|
||||
@@ -21,7 +22,16 @@ app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(errorMiddleware);
|
||||
app.use(requestLoggerMiddleware);
|
||||
export { app as handlers };
|
||||
|
||||
const authRateLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // 5 attempts per window
|
||||
legacyHeaders: false,
|
||||
standardHeaders: true,
|
||||
message: { status: 'error', reason: 'too-many-requests' },
|
||||
});
|
||||
|
||||
export { app as handlers, authRateLimiter };
|
||||
|
||||
// Non-authenticated endpoints:
|
||||
//
|
||||
@@ -45,7 +55,7 @@ app.get('/needs-bootstrap', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/bootstrap', async (req, res) => {
|
||||
app.post('/bootstrap', authRateLimiter, async (req, res) => {
|
||||
const boot = await bootstrap(req.body);
|
||||
|
||||
if (boot?.error) {
|
||||
@@ -60,7 +70,7 @@ app.get('/login-methods', (req, res) => {
|
||||
res.send({ status: 'ok', methods });
|
||||
});
|
||||
|
||||
app.post('/login', async (req, res) => {
|
||||
app.post('/login', authRateLimiter, async (req, res) => {
|
||||
const loginMethod = getLoginMethod(req);
|
||||
console.log('Logging in via ' + loginMethod);
|
||||
let tokenRes = null;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getAccountDb, getLoginMethod, getServerPrefs } from './account-db';
|
||||
import { bootstrapPassword } from './accounts/password';
|
||||
import { handlers as app } from './app-account';
|
||||
import { handlers as app, authRateLimiter } from './app-account';
|
||||
|
||||
const ADMIN_ROLE = 'ADMIN';
|
||||
const BASIC_ROLE = 'BASIC';
|
||||
@@ -45,6 +45,51 @@ const clearAuth = () => {
|
||||
getAccountDb().mutate('DELETE FROM auth');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
authRateLimiter.resetKey('127.0.0.1');
|
||||
});
|
||||
|
||||
describe('auth rate limiting', () => {
|
||||
it('should return 429 after exceeding the rate limit on /login', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await request(app).post('/login').send({ password: 'wrong' });
|
||||
}
|
||||
|
||||
const res = await request(app).post('/login').send({ password: 'wrong' });
|
||||
|
||||
expect(res.statusCode).toEqual(429);
|
||||
expect(res.body).toEqual({
|
||||
status: 'error',
|
||||
reason: 'too-many-requests',
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply the same rate limit across /login and /bootstrap', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await request(app).post('/login').send({ password: 'wrong' });
|
||||
}
|
||||
|
||||
const res = await request(app)
|
||||
.post('/bootstrap')
|
||||
.send({ password: 'test' });
|
||||
|
||||
expect(res.statusCode).toEqual(429);
|
||||
expect(res.body).toEqual({
|
||||
status: 'error',
|
||||
reason: 'too-many-requests',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not rate limit non-auth endpoints', async () => {
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await request(app).post('/login').send({ password: 'wrong' });
|
||||
}
|
||||
|
||||
const res = await request(app).get('/needs-bootstrap');
|
||||
expect(res.statusCode).toEqual(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/change-password', () => {
|
||||
let adminUserId,
|
||||
basicUserId,
|
||||
|
||||
6
upcoming-release-notes/7432.md
Normal file
6
upcoming-release-notes/7432.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [actualbudget]
|
||||
---
|
||||
|
||||
Add rate limiting to authentication endpoints to prevent brute-force attacks.
|
||||
Reference in New Issue
Block a user