Доверенные прокси (реальный 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:
1 parent
6ff7f1b5f1
commit
5367cb5e9c
9 files changed
+211
-8
No files matched your search
+36
-2
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user