Feat. Added rate limiting

This commit is contained in:
callum5892
2026-03-10 23:02:15 +00:00
parent b2bd85aa2c
commit b1b9d5772e
4 changed files with 93 additions and 2 deletions

View File

@@ -7,3 +7,12 @@ MINIO_USE_SSL=false
MINIO_ACCESS_KEY=minioadmin MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=pastes MINIO_BUCKET=pastes
# Set true when running behind a reverse proxy (Nginx, Cloudflare, etc.)
TRUST_PROXY=false
# API rate limiting
RATE_LIMIT_READ_WINDOW_MS=60000
RATE_LIMIT_READ_MAX=240
RATE_LIMIT_CREATE_WINDOW_MS=600000
RATE_LIMIT_CREATE_MAX=40

31
package-lock.json generated
View File

@@ -1,16 +1,18 @@
{ {
"name": "minipaste", "name": "binit.app",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "minipaste", "name": "binit.app",
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express": "^4.21.0", "express": "^4.21.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.0.0",
"minio": "^8.0.0" "minio": "^8.0.0"
} }
}, },
@@ -295,6 +297,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
@@ -336,6 +339,21 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-rate-limit": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": ">= 4.11"
}
},
"node_modules/fast-xml-builder": { "node_modules/fast-xml-builder": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.0.tgz",
@@ -498,6 +516,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/helmet": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",

View File

@@ -12,7 +12,9 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express-rate-limit": "^7.5.0",
"express": "^4.21.0", "express": "^4.21.0",
"helmet": "^8.0.0",
"minio": "^8.0.0" "minio": "^8.0.0"
} }
} }

View File

@@ -1,15 +1,68 @@
require('dotenv').config(); require('dotenv').config();
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const { ensureBucket } = require('./lib/minio'); const { ensureBucket } = require('./lib/minio');
const pastesRouter = require('./routes/pastes'); const pastesRouter = require('./routes/pastes');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
function envInt(name, fallback) {
const raw = process.env[name];
if (!raw) {
return fallback;
}
const parsed = Number.parseInt(raw, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
const READ_LIMIT_WINDOW_MS = envInt('RATE_LIMIT_READ_WINDOW_MS', 60 * 1000);
const READ_LIMIT_MAX = envInt('RATE_LIMIT_READ_MAX', 240);
const CREATE_LIMIT_WINDOW_MS = envInt('RATE_LIMIT_CREATE_WINDOW_MS', 10 * 60 * 1000);
const CREATE_LIMIT_MAX = envInt('RATE_LIMIT_CREATE_MAX', 40);
app.disable('x-powered-by');
// If deployed behind a reverse proxy/load balancer, set TRUST_PROXY=true.
if (process.env.TRUST_PROXY === 'true') {
app.set('trust proxy', 1);
}
app.use(helmet({
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false,
}));
const createPasteLimiter = rateLimit({
windowMs: CREATE_LIMIT_WINDOW_MS,
max: CREATE_LIMIT_MAX,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many paste creations. Try again later.' },
});
const readPasteLimiter = rateLimit({
windowMs: READ_LIMIT_WINDOW_MS,
max: READ_LIMIT_MAX,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests. Slow down.' },
});
app.use(express.json({ limit: '110kb' })); app.use(express.json({ limit: '110kb' }));
app.use(express.urlencoded({ extended: false, limit: '110kb' }));
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public')));
app.use('/api/pastes', readPasteLimiter);
app.use('/api/pastes', (req, res, next) => {
if (req.method === 'POST') {
return createPasteLimiter(req, res, next);
}
next();
});
app.use('/api/pastes', pastesRouter); app.use('/api/pastes', pastesRouter);
// Serve the view page for any paste URL // Serve the view page for any paste URL