Aug 21 - Fixed Forwarded host
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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)
|
||||
)
|
||||
Reference in New Issue
Block a user