Accept the proxy user header only from trusted addresses

In proxy mode, each client that reached the port could send the user
header. PROXY_AUTH_TRUSTED_IPS now lists the addresses and networks that
can send it. The default is the loopback and private addresses, so a
request that comes straight from the internet cannot log in.
This commit is contained in:
tim
2026-10-02 23:37:33 +02:00
parent 9196dd75a8
commit 2d9796d77c
7 changed files with 106 additions and 5 deletions
+45
View File
@@ -173,12 +173,57 @@ def test_proxy_header(client, monkeypatch):
}
@pytest.mark.parametrize(
("trusted", "address", "accepted"),
[
(None, "127.0.0.1", True),
(None, "::1", True),
(None, "172.18.0.3", True),
(None, "192.168.1.20", True),
(None, "fd00::5", True),
(None, "::ffff:10.1.2.3", True),
(None, "203.0.113.9", False),
(None, "2001:db8::1", False),
(None, "", False),
(["172.18.0.3"], "172.18.0.3", True),
(["172.18.0.3"], "172.18.0.4", False),
(["172.18.0.3"], "127.0.0.1", False),
(["10.0.0.0/8", "fd00::/64"], "fd00::7", True),
(["10.0.0.0/8", "fd00::/64"], "192.168.1.20", False),
],
)
def test_only_a_trusted_address_can_send_the_proxy_header(client, monkeypatch, caplog, trusted, address, accepted):
monkeypatch.setattr(config, "AUTH_METHOD", "proxy")
if trusted is not None:
monkeypatch.setattr(config, "PROXY_AUTH_TRUSTED_IPS", trusted)
environ = {"REMOTE_ADDR": address}
response = client.get("/api/stories", headers={"X-Remote-User": "alice"}, environ_base=environ)
assert response.status_code == (200 if accepted else 401)
assert (
client.get("/api/auth/check", headers={"X-Remote-User": "alice"}, environ_base=environ).json["authenticated"]
is accepted
)
assert ("which is not in PROXY_AUTH_TRUSTED_IPS" in caplog.text) is not accepted
# A change needs the header too, from the same address.
change = client.post(
"/api/stories", json={"urls": []}, headers={**H, "X-Remote-User": "alice"}, environ_base=environ
)
assert change.status_code == (200 if accepted else 401)
@pytest.mark.parametrize(
("setting", "value", "error"),
[
("AUTH_METHOD", "cwa", "AUTH_METHOD=cwa needs CWA_URL or CWA_DB_PATH"),
("AUTH_METHOD", "cwa-login", "AUTH_METHOD must be none, local, cwa or proxy, not 'cwa-login'"),
("PROXY_AUTH_USER_HEADER", "", "AUTH_METHOD=proxy needs PROXY_AUTH_USER_HEADER"),
("PROXY_AUTH_TRUSTED_IPS", [], "AUTH_METHOD=proxy needs PROXY_AUTH_TRUSTED_IPS, the addresses of the proxy"),
(
"PROXY_AUTH_TRUSTED_IPS",
["10.0.0.5", "proxy.local"],
"PROXY_AUTH_TRUSTED_IPS must be a list of addresses and networks: "
"'proxy.local' does not appear to be an IPv4 or IPv6 network",
),
],
)
def test_incomplete_login_settings_refuse_every_request(client, monkeypatch, setting, value, error):
+10
View File
@@ -97,6 +97,7 @@ def test_defaults(monkeypatch, reload_config):
"DEFAULT_LOCKED_FIELDS",
"LOG_LEVEL",
"PROXY_AUTH_USER_HEADER",
"PROXY_AUTH_TRUSTED_IPS",
"SESSION_COOKIE_SECURE",
"TRUST_PROXY",
"CWA_DB_PATH",
@@ -113,6 +114,15 @@ def test_defaults(monkeypatch, reload_config):
assert config.DEFAULT_LOCKED_FIELDS == ["title"]
assert config.LOG_LEVEL == "INFO"
assert config.PROXY_AUTH_USER_HEADER == "X-Remote-User"
assert config.PROXY_AUTH_TRUSTED_IPS == [
"127.0.0.0/8",
"::1",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"fc00::/7",
"fe80::/10",
]
assert config.SESSION_COOKIE_SECURE is False
assert config.TRUST_PROXY is False
assert config.CWA_DB_PATH is None