import pytest from werkzeug.test import EnvironBuilder def _drive(app, headers): """Send one request through the app's REAL ProxyFix instance. The middleware's wrapped application is swapped for a recorder for the duration of the call, so what is measured is the exact ProxyFix object create_app() installed — not a reconstruction of it — without needing a second create_app() (blueprints cannot be registered twice per process) or a route added after the app has served its first request. """ seen = {} def recorder(environ, start_response): seen['host'] = environ.get('HTTP_HOST') seen['scheme'] = environ.get('wsgi.url_scheme') start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'ok'] middleware = app.wsgi_app original = middleware.app middleware.app = recorder try: environ = EnvironBuilder(path='/', headers=headers).get_environ() middleware(environ, lambda *a, **kw: None) finally: middleware.app = original return seen def _observed_host(app, headers): """Return the host the application resolves for a request.""" return _drive(app, headers).get('host') def test_forwarded_host_does_not_override_the_real_host(app): """The core assertion: a forged header must be ignored.""" host = _observed_host(app, { 'Host': 'attacker.jqc.app', 'X-Forwarded-Host': 'victim.jqc.app', }) assert host == 'attacker.jqc.app', ( 'X-Forwarded-Host overrode the real Host header — tenant selection is ' 'attacker-controlled. Check ProxyFix x_host in app/__init__.py.' ) assert 'victim' not in (host or '') def test_real_host_is_still_used_normally(app): """Without the forged header, nothing changes.""" assert _observed_host(app, {'Host': 'lts.jqc.app'}) == 'lts.jqc.app' def test_proxyfix_is_configured_without_x_host(app): """Pin the setting itself, so a future edit has to be deliberate.""" from werkzeug.middleware.proxy_fix import ProxyFix wsgi = app.wsgi_app assert isinstance(wsgi, ProxyFix), 'ProxyFix is no longer installed' assert wsgi.x_host == 0, ( 'ProxyFix x_host must stay 0 — nginx sets Host from the real request, ' 'and X-Forwarded-Host is client-controlled.' ) # The two we DO want unwrapped, for rate limiting and HTTPS URL building. assert wsgi.x_for == 1 assert wsgi.x_proto == 1 def test_forwarded_proto_is_still_honoured(app): """x_proto must keep working — email links depend on it.""" seen = _drive(app, {'Host': 'lts.jqc.app', 'X-Forwarded-Proto': 'https'}) assert seen.get('scheme') == 'https' # ── Config guard ───────────────────────────────────────────────────────────── def test_shipped_nginx_configs_pin_forwarded_host(): """Every proxying block in deploy/nginx must pin X-Forwarded-Host. These files are the reference the server is deployed from. A block that proxies without pinning the header reopens the hole on whichever host uses it, so the omission should fail here rather than on the box. """ import pathlib conf_dir = pathlib.Path(__file__).resolve().parent.parent / 'deploy' / 'nginx' configs = sorted(conf_dir.glob('*.conf')) assert configs, 'no nginx reference configs found in deploy/nginx/' offenders = [] for path in configs: text = path.read_text(encoding='utf-8') if 'proxy_pass' not in text: continue if 'proxy_set_header X-Forwarded-Host' not in text.replace(' ', ' '): offenders.append(path.name) assert not offenders, ( 'nginx config proxies without pinning X-Forwarded-Host: ' + ', '.join(offenders) )