Add rate limiting on /send-article
Prevents abuse before exposing the app publicly. Limits each client IP to 20 article sends per hour. trust proxy is set so the real client IP is used (not Nginx's loopback) once the app is behind a reverse proxy.
This commit is contained in:
parent
66bb1f497b
commit
304708fb6c
3 changed files with 48 additions and 4 deletions
36
package-lock.json
generated
36
package-lock.json
generated
|
|
@ -1,18 +1,19 @@
|
|||
{
|
||||
"name": "send_to_kindle",
|
||||
"version": "0.0.2",
|
||||
"name": "send-to-ereader",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "send_to_kindle",
|
||||
"version": "0.0.2",
|
||||
"name": "send-to-ereader",
|
||||
"version": "0.1.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@lesjoursfr/html-to-epub": "^6.1.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.5.2",
|
||||
"jsdom": "^29.1.1",
|
||||
"nodemailer": "^8.0.7",
|
||||
"sanitize-filename": "^1.6.4",
|
||||
|
|
@ -1774,6 +1775,24 @@
|
|||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
|
||||
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^10.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
|
|
@ -2287,6 +2306,15 @@
|
|||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"@mozilla/readability": "^0.6.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.5.2",
|
||||
"jsdom": "^29.1.1",
|
||||
"nodemailer": "^8.0.7",
|
||||
"sanitize-filename": "^1.6.4",
|
||||
|
|
|
|||
15
src/app.ts
15
src/app.ts
|
|
@ -6,6 +6,7 @@ import path from "path";
|
|||
import fs from "fs";
|
||||
import sanitizeFilename from "sanitize-filename";
|
||||
import { EpubOptions, EPub } from "@lesjoursfr/html-to-epub";
|
||||
import { rateLimit } from "express-rate-limit";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
|
@ -25,9 +26,22 @@ if (!SMTP_USER || !SMTP_PASSWORD || !MAIL_FROM) {
|
|||
}
|
||||
|
||||
const app = express();
|
||||
|
||||
// Trust the first hop in front of us (Nginx) so req.ip reflects the real
|
||||
// client IP for rate limiting, not the proxy's loopback address.
|
||||
app.set("trust proxy", 1);
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.static("public"));
|
||||
|
||||
const sendArticleLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
limit: 20,
|
||||
standardHeaders: "draft-7",
|
||||
legacyHeaders: false,
|
||||
message: { error: "Too many send requests. Try again in an hour." },
|
||||
});
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: SMTP_PORT,
|
||||
|
|
@ -164,6 +178,7 @@ const emailShape = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|||
|
||||
app.post(
|
||||
"/send-article",
|
||||
sendArticleLimiter,
|
||||
async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
const { url, readerEmail } = req.body;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue