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 -0Ignore whitespace
default/config.yaml+12 -0
@@ -94,6 +94,18 @@ autheliaAuth: false
94# the username and passwords for basic auth are the same as those94# the username and passwords for basic auth are the same as those
95# for the individual accounts95# for the individual accounts
96perUserBasicAuth: false96perUserBasicAuth: false
97# Host whitelist configuration. Recommended if you're using a listen mode
98hostWhitelist:
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
98# User session timeout *in seconds* (defaults to 24 hours).110# User session timeout *in seconds* (defaults to 24 hours).
99## Set to a positive number to expire session after a certain time of inactivity111## 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 @@
64 "handlebars": "^4.7.8",64 "handlebars": "^4.7.8",
65 "helmet": "^8.1.0",65 "helmet": "^8.1.0",
66 "highlight.js": "^11.11.1",66 "highlight.js": "^11.11.1",
67 "host-validation-middleware": "^0.1.1",
67 "html-entities": "^2.6.0",68 "html-entities": "^2.6.0",
68 "iconv-lite": "^0.6.3",69 "iconv-lite": "^0.6.3",
69 "ip-matching": "^2.1.2",70 "ip-matching": "^2.1.2",
@@ -5249,6 +5250,15 @@
5249 "node": ">=12.0.0"5250 "node": ">=12.0.0"
5250 }5251 }
5251 },5252 },
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 },
5252 "node_modules/html-entities": {5262 "node_modules/html-entities": {
5253 "version": "2.6.0",5263 "version": "2.6.0",
5254 "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",5264 "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
package.json+1 -0
@@ -54,6 +54,7 @@
54 "handlebars": "^4.7.8",54 "handlebars": "^4.7.8",
55 "helmet": "^8.1.0",55 "helmet": "^8.1.0",
56 "highlight.js": "^11.11.1",56 "highlight.js": "^11.11.1",
57 "host-validation-middleware": "^0.1.1",
57 "html-entities": "^2.6.0",58 "html-entities": "^2.6.0",
58 "iconv-lite": "^0.6.3",59 "iconv-lite": "^0.6.3",
59 "ip-matching": "^2.1.2",60 "ip-matching": "^2.1.2",
src/middleware/hostWhitelist.js+48 -0
@@ -0,0 +1,48 @@
1import path from 'node:path';
2import { color, getConfigValue, safeReadFileSync } from '../util.js';
3import { serverDirectory } from '../server-directory.js';
4import { isHostAllowed, hostValidationMiddleware } from 'host-validation-middleware';
5
6const knownHosts = new Set();
7const maxKnownHosts = 1000;
8
9const hostWhitelistEnabled = !!getConfigValue('hostWhitelist.enabled', false);
10const hostWhitelist = Object.freeze(getConfigValue('hostWhitelist.hosts', []));
11const hostWhitelistScan = !!getConfigValue('hostWhitelist.scan', false, 'boolean');
12
13const hostNotAllowedHtml = safeReadFileSync(path.join(serverDirectory, 'public/error/host-not-allowed.html'))?.toString() ?? '';
14
15const 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 */
28export 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';
46import initRequestProxy from './request-proxy.js';46import initRequestProxy from './request-proxy.js';
47import cacheBuster from './middleware/cacheBuster.js';47import cacheBuster from './middleware/cacheBuster.js';
48import corsProxyMiddleware from './middleware/corsProxy.js';48import corsProxyMiddleware from './middleware/corsProxy.js';
49import hostWhitelistMiddleware from './middleware/hostWhitelist.js';
49import {50import {
50 getVersion,51 getVersion,
51 color,52 color,
@@ -116,6 +117,8 @@ if (cliArgs.whitelistMode) {
116 app.use(whitelistMiddleware);117 app.use(whitelistMiddleware);
117}118}
118119
120app.use(hostWhitelistMiddleware);
121
119if (cliArgs.listen) {122if (cliArgs.listen) {
120 app.use(accessLoggerMiddleware());123 app.use(accessLoggerMiddleware());
121}124}