Server: Add host whitelisting (#4476) * Add host whitelisting middleware * Add prompt to enable hostWhitelist * perf: Freeze config array * Update src/middleware/hostWhitelist.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * perf: Add max known hosts limit * Add validation warning disable hint * Add conditional host whitelist middleware based on SSL configuration * Check for cache exhaustion before logging * Revert "Add conditional host whitelist middleware based on SSL configuration" This reverts commit 968104c6f4f2e4b72e1fd8ceff0a4b0ded216d69. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

d134abd50e4a416e3b81233242583b0a23f38320

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
6 files changed, +95 -0Showing whitespace changes
default/config.yaml+12 -0
@@ -94,6 +94,18 @@ autheliaAuth: false
9494# the username and passwords for basic auth are the same as those
9595# for the individual accounts
9696perUserBasicAuth: false
97+# Host whitelist configuration. Recommended if you're using a listen mode
98+hostWhitelist:
99+ # Enable or disable host whitelisting
100+ enabled: false
101+ # Scan incoming requests for potential host header spoofing
102+ scan: true
103+ # List of allowed hosts. Do not include localhost or IPs, these are safe.
104+ # Use a dot to create subdomain patterns.
105+ # Examples:
106+ # - example.com
107+ # - .trycloudflare.com
108+ hosts: []
97109
98110# User session timeout *in seconds* (defaults to 24 hours).
99111## Set to a positive number to expire session after a certain time of inactivity
default/public/error/host-not-allowed.html+21 -0
@@ -0,0 +1,21 @@
1+<!DOCTYPE html>
2+<html>
3+
4+<head>
5+ <title>Forbidden</title>
6+</head>
7+
8+<body>
9+ <h1>Forbidden</h1>
10+ <p>
11+ If you are the system administrator, add the hostname you are accessing from to the
12+ host whitelist, or disable host whitelisting in the
13+ <code>config.yaml</code> file located in the root directory of your installation.
14+ </p>
15+ <hr />
16+ <p>
17+ <em>Access from this host is not allowed. This attempt has been logged.</em>
18+ </p>
19+</body>
20+
21+</html>
package-lock.json+10 -0
@@ -64,6 +64,7 @@
6464 "handlebars": "^4.7.8",
6565 "helmet": "^8.1.0",
6666 "highlight.js": "^11.11.1",
67+ "host-validation-middleware": "^0.1.1",
6768 "html-entities": "^2.6.0",
6869 "iconv-lite": "^0.6.3",
6970 "ip-matching": "^2.1.2",
@@ -5249,6 +5250,15 @@
52495250 "node": ">=12.0.0"
52505251 }
52515252 },
5253+ "node_modules/host-validation-middleware": {
5254+ "version": "0.1.1",
5255+ "resolved": "https://registry.npmjs.org/host-validation-middleware/-/host-validation-middleware-0.1.1.tgz",
5256+ "integrity": "sha512-fakcpp+x4nbP0fACY5gaHWpaOfstq3w8uB6wvhbPBLqH9GV/tdiM9Ht5mclZVbUuPLGBw1bkH5yyTD6HZq057g==",
5257+ "license": "MIT",
5258+ "engines": {
5259+ "node": "^18.0.0 || >=20.0.0"
5260+ }
5261+ },
52525262 "node_modules/html-entities": {
52535263 "version": "2.6.0",
52545264 "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
package.json+1 -0
@@ -54,6 +54,7 @@
5454 "handlebars": "^4.7.8",
5555 "helmet": "^8.1.0",
5656 "highlight.js": "^11.11.1",
57+ "host-validation-middleware": "^0.1.1",
5758 "html-entities": "^2.6.0",
5859 "iconv-lite": "^0.6.3",
5960 "ip-matching": "^2.1.2",
src/middleware/hostWhitelist.js+48 -0
@@ -0,0 +1,48 @@
1+import path from 'node:path';
2+import { color, getConfigValue, safeReadFileSync } from '../util.js';
3+import { serverDirectory } from '../server-directory.js';
4+import { isHostAllowed, hostValidationMiddleware } from 'host-validation-middleware';
5+
6+const knownHosts = new Set();
7+const maxKnownHosts = 1000;
8+
9+const hostWhitelistEnabled = !!getConfigValue('hostWhitelist.enabled', false);
10+const hostWhitelist = Object.freeze(getConfigValue('hostWhitelist.hosts', []));
11+const hostWhitelistScan = !!getConfigValue('hostWhitelist.scan', false, 'boolean');
12+
13+const hostNotAllowedHtml = safeReadFileSync(path.join(serverDirectory, 'public/error/host-not-allowed.html'))?.toString() ?? '';
14+
15+const validationMiddleware = hostValidationMiddleware({
16+ allowedHosts: hostWhitelist,
17+ generateErrorMessage: () => hostNotAllowedHtml,
18+ errorResponseContentType: 'text/html',
19+});
20+
21+/**
22+ * Middleware to validate remote hosts.
23+ * Useful to protect against DNS rebinding attacks.
24+ * @param {import('express').Request} req Request
25+ * @param {import('express').Response} res Response
26+ * @param {import('express').NextFunction} next Next middleware
27+ */
28+export default function hostWhitelistMiddleware(req, res, next) {
29+ const hostValue = req.headers.host;
30+ if (hostWhitelistScan && !isHostAllowed(hostValue, hostWhitelist) && !knownHosts.has(hostValue) && knownHosts.size < maxKnownHosts) {
31+ const isFirstWarning = knownHosts.size === 0;
32+ console.warn(color.red('Request from untrusted host:'), hostValue);
33+ console.warn(`If you trust this host, you can add it to ${color.yellow('hostWhitelist.hosts')} in config.yaml`);
34+ if (!hostWhitelistEnabled && isFirstWarning) {
35+ console.warn(`To protect against host spoofing, consider setting ${color.yellow('hostWhitelist.enabled')} to true`);
36+ }
37+ if (isFirstWarning) {
38+ console.warn(`To disable this warning, set ${color.yellow('hostWhitelist.scan')} to false`);
39+ }
40+ knownHosts.add(hostValue);
41+ }
42+
43+ if (!hostWhitelistEnabled) {
44+ return next();
45+ }
46+
47+ return validationMiddleware(req, res, next);
48+}
src/server-main.js+3 -0
@@ -46,6 +46,7 @@ import multerMonkeyPatch from './middleware/multerMonkeyPatch.js';
4646import initRequestProxy from './request-proxy.js';
4747import cacheBuster from './middleware/cacheBuster.js';
4848import corsProxyMiddleware from './middleware/corsProxy.js';
49+import hostWhitelistMiddleware from './middleware/hostWhitelist.js';
4950import {
5051 getVersion,
5152 color,
@@ -116,6 +117,8 @@ if (cliArgs.whitelistMode) {
116117 app.use(whitelistMiddleware);
117118}
118119
120+app.use(hostWhitelistMiddleware);
121+
119122if (cliArgs.listen) {
120123 app.use(accessLoggerMiddleware());
121124}