Доверенные прокси (реальный IP клиента) и параметризация порта

Пункты 14 и 16 ревью 2026-09-28 17:35 (docs/changes/025):
- TRUSTED_PROXIES (CIDR, по умолчанию пусто): X-Forwarded-For учитывается
  только от доверенного peer, цепочка разбирается справа налево; иначе
  заголовок игнорируется — подделать IP нельзя. Реальный IP — в блокировке
  входа и событиях auth.*; неверный CIDR — отказ старта;
- docker-compose: "${APP_BIND:-0.0.0.0}:${APP_PORT:-8000}:8000"; стенд на
  8001 через APP_PORT в .env, override-файл больше не нужен.

Тесты: 36 из 36. Стенд: поддельный X-Forwarded-For проигнорирован,
заблокирован реальный адрес; боевые данные не изменены. Ручная проверка
UI пользователем на момент коммита не подтверждена.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Opus 5.5 committed 2026-09-28 21:19:10 +03:00
1 parent 6ff7f1b5f1
commit 5367cb5e9c
9 files changed
+211 -8

No files matched your search

+36 -2
View File
@@ -1,12 +1,13 @@
import hmac
import ipaddress
import threading
import time
from cryptography.fernet import Fernet
from fastapi import Depends, HTTPException
from fastapi import Depends, HTTPException, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from app.config import get_settings
from app.config import get_settings, trusted_networks
_bearer = HTTPBearer(auto_error=False)
@@ -33,6 +34,39 @@ def check_admin(user: str, password: str) -> bool:
return ok_user and ok_pass
def _in_networks(ip: str, networks) -> bool:
try:
addr = ipaddress.ip_address(ip)
except ValueError:
return False
return any(addr in net for net in networks)
def client_ip(request: Request) -> str:
"""IP клиента с учётом TRUSTED_PROXIES. Пустой список или peer не из доверенной сети — X-Forwarded-For
полностью игнорируется (подделка заголовка бессмысленна), возвращается адрес соединения.
Иначе цепочка `X-Forwarded-For` (все заголовки, значения через запятую) разбирается справа налево:
доверенные прокси пропускаются, первый адрес не из доверенной сети — IP клиента. Невалидная запись
обрывает разбор — возвращается последний валидный разобранный адрес (или peer, если такого не было).
Если все адреса цепочки доверенные (или заголовка нет) — возвращается peer."""
peer = request.client.host if request.client else "unknown"
networks = trusted_networks(get_settings().trusted_proxies)
if not networks or not _in_networks(peer, networks):
return peer
chain = [part.strip() for header in request.headers.getlist("x-forwarded-for") for part in header.split(",")]
last_valid = None
for hop in reversed(chain):
try:
ipaddress.ip_address(hop)
except ValueError:
return last_valid if last_valid is not None else peer
if not _in_networks(hop, networks):
return hop
last_valid = hop
return peer
# --- защита от перебора: вход в UI (по IP) и подтверждение действий паролем (очистка журнала, по пользователю) ---
FAIL_LIMIT, WINDOW_S, LOCK_S = 5, 600, 600 # 5 неверных за 10 минут → блокировка на 10 минут
_guard = threading.Lock()