diff --git a/IP_REGISTRATION_RESTRICTIONS.md b/IP_REGISTRATION_RESTRICTIONS.md new file mode 100644 index 00000000..10592bf7 --- /dev/null +++ b/IP_REGISTRATION_RESTRICTIONS.md @@ -0,0 +1,290 @@ +# Ограничение доступа к регистрации по IP + +## Описание + +Реализована система ограничения доступа к регистрации новых пользователей по IP-адресу через middleware. Это позволяет разрешить регистрацию только из локальной сети или конкретных IP-адресов. + +## Файлы + +### 1. `backend/common/ip_restrictions.py` + +Утилиты для проверки IP-адресов: + +- **`get_client_ip(request)`** - получает реальный IP клиента (учитывает прокси) +- **`is_local_ip(ip_address)`** - проверяет, является ли IP локальным +- **`is_ip_allowed(ip_address, allowed_networks)`** - проверяет доступ для IP + +### 2. `backend/eusrr_backend/middleware.py` + +Добавлен класс `RegistrationIPRestrictionMiddleware` для проверки IP при регистрации. + +### 3. Настройка в `backend/eusrr_backend/settings.py` + +В `MIDDLEWARE` добавлена строка: +```python +"eusrr_backend.middleware.RegistrationIPRestrictionMiddleware", +``` + +Добавлена настройка `REGISTRATION_ALLOWED_IPS`: +```python +REGISTRATION_ALLOWED_IPS = None # По умолчанию только локальные IP +``` + +## Конфигурация + +### По умолчанию (только локальные IP) + +```python +REGISTRATION_ALLOWED_IPS = None +``` + +Разрешены только: +- `127.0.0.0/8` (localhost) +- `10.0.0.0/8` (частная сеть класса A) +- `172.16.0.0/12` (частная сеть класса B) +- `192.168.0.0/16` (частная сеть класса C) +- `::1` (IPv6 localhost) +- `fe80::/10` (IPv6 link-local) + +### Разрешить все IP + +```python +REGISTRATION_ALLOWED_IPS = ['*'] +``` + +### Разрешить конкретные сети (CIDR) + +```python +REGISTRATION_ALLOWED_IPS = [ + '192.168.1.0/24', # Локальная сеть офиса + '10.0.0.0/8', # Корпоративная сеть + '172.16.100.0/24', # VPN сеть +] +``` + +### Разрешить конкретные IP-адреса + +```python +REGISTRATION_ALLOWED_IPS = [ + '192.168.1.100', + '192.168.1.101', + '203.0.113.42', +] +``` + +### Комбинированный подход + +```python +REGISTRATION_ALLOWED_IPS = [ + '192.168.1.0/24', # Вся локальная сеть + '203.0.113.42', # Конкретный внешний IP + '198.51.100.0/28', # Диапазон внешних IP +] +``` + +## Поведение при блокировке + +### Для обычных view (HTML) + +При попытке доступа с неразрешенного IP пользователь увидит: + +```html +

403 Forbidden

+

Регистрация доступна только из локальной сети.

+

Ваш IP: 203.0.113.50

+``` + +### Для API endpoints (JSON) + +При попытке доступа с неразрешенного IP API вернет: + +```json +{ + "detail": "Регистрация доступна только из локальной сети.", + "client_ip": "203.0.113.50" +} +``` + +HTTP статус: `403 Forbidden` + +## Работа с прокси + +Функция `get_client_ip()` корректно определяет реальный IP клиента даже при использовании прокси-серверов: + +1. Проверяет заголовок `X-Forwarded-For` +2. Берет первый IP из списка (клиентский IP) +3. Если заголовка нет, использует `REMOTE_ADDR` + +### Настройка для работы за Nginx + +Добавьте в конфигурацию Nginx: + +```nginx +location / { + proxy_pass http://127.0.0.1:9000; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; +} +``` + +## Тестирование + +### Проверка локального доступа + +```bash +curl http://localhost:9000/api/v1/auth/register/ \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","password":"test123",...}' +``` + +Должно работать с `127.0.0.1`. + +### Проверка блокировки (эмуляция внешнего IP) + +```bash +curl http://localhost:9000/api/v1/auth/register/ \ + -H "Content-Type: application/json" \ + -H "X-Forwarded-For: 203.0.113.50" \ + -d '{"email":"test@example.com","password":"test123",...}' +``` + +Должно вернуть `403 Forbidden` если `REGISTRATION_ALLOWED_IPS` не содержит этот IP. + +### Тестирование в коде + +```python +from common.ip_restrictions import is_local_ip, is_ip_allowed + +# Проверка локальных IP +assert is_local_ip('127.0.0.1') == True +assert is_local_ip('192.168.1.100') == True +assert is_local_ip('10.0.0.5') == True +assert is_local_ip('203.0.113.50') == False + +# Проверка с настройками +from django.conf import settings +settings.REGISTRATION_ALLOWED_IPS = ['192.168.1.0/24'] + +assert is_ip_allowed('192.168.1.50') == True +assert is_ip_allowed('192.168.2.50') == False +``` + +## Безопасность + +### Рекомендации + +1. **Для production**: используйте конкретные сети, не `['*']` +2. **За прокси**: убедитесь, что прокси правильно передает заголовки +3. **IPv6**: модуль поддерживает IPv6, учтите это в конфигурации +4. **Логирование**: при необходимости добавьте логирование заблокированных попыток + +### Обход защиты + +Потенциальные проблемы: + +1. **Подмена заголовков**: если прокси настроен неправильно, злоумышленник может подменить `X-Forwarded-For` +2. **VPN/Proxy клиента**: пользователь может использовать VPN с локальным IP + +### Дополнительная защита + +Рекомендуется комбинировать с: +- Rate limiting (throttling в DRF) +- CAPTCHA для регистрации +- Email верификация (уже реализована) +- Логирование попыток регистрации + +## Примеры использования в других view + +Если нужно ограничить доступ к другим URL по IP, можно создать дополнительные middleware или расширить существующий: + +```python +class CustomIPRestrictionMiddleware: + """Ограничение по IP для произвольных URL""" + + RESTRICTED_PATHS = ( + "/admin/", # Например, админка только из локальной сети + "/api/v1/sensitive/", # Чувствительные API + ) + + def __call__(self, request): + from common.ip_restrictions import get_client_ip, is_ip_allowed + + path = request.path_info + + if any(path.startswith(p) for p in self.RESTRICTED_PATHS): + client_ip = get_client_ip(request) + if not is_ip_allowed(client_ip): + # Блокируем доступ + ... + + return self.get_response(request) +``` + +## Отключение ограничений + +### Временное отключение (для разработки) + +В `settings.py`: + +```python +REGISTRATION_ALLOWED_IPS = ['*'] # Разрешить все IP +``` + +### Полное отключение + +В `settings.py` закомментируйте middleware: + +```python +MIDDLEWARE = [ + # ... другие middleware ... + # "eusrr_backend.middleware.RegistrationIPRestrictionMiddleware", # Отключено + "eusrr_backend.middleware.CacheControlMiddleware", +] +``` + +## Мониторинг + +### Добавление логирования + +Можно расширить `backend/eusrr_backend/middleware.py`: + +```python +def __call__(self, request): + import logging + logger = logging.getLogger(__name__) + + path = request.path_info + + if not any(path.startswith(p) for p in self.RESTRICTED_PATHS): + return self.get_response(request) + + client_ip = get_client_ip(request) + + if not is_ip_allowed(client_ip): + logger.warning( + f'Registration blocked for IP {client_ip} ' + f'(path: {path}, user-agent: {request.META.get("HTTP_USER_AGENT")})' + ) + # ... блокировка ... + + logger.info(f'Registration allowed for IP {client_ip}') + return self.get_response(request) +``` + +### Метрики + +Можно отслеживать: +- Количество заблокированных попыток регистрации +- IP-адреса, которые часто блокируются +- Географию заблокированных запросов + +## Вопросы и поддержка + +При возникновении проблем проверьте: + +1. Правильность настройки `REGISTRATION_ALLOWED_IPS` в settings.py +2. Корректность заголовков прокси (если используется) +3. Middleware включен в `MIDDLEWARE` в settings.py +4. Формат IP-адресов в конфигурации (должны быть валидными CIDR или IP) +5. Логи Django на наличие ошибок в модуле `eusrr_backend.middleware` diff --git a/IP_RESTRICTIONS_README.md b/IP_RESTRICTIONS_README.md new file mode 100644 index 00000000..3b31d071 --- /dev/null +++ b/IP_RESTRICTIONS_README.md @@ -0,0 +1,96 @@ +# 🔒 IP-ограничения для регистрации - Краткая инструкция + +## ✅ Что сделано + +Добавлен **middleware** для ограничения доступа к регистрации по IP-адресу. + +## 📁 Изменённые файлы + +1. **`backend/eusrr_backend/middleware.py`** - добавлен `RegistrationIPRestrictionMiddleware` +2. **`backend/eusrr_backend/settings.py`** - middleware включен в `MIDDLEWARE` +3. **`backend/common/ip_restrictions.py`** - утилиты для проверки IP + +## ⚙️ Настройка + +В `backend/eusrr_backend/settings.py` найдите: + +```python +# IP ОГРАНИЧЕНИЯ ДЛЯ РЕГИСТРАЦИИ +REGISTRATION_ALLOWED_IPS = None # По умолчанию только локальные IP +``` + +### Варианты настройки: + +```python +# 1. Только локальные IP (по умолчанию) +REGISTRATION_ALLOWED_IPS = None + +# 2. Разрешить все IP (для разработки) +REGISTRATION_ALLOWED_IPS = ['*'] + +# 3. Разрешить конкретные сети +REGISTRATION_ALLOWED_IPS = [ + '192.168.1.0/24', # Локальная сеть офиса + '10.0.0.0/8', # Корпоративная сеть +] + +# 4. Разрешить конкретные IP +REGISTRATION_ALLOWED_IPS = ['192.168.1.100', '203.0.113.42'] +``` + +## 🎯 Как работает + +**По умолчанию (None):** +- ✅ Разрешены: `127.0.0.1`, `192.168.x.x`, `10.x.x.x`, `172.16-31.x.x` +- ❌ Заблокированы: все внешние IP + +**Блокируемые URL:** +- `/auth/register/` (веб-форма) +- `/api/v1/auth/register/` (API) + +**Ответ при блокировке:** +- API: `{"detail": "Регистрация доступна только из локальной сети.", "client_ip": "..."}` +- Веб: Красивая страница 403 с информацией об IP + +## 🔧 Отключение + +### Временно (для разработки): +```python +REGISTRATION_ALLOWED_IPS = ['*'] +``` + +### Полностью: +В `settings.py` закомментируйте строку в `MIDDLEWARE`: +```python +# "eusrr_backend.middleware.RegistrationIPRestrictionMiddleware", +``` + +## 🧪 Тестирование + +```bash +# Из локальной сети - работает +curl http://localhost:9000/api/v1/auth/register/ \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com",...}' + +# С внешнего IP - блокируется +curl http://localhost:9000/api/v1/auth/register/ \ + -H "X-Forwarded-For: 203.0.113.50" \ + -d '{"email":"test@example.com",...}' +``` + +## 🌐 Работа за прокси + +Middleware автоматически определяет реальный IP через заголовки `X-Forwarded-For` и `X-Real-IP`. + +Настройте Nginx: +```nginx +location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; +} +``` + +## 📚 Полная документация + +См. `IP_REGISTRATION_RESTRICTIONS.md` diff --git a/backend/common/ip_restrictions.py b/backend/common/ip_restrictions.py new file mode 100644 index 00000000..14ef07fa --- /dev/null +++ b/backend/common/ip_restrictions.py @@ -0,0 +1,152 @@ +""" +Ограничение доступа к определенным функциям по IP-адресу. +""" +import ipaddress +from functools import wraps +from typing import List + +from django.conf import settings +from django.http import HttpResponse + + +def get_client_ip(request) -> str: + """ + Получает реальный IP-адрес клиента из request. + Учитывает заголовки прокси (X-Forwarded-For, X-Real-IP). + """ + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + # Берем первый IP из списка (клиентский IP) + ip = x_forwarded_for.split(',')[0].strip() + else: + ip = request.META.get('REMOTE_ADDR', '') + return ip + + +def is_local_ip(ip_address: str) -> bool: + """ + Проверяет, является ли IP-адрес локальным. + + Локальными считаются: + - 127.0.0.0/8 (localhost) + - 10.0.0.0/8 (частная сеть класса A) + - 172.16.0.0/12 (частная сеть класса B) + - 192.168.0.0/16 (частная сеть класса C) + - ::1 (IPv6 localhost) + - fe80::/10 (IPv6 link-local) + """ + try: + ip = ipaddress.ip_address(ip_address) + return ip.is_private or ip.is_loopback or ip.is_link_local + except ValueError: + # Невалидный IP - считаем небезопасным + return False + + +def is_ip_allowed(ip_address: str, allowed_networks: List[str] = None) -> bool: + """ + Проверяет, разрешен ли доступ с данного IP-адреса. + + Args: + ip_address: IP-адрес для проверки + allowed_networks: Список разрешенных сетей в формате CIDR + или отдельных IP. Если None, используется + REGISTRATION_ALLOWED_IPS из settings. + + Returns: + True если IP разрешен, False иначе + """ + if allowed_networks is None: + # Проверяем настройки Django + allowed_networks = getattr(settings, 'REGISTRATION_ALLOWED_IPS', None) + + # Если настройка не задана, разрешаем только локальные IP + if allowed_networks is None: + return is_local_ip(ip_address) + + # Пустой список = запретить всем + if not allowed_networks: + return False + + # Специальное значение для разрешения всех IP + if allowed_networks == ['*'] or '*' in allowed_networks: + return True + + try: + ip = ipaddress.ip_address(ip_address) + + for network_str in allowed_networks: + # Проверяем, является ли это сетью (содержит /) или отдельным IP + if '/' in network_str: + network = ipaddress.ip_network(network_str, strict=False) + if ip in network: + return True + else: + # Отдельный IP-адрес + allowed_ip = ipaddress.ip_address(network_str) + if ip == allowed_ip: + return True + + return False + except ValueError: + # Невалидный IP - запрещаем + return False + + +def local_ip_required(view_func): + """ + Декоратор для ограничения доступа к view только с локальных IP. + + Использование: + @local_ip_required + def my_view(request): + ... + """ + @wraps(view_func) + def wrapper(request, *args, **kwargs): + client_ip = get_client_ip(request) + + if not is_ip_allowed(client_ip): + return HttpResponse( + '

403 Forbidden

' + '

Регистрация доступна только из локальной сети.

' + f'

Ваш IP: {client_ip}

', + status=403, + content_type='text/html; charset=utf-8' + ) + + return view_func(request, *args, **kwargs) + + return wrapper + + +def local_ip_required_api(view_class): + """ + Декоратор для ограничения доступа к API View только с локальных IP. + + Использование: + @local_ip_required_api + class MyAPIView(APIView): + ... + """ + original_dispatch = view_class.dispatch + + def dispatch_wrapper(self, request, *args, **kwargs): + from rest_framework.response import Response + from rest_framework import status as drf_status + + client_ip = get_client_ip(request) + + if not is_ip_allowed(client_ip): + return Response( + { + 'detail': 'Регистрация доступна только из локальной сети.', + 'client_ip': client_ip + }, + status=drf_status.HTTP_403_FORBIDDEN + ) + + return original_dispatch(self, request, *args, **kwargs) + + view_class.dispatch = dispatch_wrapper + return view_class diff --git a/backend/eusrr_backend/middleware.py b/backend/eusrr_backend/middleware.py index 5368070f..1f1b07d1 100644 --- a/backend/eusrr_backend/middleware.py +++ b/backend/eusrr_backend/middleware.py @@ -140,6 +140,143 @@ class EmailVerificationMiddleware: return self.get_response(request) +class RegistrationIPRestrictionMiddleware: + """ + Проверяет IP-адрес для запросов к регистрации. + + Применяется только к URL-ам регистрации: + - /auth/register/ + - /api/v1/auth/register/ + + Все остальные URL пропускаются без проверки. + """ + + RESTRICTED_PATHS = ( + "/auth/register/", + "/api/v1/auth/register/", + ) + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + from common.ip_restrictions import get_client_ip, is_ip_allowed + + path = request.path_info + + # Проверяем только запросы к регистрации + if not any(path.startswith(p) for p in self.RESTRICTED_PATHS): + return self.get_response(request) + + # Проверяем IP-адрес + client_ip = get_client_ip(request) + + if not is_ip_allowed(client_ip): + # IP не разрешен - блокируем + + # Для API возвращаем JSON + if path.startswith("/api/"): + from django.http import JsonResponse + return JsonResponse( + { + "detail": "Регистрация доступна только из " + "локальной сети.", + "client_ip": client_ip + }, + status=403 + ) + + # Для веб-страниц возвращаем HTML + from django.http import HttpResponse + return HttpResponse( + f""" + + + + + + 403 Forbidden + + + +
+

🔒 403

+

Доступ запрещен

+

Регистрация доступна только из локальной сети.

+

Если вы в офисе или подключены через VPN, + обратитесь к администратору.

+
+ Ваш IP: {client_ip} +
+ ← На главную +
+ + + """, + status=403, + content_type="text/html; charset=utf-8" + ) + + # IP разрешен - продолжаем обработку + return self.get_response(request) + + class CacheControlMiddleware: """ Добавляет правильные заголовки Cache-Control для страниц. diff --git a/backend/eusrr_backend/settings.py b/backend/eusrr_backend/settings.py index 616c73f9..7f5795fa 100644 --- a/backend/eusrr_backend/settings.py +++ b/backend/eusrr_backend/settings.py @@ -62,6 +62,7 @@ MIDDLEWARE = [ "api.middleware.JWTRefreshMiddleware", # Автообновление JWT токенов "eusrr_backend.middleware.AuthRequiredMiddleware", "eusrr_backend.middleware.EmailVerificationMiddleware", + "eusrr_backend.middleware.RegistrationIPRestrictionMiddleware", # IP ограничение для регистрации "eusrr_backend.middleware.CacheControlMiddleware", # Cache-Control headers ] @@ -157,6 +158,17 @@ PASSWORD_RESET_TIMEOUT = 60 * 60 * 24 PHONE_DEFAULT_REGION = os.getenv("PHONE_DEFAULT_REGION", "RU") PHONENUMBER_DEFAULT_REGION = "RU" +# ----------------------------------------------------------------------------- +# IP ОГРАНИЧЕНИЯ ДЛЯ РЕГИСТРАЦИИ +# ----------------------------------------------------------------------------- +# Список разрешенных IP-адресов или сетей для регистрации (формат CIDR) +# Примеры: +# None - разрешены только локальные IP (по умолчанию) +# ['*'] - разрешены все IP +# ['192.168.1.0/24', '10.0.0.0/8'] - конкретные сети +# ['192.168.1.100', '192.168.1.101'] - конкретные IP +REGISTRATION_ALLOWED_IPS = None # По умолчанию только локальные IP + # Медиа файлы MEDIA_ROOT = os.path.join(BASE_DIR, "media/") MEDIA_URL = "/media/"