Implement header based safety features

Using the `helmet` package, a certain set of security critical headers
are set to prevent XSS attacks and the like.
This commit is contained in:
Wolvan 2022-01-30 16:59:11 +01:00
parent 26a42333fe
commit afc7bbad01
8 changed files with 114 additions and 98 deletions

View file

@ -9,11 +9,11 @@
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/index.css">
<script type="text/javascript">
<script nonce="{{ CORS_SCRIPT_NONCE }}" type="text/javascript">
const MAX_POLL_OPTIONS = "{{ MAX_POLL_OPTIONS }}";
const MAX_CHARACTER_LENGTH = "{{ MAX_CHARACTER_LENGTH }}";
</script>
<script type="text/javascript" src="/static/js/index.js" defer="true" async="true"></script>
<script nonce="{{ CORS_SCRIPT_NONCE }}" type="text/javascript" src="/static/js/index.js" defer="true" async="true"></script>
</head>
<body>
<header>

View file

@ -9,8 +9,8 @@
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/vote.css">
<script type="text/javascript" src="/static/vendor/js/textfit.js" defer="true"></script>
<script type="text/javascript">
<script nonce="{{ CORS_SCRIPT_NONCE }}" type="text/javascript" src="/static/vendor/js/textfit.js" defer="true"></script>
<script nonce="{{ CORS_SCRIPT_NONCE }}" type="text/javascript">
const textFitOptions = {
multiLine: true
};

View file

@ -9,14 +9,14 @@
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/result.css">
<script type="text/javascript" src="/static/vendor/js/textfit.js" defer="true"></script>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js" defer="true"></script>
<script type="text/javascript">
<script nonce="{{ CORS_SCRIPT_NONCE }}" type="text/javascript" src="/static/vendor/js/textfit.js" defer="true"></script>
<script nonce="{{ CORS_SCRIPT_NONCE }}" type="text/javascript" src="https://www.gstatic.com/charts/loader.js" defer="true"></script>
<script nonce="{{ CORS_SCRIPT_NONCE }}" type="text/javascript">
const POLL_VOTE_DATA_STRING = `{{ POLL_OPTION_VOTES }}`;
const POLL_BACKEND_URL = "{{ BACKEND_BASE_PATH }}";
const POLL_ID = "{{ POLL_ID }}";
</script>
<script type="text/javascript" src="/static/js/result.js" defer="true"></script>
<script nonce="{{ CORS_SCRIPT_NONCE }}" type="text/javascript" src="/static/js/result.js" defer="true"></script>
</head>
<body>
<header>

164
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "poll-horse",
"version": "1.0.1",
"version": "1.0.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "poll-horse",
"version": "1.0.1",
"version": "1.0.4",
"license": "MIT",
"dependencies": {
"commander": "^8.3.0",
@ -14,6 +14,7 @@
"cookie-parser": "^1.4.6",
"express": "^4.17.2",
"fs-extra": "^10.0.0",
"helmet": "^5.0.2",
"mysql2": "^2.3.3",
"node-fetch": "^2.6.6",
"node-persist": "^3.1.0"
@ -830,10 +831,16 @@
}
},
"node_modules/chokidar": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
"integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@ -1715,9 +1722,9 @@
}
},
"node_modules/glob": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
@ -1834,6 +1841,14 @@
"he": "bin/he"
}
},
"node_modules/helmet": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz",
"integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg==",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/http-errors": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
@ -2232,32 +2247,32 @@
}
},
"node_modules/mocha": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz",
"integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==",
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.0.tgz",
"integrity": "sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==",
"dev": true,
"dependencies": {
"@ungap/promise-all-settled": "1.1.2",
"ansi-colors": "4.1.1",
"browser-stdout": "1.3.1",
"chokidar": "3.5.2",
"debug": "4.3.2",
"chokidar": "3.5.3",
"debug": "4.3.3",
"diff": "5.0.0",
"escape-string-regexp": "4.0.0",
"find-up": "5.0.0",
"glob": "7.1.7",
"glob": "7.2.0",
"growl": "1.10.5",
"he": "1.2.0",
"js-yaml": "4.1.0",
"log-symbols": "4.1.0",
"minimatch": "3.0.4",
"ms": "2.1.3",
"nanoid": "3.1.25",
"nanoid": "3.2.0",
"serialize-javascript": "6.0.0",
"strip-json-comments": "3.1.1",
"supports-color": "8.1.1",
"which": "2.0.2",
"workerpool": "6.1.5",
"workerpool": "6.2.0",
"yargs": "16.2.0",
"yargs-parser": "20.2.4",
"yargs-unparser": "2.0.0"
@ -2274,29 +2289,6 @@
"url": "https://opencollective.com/mochajs"
}
},
"node_modules/mocha/node_modules/debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/mocha/node_modules/debug/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/mocha/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -2379,9 +2371,9 @@
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
},
"node_modules/nanoid": {
"version": "3.1.25",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz",
"integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
"integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==",
"dev": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
@ -2405,14 +2397,22 @@
}
},
"node_modules/node-fetch": {
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-persist": {
@ -3420,9 +3420,9 @@
}
},
"node_modules/workerpool": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz",
"integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==",
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz",
"integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==",
"dev": true
},
"node_modules/wrap-ansi": {
@ -4149,9 +4149,9 @@
"dev": true
},
"chokidar": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
"integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"requires": {
"anymatch": "~3.1.2",
@ -4844,9 +4844,9 @@
"dev": true
},
"glob": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
@ -4929,6 +4929,11 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"helmet": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz",
"integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg=="
},
"http-errors": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
@ -5220,54 +5225,37 @@
"dev": true
},
"mocha": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz",
"integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==",
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.0.tgz",
"integrity": "sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==",
"dev": true,
"requires": {
"@ungap/promise-all-settled": "1.1.2",
"ansi-colors": "4.1.1",
"browser-stdout": "1.3.1",
"chokidar": "3.5.2",
"debug": "4.3.2",
"chokidar": "3.5.3",
"debug": "4.3.3",
"diff": "5.0.0",
"escape-string-regexp": "4.0.0",
"find-up": "5.0.0",
"glob": "7.1.7",
"glob": "7.2.0",
"growl": "1.10.5",
"he": "1.2.0",
"js-yaml": "4.1.0",
"log-symbols": "4.1.0",
"minimatch": "3.0.4",
"ms": "2.1.3",
"nanoid": "3.1.25",
"nanoid": "3.2.0",
"serialize-javascript": "6.0.0",
"strip-json-comments": "3.1.1",
"supports-color": "8.1.1",
"which": "2.0.2",
"workerpool": "6.1.5",
"workerpool": "6.2.0",
"yargs": "16.2.0",
"yargs-parser": "20.2.4",
"yargs-unparser": "2.0.0"
},
"dependencies": {
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"dev": true,
"requires": {
"ms": "2.1.2"
},
"dependencies": {
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -5341,9 +5329,9 @@
}
},
"nanoid": {
"version": "3.1.25",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz",
"integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
"integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==",
"dev": true
},
"natural-compare": {
@ -5358,9 +5346,9 @@
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"node-fetch": {
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
}
@ -6056,9 +6044,9 @@
"dev": true
},
"workerpool": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz",
"integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==",
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz",
"integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==",
"dev": true
},
"wrap-ansi": {

View file

@ -66,6 +66,7 @@
"cookie-parser": "^1.4.6",
"express": "^4.17.2",
"fs-extra": "^10.0.0",
"helmet": "^5.0.2",
"mysql2": "^2.3.3",
"node-fetch": "^2.6.6",
"node-persist": "^3.1.0"

View file

@ -110,6 +110,10 @@ export default async function init(router: Router, polls: Storage): Promise<void
return null;
}
router.all("*", (req, res, next) => {
res.header("Content-Type", "application/json; charset=utf-8");
next();
});
// #region API
router.get("/api/poll/:id", async (req, res) => {
try {

View file

@ -116,6 +116,10 @@ function xss(unsafe: string) {
}
export default function init(router: Router): void {
router.all("*", (req, res, next) => {
res.header("Content-Type", "text/html; charset=utf-8");
next();
});
router.get("/:id/r", async (req, res) => {
const id = req.params.id;
try {
@ -145,6 +149,7 @@ export default function init(router: Router): void {
"BACKEND_BASE_PATH": (program.opts().backendBaseUrl || ""),
"POLL_OPTION_VOTES": Buffer.from(JSON.stringify(Object.entries(poll.votes))).toString("base64"),
"QR_CODE": `https://chart.googleapis.com/chart?cht=qr&chs=190x190&chld=L|1&chl=${ encodeURIComponent(`${ req.protocol }://${ req.headers.host }/${ id }`) }`,
"CORS_SCRIPT_NONCE": res.locals.cspNonce
});
} catch (error) {
console.log(error);
@ -182,6 +187,7 @@ export default function init(router: Router): void {
"FORM_SUBMISSION_ERROR": xss(req.query.error + ""),
"FORM_SUBMISSION_ERROR_SHOWN_CLASS": req.query.error ? "error-visible" : "",
"QR_CODE": `https://chart.googleapis.com/chart?cht=qr&chs=190x190&chld=L|1&chl=${ encodeURIComponent(`${ req.protocol }://${ req.headers.host }/${ id }`) }`,
"CORS_SCRIPT_NONCE": res.locals.cspNonce
});
} catch (error) {
console.log(error);
@ -214,7 +220,8 @@ export default function init(router: Router): void {
"FORM_CAPTCHA": req.query.captcha === "true" ? "checked" : "",
"FORM_OPTION_DIVS": pollOptionDivs,
"MAX_POLL_OPTIONS": MAX_POLL_OPTIONS,
"MAX_CHARACTER_LENGTH": MAX_CHARACTER_LENGTH
"MAX_CHARACTER_LENGTH": MAX_CHARACTER_LENGTH,
"CORS_SCRIPT_NONCE": res.locals.cspNonce
});
});
}

View file

@ -2,13 +2,15 @@
"use strict";
import loadConfig from "./config-loader";
import { program } from "commander";
import express from "express";
import express, { Response } from "express";
import compression from "compression";
import cookiepaser from "cookie-parser";
import { resolve } from "path";
import FlatFileStorage from "./FlatFileStorage";
import Storage from "./Storage";
import MySQLStorage from "./MySQLStorage";
import helmet from "helmet";
import crypto from "crypto";
async function main(): Promise<void> {
await loadConfig([
@ -32,6 +34,20 @@ async function main(): Promise<void> {
app.use(express.urlencoded({ extended: false }));
app.use(compression());
app.use(cookiepaser());
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString("hex");
next();
});
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "'strict-dynamic'", "https: 'self'", "http: 'self'", (req, res) => `'nonce-${(res as Response).locals.cspNonce}'`],
imgSrc: ["'self'", "data:", "https://chart.googleapis.com"],
}
},
crossOriginEmbedderPolicy: false
}));
const storage: Storage = (opts.useMysql) ?
new MySQLStorage({