diff --git a/backend/atlas_portal/routes/account_wolf.py b/backend/atlas_portal/routes/account_wolf.py index 15be7a1..d090557 100644 --- a/backend/atlas_portal/routes/account_wolf.py +++ b/backend/atlas_portal/routes/account_wolf.py @@ -9,10 +9,8 @@ from ..keycloak import require_auth, require_account_access def _client_ip() -> str: - for header in ("CF-Connecting-IP", "X-Forwarded-For", "X-Real-IP"): - value = request.headers.get(header, "").strip() - if value: - return value.split(",", 1)[0].strip() + # ProxyFix normalizes this from the trusted Traefik hop. Reading raw + # forwarding headers here would let a browser request choose an unlock IP. return request.remote_addr or "" @@ -58,7 +56,7 @@ def register_account_wolf(app) -> None: if not ok: return resp payload = _json_payload() - payload["ip"] = payload.get("ip") or _client_ip() + payload["ip"] = _client_ip() return ariadne_client.proxy("POST", "/api/game-stream/firewall/revoke", payload=payload) @app.route("/api/account/wolf/pairing/status", methods=["GET"]) diff --git a/backend/tests/test_account_wolf.py b/backend/tests/test_account_wolf.py index c91aa59..b755f22 100644 --- a/backend/tests/test_account_wolf.py +++ b/backend/tests/test_account_wolf.py @@ -35,7 +35,11 @@ def make_client(monkeypatch, *, ariadne: DummyAriadne | None = None, account_ok: def test_wolf_status_proxies_source_ip(monkeypatch) -> None: client, ariadne = make_client(monkeypatch) - resp = client.get("/api/account/wolf", headers={"X-Forwarded-For": "1.2.3.4, 10.0.0.1"}) + resp = client.get( + "/api/account/wolf", + environ_base={"REMOTE_ADDR": "1.2.3.4"}, + headers={"X-Forwarded-For": "9.9.9.9, 10.0.0.1"}, + ) assert resp.status_code == 200 assert ariadne.calls == [("GET", "/api/game-stream/status", None, {"source_ip": "1.2.3.4"})] @@ -44,7 +48,12 @@ def test_wolf_status_proxies_source_ip(monkeypatch) -> None: def test_wolf_unlock_uses_current_source_ip(monkeypatch) -> None: client, ariadne = make_client(monkeypatch) - resp = client.post("/api/account/wolf/firewall/unlock", json={"ttl_seconds": 120}, headers={"X-Real-IP": "5.6.7.8"}) + resp = client.post( + "/api/account/wolf/firewall/unlock", + json={"ttl_seconds": 120}, + environ_base={"REMOTE_ADDR": "5.6.7.8"}, + headers={"X-Real-IP": "9.9.9.9"}, + ) assert resp.status_code == 200 assert ariadne.calls == [("POST", "/api/game-stream/firewall/unlock", {"ttl_seconds": 120, "ip": "5.6.7.8"}, None)] @@ -53,8 +62,12 @@ def test_wolf_unlock_uses_current_source_ip(monkeypatch) -> None: def test_wolf_pairing_and_admin_actions_proxy(monkeypatch) -> None: client, ariadne = make_client(monkeypatch) - client.get("/api/account/wolf/pairing/status", headers={"X-Forwarded-For": "1.2.3.4"}) - client.post("/api/account/wolf/pairing/submit-pin", json={"pair_secret": "secret", "pin": "1234"}, headers={"X-Forwarded-For": "1.2.3.4"}) + client.get("/api/account/wolf/pairing/status", environ_base={"REMOTE_ADDR": "1.2.3.4"}) + client.post( + "/api/account/wolf/pairing/submit-pin", + json={"pair_secret": "secret", "pin": "1234"}, + environ_base={"REMOTE_ADDR": "1.2.3.4"}, + ) client.post("/api/account/wolf/game-mode/start", json={"game": "steam"}) client.post("/api/account/wolf/game-mode/stop", json={"game": "steam"}) client.post("/api/account/wolf/admin/firewall/unlock", json={"ip": "8.8.8.8", "target_user": "olya"}) @@ -68,6 +81,19 @@ def test_wolf_pairing_and_admin_actions_proxy(monkeypatch) -> None: ] +def test_wolf_user_revoke_cannot_choose_arbitrary_ip(monkeypatch) -> None: + client, ariadne = make_client(monkeypatch) + + resp = client.post( + "/api/account/wolf/firewall/revoke", + json={"ip": "9.9.9.9"}, + environ_base={"REMOTE_ADDR": "1.2.3.4"}, + ) + + assert resp.status_code == 200 + assert ariadne.calls == [("POST", "/api/game-stream/firewall/revoke", {"ip": "1.2.3.4"}, None)] + + def test_wolf_routes_require_account_and_ariadne(monkeypatch) -> None: client, _ariadne = make_client(monkeypatch, account_ok=False) assert client.get("/api/account/wolf").status_code == 403