mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(auth): widen the per-IP login throttle to 50, keep the per-account lockout at 10
One AUTH_FAILURE_MAX (10 per 15 min) gated the single-user per-IP bucket, the multi-user per-IP pre-gate, the per-username lockout and the hook-secret bucket. Clients sharing one egress IP (NAT, a tunnel, an automated client retrying) were throttled after 10 failures in total. Add AUTH_IP_FAILURE_MAX = 50 and use it for the two per-IP login buckets only. The per-username lockout and the hook-secret bucket keep AUTH_FAILURE_MAX = 10, so guessing against any one account, or against the hook secret, is no easier than before.
This commit is contained in:
1 parent
002de32a08
commit
ebb9e00de9
5 files changed
+33
-6
No files matched your search
@@ -15,6 +15,7 @@ import { WebServer } from '../src/web/server.js';
|
||||
import { TmuxManager } from '../src/tmux-manager.js';
|
||||
import { SettingsUpdateSchema } from '../src/web/schemas.js';
|
||||
import { getHookSecret, HOOK_SECRET_HEADER } from '../src/config/hook-secret.js';
|
||||
import { AUTH_FAILURE_MAX, AUTH_IP_FAILURE_MAX } from '../src/config/auth-config.js';
|
||||
|
||||
const TEST_USER = 'admin';
|
||||
const TEST_PASS = 'test-password-12345';
|
||||
@@ -46,7 +47,7 @@ async function getSessionCookie(baseUrl: string): Promise<string> {
|
||||
}
|
||||
|
||||
async function exhaustAuthFailures(baseUrl: string, prefix: string): Promise<void> {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let i = 0; i < AUTH_IP_FAILURE_MAX; i++) {
|
||||
const res = await fetch(`${baseUrl}/api/status`, {
|
||||
headers: { Authorization: basicAuthHeader(TEST_USER, `${prefix}-${i}`) },
|
||||
});
|
||||
@@ -207,6 +208,20 @@ describe('Auth Security', () => {
|
||||
expect(res.headers.get('retry-after')).toMatch(/^\d+$/);
|
||||
});
|
||||
|
||||
it('leaves per-IP headroom above the per-account cap before throttling', async () => {
|
||||
// One egress IP (NAT, tunnel, an automated client) can carry more failures
|
||||
// than one person does, so the per-IP login bucket is wider than the
|
||||
// per-username one.
|
||||
expect(AUTH_IP_FAILURE_MAX).toBe(50);
|
||||
expect(AUTH_IP_FAILURE_MAX).toBeGreaterThan(AUTH_FAILURE_MAX);
|
||||
for (let i = 0; i <= AUTH_FAILURE_MAX; i++) {
|
||||
const res = await fetch(`${rateBaseUrl}/api/status`, {
|
||||
headers: { Authorization: basicAuthHeader(TEST_USER, `ip-headroom-${i}`) },
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow an existing valid session cookie during auth failure lockout', async () => {
|
||||
const cookie = await getSessionCookie(rateBaseUrl);
|
||||
await exhaustAuthFailures(rateBaseUrl, 'cod21-cookie');
|
||||
|
||||
Reference in new issue
Block a user