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>
This commit is contained in:
Cohee
2025-09-04 20:52:23 +03:00
committed by GitHub
parent e871886b13
commit d134abd50e
6 changed files with 95 additions and 0 deletions
+12
View File
@@ -94,6 +94,18 @@ autheliaAuth: false
# the username and passwords for basic auth are the same as those
# for the individual accounts
perUserBasicAuth: false
# Host whitelist configuration. Recommended if you're using a listen mode
hostWhitelist:
# Enable or disable host whitelisting
enabled: false
# Scan incoming requests for potential host header spoofing
scan: true
# List of allowed hosts. Do not include localhost or IPs, these are safe.
# Use a dot to create subdomain patterns.
# Examples:
# - example.com
# - .trycloudflare.com
hosts: []
# User session timeout *in seconds* (defaults to 24 hours).
## Set to a positive number to expire session after a certain time of inactivity
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>Forbidden</title>
</head>
<body>
<h1>Forbidden</h1>
<p>
If you are the system administrator, add the hostname you are accessing from to the
host whitelist, or disable host whitelisting in the
<code>config.yaml</code> file located in the root directory of your installation.
</p>
<hr />
<p>
<em>Access from this host is not allowed. This attempt has been logged.</em>
</p>
</body>
</html>
+10
View File
@@ -64,6 +64,7 @@
"handlebars": "^4.7.8",
"helmet": "^8.1.0",
"highlight.js": "^11.11.1",
"host-validation-middleware": "^0.1.1",
"html-entities": "^2.6.0",
"iconv-lite": "^0.6.3",
"ip-matching": "^2.1.2",
@@ -5249,6 +5250,15 @@
"node": ">=12.0.0"
}
},
"node_modules/host-validation-middleware": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/host-validation-middleware/-/host-validation-middleware-0.1.1.tgz",
"integrity": "sha512-fakcpp+x4nbP0fACY5gaHWpaOfstq3w8uB6wvhbPBLqH9GV/tdiM9Ht5mclZVbUuPLGBw1bkH5yyTD6HZq057g==",
"license": "MIT",
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/html-entities": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
+1
View File
@@ -54,6 +54,7 @@
"handlebars": "^4.7.8",
"helmet": "^8.1.0",
"highlight.js": "^11.11.1",
"host-validation-middleware": "^0.1.1",
"html-entities": "^2.6.0",
"iconv-lite": "^0.6.3",
"ip-matching": "^2.1.2",
+48
View File
@@ -0,0 +1,48 @@
import path from 'node:path';
import { color, getConfigValue, safeReadFileSync } from '../util.js';
import { serverDirectory } from '../server-directory.js';
import { isHostAllowed, hostValidationMiddleware } from 'host-validation-middleware';
const knownHosts = new Set();
const maxKnownHosts = 1000;
const hostWhitelistEnabled = !!getConfigValue('hostWhitelist.enabled', false);
const hostWhitelist = Object.freeze(getConfigValue('hostWhitelist.hosts', []));
const hostWhitelistScan = !!getConfigValue('hostWhitelist.scan', false, 'boolean');
const hostNotAllowedHtml = safeReadFileSync(path.join(serverDirectory, 'public/error/host-not-allowed.html'))?.toString() ?? '';
const validationMiddleware = hostValidationMiddleware({
allowedHosts: hostWhitelist,
generateErrorMessage: () => hostNotAllowedHtml,
errorResponseContentType: 'text/html',
});
/**
* Middleware to validate remote hosts.
* Useful to protect against DNS rebinding attacks.
* @param {import('express').Request} req Request
* @param {import('express').Response} res Response
* @param {import('express').NextFunction} next Next middleware
*/
export default function hostWhitelistMiddleware(req, res, next) {
const hostValue = req.headers.host;
if (hostWhitelistScan && !isHostAllowed(hostValue, hostWhitelist) && !knownHosts.has(hostValue) && knownHosts.size < maxKnownHosts) {
const isFirstWarning = knownHosts.size === 0;
console.warn(color.red('Request from untrusted host:'), hostValue);
console.warn(`If you trust this host, you can add it to ${color.yellow('hostWhitelist.hosts')} in config.yaml`);
if (!hostWhitelistEnabled && isFirstWarning) {
console.warn(`To protect against host spoofing, consider setting ${color.yellow('hostWhitelist.enabled')} to true`);
}
if (isFirstWarning) {
console.warn(`To disable this warning, set ${color.yellow('hostWhitelist.scan')} to false`);
}
knownHosts.add(hostValue);
}
if (!hostWhitelistEnabled) {
return next();
}
return validationMiddleware(req, res, next);
}
+3
View File
@@ -46,6 +46,7 @@ import multerMonkeyPatch from './middleware/multerMonkeyPatch.js';
import initRequestProxy from './request-proxy.js';
import cacheBuster from './middleware/cacheBuster.js';
import corsProxyMiddleware from './middleware/corsProxy.js';
import hostWhitelistMiddleware from './middleware/hostWhitelist.js';
import {
getVersion,
color,
@@ -116,6 +117,8 @@ if (cliArgs.whitelistMode) {
app.use(whitelistMiddleware);
}
app.use(hostWhitelistMiddleware);
if (cliArgs.listen) {
app.use(accessLoggerMiddleware());
}