← back to NationalPaperHangers
admin/uploads: per-installer rate limit (30/hr) keyed on installer.id — protects upload dir from runaway/hostile authenticated client
8c19628eb4eb42dd6ec2ad15c7f1d3b91219b3df · 2026-05-06 11:47:59 -0700 · Steve
Files touched
Diff
commit 8c19628eb4eb42dd6ec2ad15c7f1d3b91219b3df
Author: Steve <steve@designerwallcoverings.com>
Date: Wed May 6 11:47:59 2026 -0700
admin/uploads: per-installer rate limit (30/hr) keyed on installer.id — protects upload dir from runaway/hostile authenticated client
---
routes/admin.js | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/routes/admin.js b/routes/admin.js
index f92c997..3b3be36 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -3,6 +3,7 @@ const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const multer = require('multer');
+const rateLimit = require('express-rate-limit');
const db = require('../lib/db');
const { requireInstaller, requirePaidTier } = require('../lib/auth');
const stripe = require('../lib/stripe');
@@ -645,7 +646,18 @@ router.post('/template', async (req, res, next) => {
// Drag-drop image upload (used by /admin/template hero + portfolio)
// ===================================================================
-router.post('/uploads', (req, res, next) => {
+// Per-installer rate limit on uploads — caps a runaway/hostile authenticated
+// installer at 30 uploads / hour. Keyed on installer.id (not IP) so studios
+// behind shared NATs don't share a bucket. requireInstaller above guarantees
+// req.installer is present at this point.
+const uploadLimiter = rateLimit({
+ windowMs: 60 * 60 * 1000, max: 30,
+ standardHeaders: true, legacyHeaders: false,
+ keyGenerator: (req) => 'inst:' + (req.installer && req.installer.id || 'anon'),
+ message: { ok: false, error: 'Upload rate limit reached — try again in an hour.' }
+});
+
+router.post('/uploads', uploadLimiter, (req, res, next) => {
upload.single('file')(req, res, async (err) => {
if (err) {
const msg = err.message === 'UNSUPPORTED_TYPE'
← a7e5ecf watch page: ops-curated YT videos about pro installations +
·
back to NationalPaperHangers
·
claude-codex r3 cleanup: iframe width/height (CLS fix, MEDIU dd2fcde →