From 80b5d56010665604ee517918fc4e059473e4e482 Mon Sep 17 00:00:00 2001 From: null Date: Sat, 6 Jun 2026 15:08:33 -0500 Subject: [PATCH] feat(sync): rate limit sync/backfill endpoints to 10 per 15 minutes --- middleware/rateLimiter.js | 14 ++++++++++++++ routes/dataSources.js | 7 ++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/middleware/rateLimiter.js b/middleware/rateLimiter.js index bee0077..5c73bf0 100644 --- a/middleware/rateLimiter.js +++ b/middleware/rateLimiter.js @@ -63,6 +63,18 @@ const demoDataLimiter = makeLimiter( 'Too many demo data clear operations. Please try again in 15 minutes.', ); +// 10 sync/backfill requests per 15 minutes per user — prevents SimpleFIN hammering +const syncLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 10, + standardHeaders: 'draft-7', + legacyHeaders: false, + keyGenerator: (req) => req.user?.id?.toString() || req.ip, + handler(req, res) { + res.status(429).json({ error: 'Too many sync requests. Please try again in 15 minutes.' }); + }, +}); + // ── Export all limiters plus reset function ──────────────────────────────────── const allLimiters = [ loginLimiter, @@ -73,6 +85,7 @@ const allLimiters = [ oidcLimiter, backupOperationLimiter, demoDataLimiter, + syncLimiter, ]; function resetStores() { @@ -92,5 +105,6 @@ module.exports = { oidcLimiter, backupOperationLimiter, demoDataLimiter, + syncLimiter, resetStores, }; diff --git a/routes/dataSources.js b/routes/dataSources.js index a3bc1f2..bb789c1 100644 --- a/routes/dataSources.js +++ b/routes/dataSources.js @@ -7,6 +7,7 @@ const { decorateDataSource, ensureManualDataSource } = require('../services/tran const { connectSimplefin, syncDataSource, backfillDataSource, disconnectDataSource } = require('../services/bankSyncService'); const { sanitizeErrorMessage } = require('../services/simplefinService'); const { getBankSyncConfig } = require('../services/bankSyncConfigService'); +const { syncLimiter } = require('../middleware/rateLimiter'); const VALID_TYPES = new Set(['manual', 'file_import', 'provider_sync']); const VALID_STATUSES = new Set(['active', 'inactive', 'error']); @@ -207,7 +208,7 @@ router.put('/:sourceId/accounts/:accountId', (req, res) => { // ─── POST /api/data-sources/:id/sync ───────────────────────────────────────── -router.post('/:id/sync', async (req, res) => { +router.post('/:id/sync', syncLimiter, async (req, res) => { if (!getBankSyncConfig().enabled) { return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); } @@ -230,7 +231,7 @@ router.post('/:id/sync', async (req, res) => { // ─── POST /api/data-sources/sync-all ───────────────────────────────────────── // Syncs every SimpleFIN source for the current user. Returns aggregated stats. -router.post('/sync-all', async (req, res) => { +router.post('/sync-all', syncLimiter, async (req, res) => { if (!getBankSyncConfig().enabled) { return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); } @@ -284,7 +285,7 @@ router.post('/sync-all', async (req, res) => { // ─── POST /api/data-sources/:id/backfill ───────────────────────────────────── -router.post('/:id/backfill', async (req, res) => { +router.post('/:id/backfill', syncLimiter, async (req, res) => { if (!getBankSyncConfig().enabled) { return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); }