diff --git a/src/app.js b/src/app.js index f00c814..b9a9c12 100644 --- a/src/app.js +++ b/src/app.js @@ -34,7 +34,7 @@ app.set("view engine", "handlebars"); app.set("views", "./src/views"); -setupMiddleware(app); +app.use(setupMiddleware()); port = process.env.PORT || 3400; diff --git a/src/middleware/applyProductionSecurity.js b/src/middleware/applyProductionSecurity.js new file mode 100644 index 0000000..a5d879a --- /dev/null +++ b/src/middleware/applyProductionSecurity.js @@ -0,0 +1,58 @@ +const express = require("express"); +const helmet = require("helmet"); +const hpp = require("hpp"); +const xssSanitizer = require("./xssSanitizer"); + +function setupMiddleware(app) { + const app = express(); + app.disable("x-powered-by"); + app.set("trust proxy", true); + + app.use((req, res, next) => { + const forwardedIp = req.ip; + const directIp = req.connection.remoteAddress; + + if (req.log?.info) { + req.log.info(`Forwarded IP: ${forwardedIp}`); + req.log.info(`Direct IP: ${directIp}`); + } + next(); + }); + app.use(hpp()); + app.use(xssSanitizer); + // 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: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "https://hcaptcha.com"], + styleSrc: ["'self'", "https:"], + imgSrc: [ + "'self'", + "data:", + "https://licensebuttons.net", + "https://cdn.jsdelivr.net", + ], + frameSrc: ["'self'", "https://newassets.hcaptcha.com"], + objectSrc: ["'none'"], + upgradeInsecureRequests: [], + + // add other directives as needed + }, + }) + ); // Sets secure HTTP headers. Prevents common attacks. + return app; +} + +module.exports = setupMiddleware; diff --git a/src/middleware/index.js b/src/middleware/index.js index c37cbd1..994cff5 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -2,15 +2,12 @@ const express = require("express"); const bodyParser = require("body-parser"); const errorHandler = require("./errorHandler"); -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"); -const xssSanitizer = require("./xssSanitizer"); +const applyProductionSecurity = require("./applyProductionSecurity"); +const validateRequestIntegrity = require("./validateRequestIntegrity"); const { loggingMiddleware, @@ -19,98 +16,21 @@ morganError, } = require("./logging"); -function setupMiddleware(app) { - if (process.env.NODE_ENV === "production") { - app.disable("x-powered-by"); - app.use((req, res, next) => { - const { winstonLogger, manualLogger } = require("../utils/logging"); - winstonLogger.info(req.ip); - console.log(req.ip); - next(); - }); - app.set("trust proxy", true); - app.use((req, res, next) => { - const { winstonLogger, manualLogger } = require("../utils/logging"); - winstonLogger.info(req.ip); - console.log(req.ip); - next(); - }); - app.set("trust-proxy", false); - app.use(hpp()); - app.use(xssSanitizer); - // 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: { - defaultSrc: ["'self'"], - scriptSrc: ["'self'", "https://hcaptcha.com"], - styleSrc: ["'self'", "https:"], - imgSrc: [ - "'self'", - "data:", - "https://licensebuttons.net", - "https://cdn.jsdelivr.net", - ], - frameSrc: ["'self'", "https://newassets.hcaptcha.com"], - objectSrc: ["'none'"], - upgradeInsecureRequests: [], - - // add other directives as needed - }, - }) - ); // Sets secure HTTP headers. Prevents common attacks. - } - app.use(express.json({ limit: "4kb" })); - app.use(bodyParser.urlencoded({ extended: false, limit: "4kb" })); +function setupMiddleware() { + const app = express(); 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(); - }); + + if (process.env.NODE_ENV === "production") { + app.use(applyProductionSecurity()); + } + app.use(express.json({ limit: "4kb" })); + app.use(bodyParser.urlencoded({ extended: false, limit: "4kb" })); + app.use(compression()); + app.use(validateRequestIntegrity()); app.use( "/static", express.static("public", { @@ -130,14 +50,8 @@ err.statusCode = 404; next(err); }); - app.use((err, req, res, next) => { - if (err.code === "EBADCSRFTOKEN") { - err.message = "CSRF token invalid."; - err.statusCode = 403; - } - next(err); - }); app.use(errorHandler); + return app; } module.exports = setupMiddleware; diff --git a/src/middleware/validateRequestIntegrity.js b/src/middleware/validateRequestIntegrity.js new file mode 100644 index 0000000..ed05f72 --- /dev/null +++ b/src/middleware/validateRequestIntegrity.js @@ -0,0 +1,34 @@ +module.exports = (req, res, next) => { + const allowedMethods = ["GET", "POST"]; + const contentLength = parseInt(req.get("content-length") || "0", 10); + const contentType = req.headers["content-type"] || ""; + const headerCount = Object.keys(req.headers).length; + + if (!allowedMethods.includes(req.method)) { + return next( + Object.assign(new Error("Method Not Allowed"), { statusCode: 405 }) + ); + } + + if (contentLength > 4096) { + return next( + Object.assign(new Error("Payload Too Large"), { statusCode: 413 }) + ); + } + + if (contentType.includes("multipart/form-data")) { + return next( + Object.assign(new Error("File uploads are not allowed."), { + statusCode: 400, + }) + ); + } + + if (headerCount > 100) { + return next( + Object.assign(new Error("Too many headers."), { statusCode: 400 }) + ); + } + + next(); +};