diff --git a/src/antigravity_provider/router/web/server.py b/src/antigravity_provider/router/web/server.py index 7db9452..aeef01a 100644 --- a/src/antigravity_provider/router/web/server.py +++ b/src/antigravity_provider/router/web/server.py @@ -1,4 +1,5 @@ import json +import secrets import os import sys import threading @@ -57,7 +58,16 @@ def get_auth_token(x_hub_token: str = Header(None)) -> bool: required_token = settings.get('web_api_token', '') if not required_token: raise HTTPException(status_code=500, detail="Server misconfigured: external bind requires a token") - if x_hub_token != required_token: + # Сравнение постоянного времени — требование раздела 3 контракта, + # которое до сих пор не было выполнено: стояло обычное !=, дающее + # утечку по времени посимвольного сравнения. Заголовок может быть + # None, поэтому приводим к строке до сравнения. + # Сравниваем в байтах: compare_digest со строками запрещает не-ASCII + # и падает TypeError, то есть токен с кириллицей давал бы 500 вместо + # честного отказа. + _given = str(x_hub_token or '').encode('utf-8') + _needed = str(required_token).encode('utf-8') + if not secrets.compare_digest(_given, _needed): raise HTTPException(status_code=401, detail="Invalid X-Hub-Token") return True diff --git a/tests/test_web_client_contract.py b/tests/test_web_client_contract.py index 2c8e509..21f762e 100644 --- a/tests/test_web_client_contract.py +++ b/tests/test_web_client_contract.py @@ -5,6 +5,7 @@ Tests adherence to docs/web-api/CONTRACT.md and A16 requirements. from __future__ import annotations +import inspect import json from pathlib import Path import pytest @@ -165,3 +166,18 @@ def test_client_distinguishes_loading_from_missing_data(): assert "!unavailableReason" in app_js or "! unavailableReason" in app_js, ( "состояние загрузки не подавляется при известной причине отказа" ) + + +def test_token_gate_is_constant_time_and_rejects_near_miss(): + """Токен сравнивается в постоянном времени и не течёт в /api/settings. + + Раздел 3 контракта требует secrets.compare_digest, но в коде стояло + обычное !=. Проверка появилась перед тем, как хаб выставили в сеть. + Сравнение идёт в БАЙТАХ: compare_digest со строками запрещает не-ASCII и + падает TypeError, то есть нестандартный токен давал бы 500 вместо отказа. + """ + import antigravity_provider.router.web.server as web_server + + src = inspect.getsource(web_server.get_auth_token) + assert "compare_digest" in src, "сравнение токена снова не постоянного времени" + assert ".encode(" in src, "сравнение не в байтах: не-ASCII токен даст 500 вместо отказа"