diff --git a/package-lock.json b/package-lock.json index cddf7d9..6ef8330 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "gray-matter": "^4.0.3", "hbs": "^4.2.0", "helmet": "^8.1.0", + "hpp": "^0.2.3", "js-beautify": "^1.15.4", "marked": "^15.0.11", "morgan": "^1.10.0", @@ -30,7 +31,8 @@ "rss": "^1.2.2", "sqlite3": "^5.1.7", "winston": "^3.17.0", - "winston-daily-rotate-file": "^5.0.0" + "winston-daily-rotate-file": "^5.0.0", + "xss-clean": "^0.1.4" }, "devDependencies": { "@faker-js/faker": "^9.8.0", @@ -2360,6 +2362,62 @@ "node": ">=18.0.0" } }, + "node_modules/hpp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", + "integrity": "sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw==", + "license": "ISC", + "dependencies": { + "lodash": "^4.17.12", + "type-is": "^1.6.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hpp/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/hpp/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/hpp/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/hpp/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -2830,7 +2888,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/logform": { @@ -5753,6 +5810,21 @@ "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", "license": "MIT" }, + "node_modules/xss-clean": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/xss-clean/-/xss-clean-0.1.4.tgz", + "integrity": "sha512-4hArTFHYxrifK9VXOu/zFvwjTXVjKByPi6woUHb1IqxlX0Z4xtFBRjOhTKuYV/uE1VswbYsIh5vUEYp7MmoISQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "xss-filters": "1.2.7" + } + }, + "node_modules/xss-filters": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/xss-filters/-/xss-filters-1.2.7.tgz", + "integrity": "sha512-KzcmYT/f+YzcYrYRqw6mXxd25BEZCxBQnf+uXTopQDIhrmiaLwO+f+yLsIvvNlPhYvgff8g3igqrBxYh9k8NbQ==" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index e839495..4bdc18b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "gray-matter": "^4.0.3", "hbs": "^4.2.0", "helmet": "^8.1.0", + "hpp": "^0.2.3", "js-beautify": "^1.15.4", "marked": "^15.0.11", "morgan": "^1.10.0", @@ -39,7 +40,8 @@ "rss": "^1.2.2", "sqlite3": "^5.1.7", "winston": "^3.17.0", - "winston-daily-rotate-file": "^5.0.0" + "winston-daily-rotate-file": "^5.0.0", + "xss-clean": "^0.1.4" }, "devDependencies": { "@faker-js/faker": "^9.8.0", diff --git a/src/middleware/index.js b/src/middleware/index.js index 503e9d3..bd86a23 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -5,7 +5,8 @@ const rateLimit = require("express-rate-limit"); const compression = require("compression"); const helmet = require("helmet"); - +const hpp = require("hpp"); +const xss = require("xss-clean"); const routes = require("../routes"); const formatHtml = require("./formatHtml"); const logEvent = require("./analytics.js"); @@ -20,7 +21,20 @@ function setupMiddleware(app) { if (process.env.NODE_ENV === "production") { app.disable("x-powered-by"); + app.use(hpp()); + app.use(xss()); app.use(rateLimit({ windowMs: 1 * 60 * 1000, max: 100 })); + app.use((req, res, next) => { + const host = req.hostname; + if (["127.0.0.1", "localhost"].includes(host)) { + const err = new Error("Forbidden"); + err.statusCode = 403; + return next(err); + } + next(); + }); + app.use(helmet.hsts({ maxAge: 63072000 })); + app.use( helmet.contentSecurityPolicy({ directives: { @@ -42,14 +56,46 @@ }) ); // Sets secure HTTP headers. Prevents common attacks. } - app.use(express.json()); + app.use(express.json({ limit: "4kb" })); + app.use(bodyParser.urlencoded({ extended: false, limit: "4kb" })); app.use(logEvent); app.use(compression()); app.use(morganInfo); app.use(morganWarn); app.use(morganError); app.use(loggingMiddleware); - + app.use((req, res, next) => { + const allowedMethods = ["GET", "POST"]; + if (!allowedMethods.includes(req.method)) { + const err = new Error("Method Not Allowed"); + err.statusCode = 405; + return next(err); + } + next(); + }); + app.use((req, res, next) => { + if (req.get("content-length") > 4096) { + const err = new Error("Payload Too Large"); + err.statusCode = 413; + return next(err); + } + next(); + }); + app.use((req, res, next) => { + const contentType = req.headers["content-type"] || ""; + if (contentType.includes("multipart/form-data")) { + const err = new Error("File uploads are not allowed."); + err.statusCode = 400; + return next(err); + } + next(); + }); + app.use((req, res, next) => { + if (Object.keys(req.headers).length > 100) { + return res.status(400).send("Too many headers."); + } + next(); + }); app.use( "/static", express.static("public", { @@ -62,7 +108,6 @@ }, }) ); - app.use(bodyParser.urlencoded({ extended: true })); app.use(formatHtml); app.use(routes); app.use((req, res, next) => { diff --git a/src/routes/analytics.js b/src/routes/analytics.js index 1d642d5..4bfa7e3 100644 --- a/src/routes/analytics.js +++ b/src/routes/analytics.js @@ -2,26 +2,26 @@ // Route: JavaScript-enabled tracking module.exports = (req, res) => { - const { - url = "", - referrer = "", - userAgent = "", - viewport = "", - loadTime = 0, - event = "", - } = req.body; + // const { + // // url = "", + // // referrer = "", + // userAgent = "", + // viewport = "", + // loadTime = 0, + // event = "", + // } = req.body; - const ip = - req.headers["x-forwarded-for"]?.split(",")[0] || - req.connection.remoteAddress || - ""; - const timestamp = Date.now(); + // const ip = + // req.headers["x-forwarded-for"]?.split(",")[0] || + // req.connection.remoteAddress || + // ""; + // const timestamp = Date.now(); - db.run( - `INSERT INTO analytics (timestamp, url, referrer, user_agent, viewport, load_time, event, ip, js_enabled) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [timestamp, url, referrer, userAgent, viewport, loadTime, event, ip, 1] - ); + // db.run( + // `INSERT INTO analytics (timestamp, url, referrer, user_agent, viewport, load_time, event, ip, js_enabled) + // VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + // [timestamp, url, referrer, userAgent, viewport, loadTime, event, ip, 1] + // ); res.sendStatus(204); }; diff --git a/yarn.lock b/yarn.lock index d263979..3ed3a55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1286,6 +1286,14 @@ resolved "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz" integrity sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg== +hpp@^0.2.3: + version "0.2.3" + resolved "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz" + integrity sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw== + dependencies: + lodash "^4.17.12" + type-is "^1.6.12" + http-cache-semantics@^4.1.0: version "4.2.0" resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz" @@ -1571,7 +1579,7 @@ resolved "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== -lodash@^4.17.14: +lodash@^4.17.12, lodash@^4.17.14: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1647,6 +1655,11 @@ resolved "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz" integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + merge-descriptors@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz" @@ -1662,6 +1675,11 @@ resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz" integrity sha512-5k547tI4Cy+Lddr/hdjNbBEWBwSl8EBc5aSdKvedav8DReADgWJzcYiktaRIw3GtGC1jjwldXtTzvqJZmtvC7w== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + mime-types@^3.0.0, mime-types@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz" @@ -1669,6 +1687,13 @@ dependencies: mime-db "^1.54.0" +mime-types@~2.1.24: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime-types@2.1.13: version "2.1.13" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz" @@ -2963,6 +2988,14 @@ dependencies: json-stringify-safe "^5.0.1" +type-is@^1.6.12: + version "1.6.18" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + type-is@^2.0.0, type-is@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz" @@ -3135,6 +3168,18 @@ resolved "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz" integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw== +xss-clean@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/xss-clean/-/xss-clean-0.1.4.tgz" + integrity sha512-4hArTFHYxrifK9VXOu/zFvwjTXVjKByPi6woUHb1IqxlX0Z4xtFBRjOhTKuYV/uE1VswbYsIh5vUEYp7MmoISQ== + dependencies: + xss-filters "1.2.7" + +xss-filters@1.2.7: + version "1.2.7" + resolved "https://registry.npmjs.org/xss-filters/-/xss-filters-1.2.7.tgz" + integrity sha512-KzcmYT/f+YzcYrYRqw6mXxd25BEZCxBQnf+uXTopQDIhrmiaLwO+f+yLsIvvNlPhYvgff8g3igqrBxYh9k8NbQ== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz"