From c35bc4868d62cfa7abb7a1a4c1c17eca51eb6ce5 Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Tue, 25 Aug 2026 19:57:05 +0700 Subject: [PATCH] =?UTF-8?q?fix(security):=20=D0=BB=D1=8E=D0=B1=D0=BE=D0=B9?= =?UTF-8?q?=20=D1=81=D0=B0=D0=B9=D1=82=20=D0=B2=D0=BE=20=D0=B2=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D0=B4=D0=BA=D0=B5=20=D1=80=D1=8F=D0=B4=D0=BE=D0=BC=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=B3=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D1=8F?= =?UTF-8?q?=D1=82=D1=8C=20=D1=85=D0=B0=D0=B1=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CORS был настроен как allow_origins=["*"] вместе с allow_credentials=True. FastAPI в таком сочетании не отдаёт звёздочку, а ОТРАЖАЕТ присланный Origin обратно. Проверено запросом к работающему хабу: Origin: https://evil.example.com -> HTTP 200 access-control-allow-origin: https://evil.example.com access-control-allow-credentials: true Опаснее всего это на localhost. get_auth_token требует токен только при небlocalhost-привязке, то есть на 127.0.0.1 проверки нет вовсе. Значит любая открытая рядом веб-страница могла прочитать /api/snapshot со всеми аккаунтами, почтами и квотами и вызвать /api/action — удалить учётные данные, переписать маршрутизацию, запустить вход OAuth. Ровно так на обеих машинах владельца хаб и работает. Собственному интерфейсу CORS не нужен: он отдаётся тем же сервером. Межсайтовые запросы запрещены по умолчанию, список разрешённых источников вынесен в настройку web_api_allowed_origins — он понадобится, когда одна панель будет смотреть на несколько хабов. Проверено после правки: заголовков access-control в ответе нет, браузер такой запрос заблокирует; собственный интерфейс работает, снапшот приходит (24 профиля, 6 ролей, индикатор «Live API»). 451 passed, ruff чисто. Co-Authored-By: Claude Opus 5 --- src/antigravity_provider/router/web/server.py | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/antigravity_provider/router/web/server.py b/src/antigravity_provider/router/web/server.py index a6f269a..5d7e7a4 100644 --- a/src/antigravity_provider/router/web/server.py +++ b/src/antigravity_provider/router/web/server.py @@ -26,15 +26,48 @@ from antigravity_provider.updater.update_manager import get_installed_commit, Up logger = logging.getLogger("hermes.router.web") +def _bootstrap_settings() -> Dict[str, Any]: + """Настройки, нужные до объявления _web_settings (список источников CORS).""" + try: + from antigravity_provider.router.settings_service import get_hub_settings + + return get_hub_settings() or {} + except Exception: + return {} + + app = FastAPI(title="Hermes Hub Web API", version="1.0.0") -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) +# Межсайтовые запросы запрещены по умолчанию. +# +# Стояло allow_origins=["*"] вместе с allow_credentials=True. FastAPI в таком +# сочетании отражает любой присланный Origin обратно, поэтому проверено +# запросом: страница с https://evil.example.com получала +# access-control-allow-origin: https://evil.example.com +# access-control-allow-credentials: true +# на 200 от /api/snapshot. +# +# Опаснее всего это на localhost: там токен не требуется вовсе (см. +# get_auth_token), а значит ЛЮБОЙ сайт, открытый во вкладке рядом, мог читать +# снапшот со всеми аккаунтами и почтами и вызывать /api/action — удалять +# учётные данные, менять маршрутизацию, запускать входы OAuth. +# +# Собственному интерфейсу CORS не нужен: он отдаётся тем же сервером. Список +# разрешённых источников оставлен настройкой — он понадобится, когда одна +# панель будет смотреть на несколько хабов. +_cors_origins = [ + o.strip() + for o in str(_bootstrap_settings().get("web_api_allowed_origins", "")).split(",") + if o.strip() +] +if _cors_origins: + app.add_middleware( + CORSMiddleware, + allow_origins=_cors_origins, + allow_credentials=True, + allow_methods=["GET", "POST", "OPTIONS"], + allow_headers=["X-Hub-Token", "Content-Type"], + ) def _web_settings() -> Dict[str, Any]: