mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-08-04 15:18:27 +00:00
fix: check access list before force SSL redirect
Fixes #5207 - Security issue where Force SSL leaks host existence When both Force SSL and an Access List are active on a Proxy Host, HTTP requests from unauthorized IPs were receiving a 301 redirect instead of being blocked. This allowed attackers to enumerate valid hosts by brute-forcing the Host header. Solution: Use nginx geo module to check IP access before the SSL redirect. Only allowed IPs get redirected to HTTPS; denied IPs fall through to the access phase and receive 403. Changes: - Add geo block template for IP-based access control - Modify _forced_ssl.conf to check geo variable before redirecting - Generate geo config files when access lists are created/updated - Include geo configs at http level in nginx.conf - Create access_geo directory on startup
This commit is contained in:
parent
7747db994d
commit
19086ce709
5 changed files with 89 additions and 0 deletions
|
|
@ -1,4 +1,6 @@
|
|||
import fs from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import batchflow from "batchflow";
|
||||
import _ from "lodash";
|
||||
import errs from "../lib/error.js";
|
||||
|
|
@ -11,6 +13,9 @@ import proxyHostModel from "../models/proxy_host.js";
|
|||
import internalAuditLog from "./audit-log.js";
|
||||
import internalNginx from "./nginx.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const omissions = () => {
|
||||
return ["is_deleted"];
|
||||
};
|
||||
|
|
@ -311,6 +316,13 @@ const internalAccessList = {
|
|||
// do nothing
|
||||
}
|
||||
|
||||
// delete the geo config file
|
||||
try {
|
||||
fs.unlinkSync(`/data/nginx/access_geo/${row.id}.conf`);
|
||||
} catch (_err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// 4. audit log
|
||||
await internalAuditLog.add(access, {
|
||||
action: "deleted",
|
||||
|
|
@ -432,6 +444,7 @@ const internalAccessList = {
|
|||
* @param {Integer} list.id
|
||||
* @param {String} list.name
|
||||
* @param {Array} list.items
|
||||
* @param {Array} list.clients
|
||||
* @returns {Promise}
|
||||
*/
|
||||
build: async (list) => {
|
||||
|
|
@ -482,6 +495,50 @@ const internalAccessList = {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Build geo config file for IP-based access control (used by Force SSL)
|
||||
await internalAccessList.buildGeoConfig(list);
|
||||
},
|
||||
|
||||
/**
|
||||
* Build geo config file for IP-based access control
|
||||
* This is used to check IP access before Force SSL redirect
|
||||
* @param {Object} list
|
||||
* @param {Integer} list.id
|
||||
* @param {String} list.name
|
||||
* @param {Array} list.clients
|
||||
* @returns {Promise}
|
||||
*/
|
||||
buildGeoConfig: async (list) => {
|
||||
const geoDir = '/data/nginx/access_geo';
|
||||
const geoFile = `${geoDir}/${list.id}.conf`;
|
||||
|
||||
// Ensure directory exists
|
||||
try {
|
||||
fs.mkdirSync(geoDir, { recursive: true });
|
||||
} catch (_err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// Remove existing geo file
|
||||
try {
|
||||
fs.unlinkSync(geoFile);
|
||||
} catch (_err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// Only create geo config if there are client IP rules
|
||||
if (list.clients && list.clients.length > 0) {
|
||||
logger.info(`Building Geo config file #${list.id} for: ${list.name}`);
|
||||
|
||||
const renderEngine = utils.getRenderEngine();
|
||||
const template = fs.readFileSync(`${__dirname}/../templates/access_list_geo.conf`, { encoding: 'utf8' });
|
||||
|
||||
const config = await renderEngine.parseAndRender(template, list);
|
||||
fs.writeFileSync(geoFile, config, { encoding: 'utf8' });
|
||||
|
||||
logger.success(`Built Geo config file #${list.id} for: ${list.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,24 @@
|
|||
{% if certificate and certificate_id > 0 -%}
|
||||
{% if ssl_forced == 1 or ssl_forced == true %}
|
||||
{% if access_list %}{% assign clients_size = access_list.clients | size %}{% else %}{% assign clients_size = 0 %}{% endif %}
|
||||
{% if access_list_id > 0 and clients_size > 0 %}
|
||||
# Force SSL (only for allowed IPs - denied IPs will get 403 from access rules)
|
||||
set $redirect_to_ssl 0;
|
||||
if ($scheme = "http") {
|
||||
set $redirect_to_ssl 1;
|
||||
}
|
||||
if ($access_list_{{ access_list_id }}_allowed = 0) {
|
||||
set $redirect_to_ssl 0;
|
||||
}
|
||||
if ($request_uri = /.well-known/acme-challenge/test-challenge) {
|
||||
set $redirect_to_ssl 0;
|
||||
}
|
||||
if ($redirect_to_ssl = 1) {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
{% else %}
|
||||
# Force SSL
|
||||
include conf.d/include/force-ssl.conf;
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
10
backend/templates/access_list_geo.conf
Normal file
10
backend/templates/access_list_geo.conf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Geo-based access control for access list {{ id }}
|
||||
# Used to check IP access before Force SSL redirect
|
||||
geo $access_list_{{ id }}_allowed {
|
||||
default 0;
|
||||
{% for client in clients %}
|
||||
{% if client.directive == "allow" %}
|
||||
{{ client.address }} 1;
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
|
|
@ -72,6 +72,9 @@ http {
|
|||
# Custom
|
||||
include /data/nginx/custom/http_top[.]conf;
|
||||
|
||||
# Access list geo definitions for Force SSL
|
||||
include /data/nginx/access_geo/*.conf;
|
||||
|
||||
# Files generated by NPM
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /data/nginx/default_host/*.conf;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ mkdir -p \
|
|||
/data/nginx/stream \
|
||||
/data/nginx/dead_host \
|
||||
/data/nginx/temp \
|
||||
/data/nginx/access_geo \
|
||||
/data/letsencrypt-acme-challenge \
|
||||
/run/nginx \
|
||||
/tmp/nginx/body \
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue