mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
fix(admin): rate-limit /api/admin/backups/run to 3/min per admin
pg-boss does not deduplicate by payload, so each click on the manual "Backup now" button enqueues a fresh data-backup job. Without a rate-limit, an impatient admin (or compromised admin session via XSS or token theft) can stack dozens of identical fan-out-per-user jobs on the queue. Three runs per minute per admin is plenty for ad-hoc snapshots — the weekly cron in `reminder-worker` already carries the regular cadence. Closes H3 from .planning/phase-6-code-review-findings.md (also addresses M-1 from .planning/phase-6-security-findings.md). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b3f282adab
commit
8e17f227e4
1 file changed
+15
-1
@@ -12,15 +12,29 @@ import { apiHandler, requireAdmin, HttpError } from "@/lib/api-handler";
|
|||||||
import { apiSuccess } from "@/lib/api-response";
|
import { apiSuccess } from "@/lib/api-response";
|
||||||
import { annotate } from "@/lib/logging/context";
|
import { annotate } from "@/lib/logging/context";
|
||||||
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
|
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
|
||||||
|
import { checkRateLimit } from "@/lib/rate-limit";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
const DATA_BACKUP_QUEUE = "data-backup";
|
const DATA_BACKUP_QUEUE = "data-backup";
|
||||||
|
|
||||||
export const POST = apiHandler(async () => {
|
export const POST = apiHandler(async () => {
|
||||||
await requireAdmin();
|
const { user: admin } = await requireAdmin();
|
||||||
annotate({ action: { name: "admin.backups.run" } });
|
annotate({ action: { name: "admin.backups.run" } });
|
||||||
|
|
||||||
|
// pg-boss does not dedupe by payload; without a rate-limit, an
|
||||||
|
// impatient admin (or compromised admin session) can stack dozens of
|
||||||
|
// identical jobs on the queue. Three runs/min per admin is plenty
|
||||||
|
// for ad-hoc snapshots — the weekly cron carries the regular cadence.
|
||||||
|
const rl = await checkRateLimit(
|
||||||
|
`admin-backups-run:${admin.id}`,
|
||||||
|
3,
|
||||||
|
60 * 1000,
|
||||||
|
);
|
||||||
|
if (!rl.allowed) {
|
||||||
|
throw new HttpError(429, "Too many backup runs");
|
||||||
|
}
|
||||||
|
|
||||||
const boss = getGlobalBoss();
|
const boss = getGlobalBoss();
|
||||||
if (!boss) {
|
if (!boss) {
|
||||||
// 503 instead of 500 — the request is well-formed; the worker just
|
// 503 instead of 500 — the request is well-formed; the worker just
|
||||||
|
|||||||
Reference in new issue
Block a user