fix: cap the size of a synced settings blob

This commit is contained in:
Linus Rath
2026-10-19 01:15:04 +02:00
parent c0b6519136
commit 044c98e6d2
2 changed files with 29 additions and 1 deletions
+13 -1
View File
@@ -12,6 +12,9 @@ import { MAX_ACCOUNT_SLOTS } from '@/lib/account-utils';
import { mergeStoredTemplates } from '@/lib/template-merge';
import { mergeStoredSubscriptions } from '@/lib/calendar-subscription-sync';
/** Largest settings blob a client may store (templates and signatures included). */
const SETTINGS_MAX_BYTES = 2 * 1024 * 1024;
function classifyError(error: unknown): { message: string; status: number } {
const code = (error as NodeJS.ErrnoException).code;
const msg = error instanceof Error ? error.message : 'Unknown error';
@@ -142,7 +145,16 @@ export async function POST(request: NextRequest) {
}
try {
const { username, serverUrl, settings } = await request.json();
// One settings blob per account, read back on every sync: bound it
// before parsing, the way the account archive is.
if (Number(request.headers.get('content-length') ?? 0) > SETTINGS_MAX_BYTES) {
return NextResponse.json({ error: 'Settings are too large' }, { status: 413 });
}
const text = await request.text();
if (text.length > SETTINGS_MAX_BYTES) {
return NextResponse.json({ error: 'Settings are too large' }, { status: 413 });
}
const { username, serverUrl, settings } = JSON.parse(text);
if (!username || !serverUrl || !settings) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}
@@ -128,4 +128,20 @@ describe('settings route keys on the account the credential belongs to', () => {
}));
expect([...store.keys()]).toEqual([`alice@example.org|${SERVER}`]);
});
it('refuses a settings blob over the size cap before parsing it', async () => {
// One blob per account, re-read on every sync: it was accepted at any size.
await signIn({ username: 'alice@example.org' });
const { POST } = await import('@/app/api/settings/route');
const big = JSON.stringify({ username: 'alice@example.org', serverUrl: SERVER, settings: { signature: 'x'.repeat(2 * 1024 * 1024 + 10) } });
const res = await POST(new NextRequest('https://webmail.example/api/settings', { method: 'POST', headers: SAME_ORIGIN, body: big }));
expect(res.status).toBe(413);
expect(store.size).toBe(0);
const claimed = await POST(new NextRequest('https://webmail.example/api/settings', {
method: 'POST',
headers: { ...SAME_ORIGIN, 'content-length': String(3 * 1024 * 1024) },
body: JSON.stringify({ username: 'alice@example.org', serverUrl: SERVER, settings: { theme: 'dark' } }),
}));
expect(claimed.status).toBe(413);
});
});