fix: express api security
This commit is contained in:
26
package-lock.json
generated
26
package-lock.json
generated
@@ -13,6 +13,7 @@
|
|||||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
"express-rate-limit": "^8.0.1",
|
||||||
"fs-extra": "^11.3.1",
|
"fs-extra": "^11.3.1",
|
||||||
"luxon": "^3.7.1"
|
"luxon": "^3.7.1"
|
||||||
},
|
},
|
||||||
@@ -948,6 +949,23 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-rate-limit": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "10.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/express-rate-limit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">= 4.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express/node_modules/debug": {
|
"node_modules/express/node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
@@ -1401,6 +1419,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||||
|
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
@@ -15,6 +15,7 @@
|
|||||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
"express-rate-limit": "^8.0.1",
|
||||||
"fs-extra": "^11.3.1",
|
"fs-extra": "^11.3.1",
|
||||||
"luxon": "^3.7.1"
|
"luxon": "^3.7.1"
|
||||||
},
|
},
|
||||||
|
@@ -2,11 +2,37 @@ const express = require('express');
|
|||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3000;
|
const port = 3000;
|
||||||
|
|
||||||
app.set('trust proxy', true);
|
// trust proxy:
|
||||||
|
// Used by express-rate-limit to obtain the client's IP address.
|
||||||
|
// '1' means that the first hop (your reverse proxy) is trusted.
|
||||||
|
app.set('trust proxy', 1);
|
||||||
|
|
||||||
|
// --- Security Middleware ---
|
||||||
|
|
||||||
|
// 1. CORS Configuration: Only allow requests from your own domain.
|
||||||
|
// We create a list of allowed origins, so the API works in both
|
||||||
|
// development (localhost) and production (the APP_URL).
|
||||||
|
const allowedOrigins = ['http://localhost:8080'];
|
||||||
|
if (process.env.APP_URL) {
|
||||||
|
allowedOrigins.push(process.env.APP_URL);
|
||||||
|
}
|
||||||
|
const corsOptions = {
|
||||||
|
origin: allowedOrigins,
|
||||||
|
optionsSuccessStatus: 200
|
||||||
|
};
|
||||||
|
app.use(cors(corsOptions));
|
||||||
|
|
||||||
|
// 2. Rate Limiting: Apply to all API requests to prevent abuse.
|
||||||
|
const apiLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100, // Limit each IP to 100 requests per windowMs
|
||||||
|
});
|
||||||
|
app.use('/api/', apiLimiter);
|
||||||
|
|
||||||
// Path to the file where metadata counts will be stored
|
// Path to the file where metadata counts will be stored
|
||||||
const dbPath = path.join(__dirname, '_data', 'views.json');
|
const dbPath = path.join(__dirname, '_data', 'views.json');
|
||||||
@@ -25,17 +51,25 @@ if (initialLikesData.length === 0) {
|
|||||||
fs.writeJsonSync(likesDbPath, {});
|
fs.writeJsonSync(likesDbPath, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
app.use(cors());
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// In production, the server also serves the static site from the `_site` directory.
|
// In production, the server also serves the static site from the `_site` directory.
|
||||||
app.use(express.static('_site'));
|
app.use(express.static('_site'));
|
||||||
|
|
||||||
|
// 3. Input Validation Middleware
|
||||||
|
const validateSlug = (req, res, next) => {
|
||||||
|
const { slug } = req.params;
|
||||||
|
// Basic slug validation: allow alphanumeric characters, hyphens, underscores, and dots.
|
||||||
|
if (!/^[a-z0-9_.-]+$/.test(slug)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid slug format.' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
// --- API Endpoints ---
|
// --- API Endpoints ---
|
||||||
|
|
||||||
// GET: Fetch the view count for a specific post slug
|
// GET: Fetch the view count for a specific post slug
|
||||||
app.get('/api/views/:slug', async (req, res) => {
|
app.get('/api/views/:slug', validateSlug, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { slug } = req.params;
|
const { slug } = req.params;
|
||||||
const views = await fs.readJson(dbPath);
|
const views = await fs.readJson(dbPath);
|
||||||
@@ -48,7 +82,7 @@ app.get('/api/views/:slug', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// GET: Fetch the like count for a specific post slug
|
// GET: Fetch the like count for a specific post slug
|
||||||
app.get('/api/likes/:slug', async (req, res) => {
|
app.get('/api/likes/:slug', validateSlug, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { slug } = req.params;
|
const { slug } = req.params;
|
||||||
const likes = await fs.readJson(likesDbPath);
|
const likes = await fs.readJson(likesDbPath);
|
||||||
@@ -62,7 +96,7 @@ app.get('/api/likes/:slug', async (req, res) => {
|
|||||||
|
|
||||||
|
|
||||||
// POST: Increment the view count for a specific post slug
|
// POST: Increment the view count for a specific post slug
|
||||||
app.post('/api/views/:slug', async (req, res) => {
|
app.post('/api/views/:slug', validateSlug, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { slug } = req.params;
|
const { slug } = req.params;
|
||||||
const views = await fs.readJson(dbPath);
|
const views = await fs.readJson(dbPath);
|
||||||
@@ -76,7 +110,7 @@ app.post('/api/views/:slug', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// POST: Increment the like count for a specific post slug
|
// POST: Increment the like count for a specific post slug
|
||||||
app.post('/api/likes/:slug', async (req, res) => {
|
app.post('/api/likes/:slug', validateSlug, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { slug } = req.params;
|
const { slug } = req.params;
|
||||||
const likes = await fs.readJson(likesDbPath);
|
const likes = await fs.readJson(likesDbPath);
|
||||||
@@ -90,7 +124,7 @@ app.post('/api/likes/:slug', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// DELETE: Decrement the like count for a specific post slug (unlike)
|
// DELETE: Decrement the like count for a specific post slug (unlike)
|
||||||
app.delete('/api/likes/:slug', async (req, res) => {
|
app.delete('/api/likes/:slug', validateSlug, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { slug } = req.params;
|
const { slug } = req.params;
|
||||||
const likes = await fs.readJson(likesDbPath);
|
const likes = await fs.readJson(likesDbPath);
|
||||||
|
Reference in New Issue
Block a user