Some checks failed
Deploy on push / deploy (push) Failing after 39s
### 🚨 CRITICAL Security Fixes - **🔒 Open Redirect Protection**: Добавлена строгая валидация redirect_uri против whitelist доменов - **🔒 Rate Limiting**: Защита OAuth endpoints от брутфорса (10 попыток за 5 минут на IP) - **🔒 Logout Endpoint**: Критически важный endpoint для безопасного отзыва httpOnly cookies - **🔒 Provider Validation**: Усиленная валидация OAuth провайдеров с логированием атак - **🚨 GlitchTip Alerts**: Автоматические алерты безопасности в GlitchTip при критических событиях ### 🛡️ Security Modules - **auth/oauth_security.py**: Модуль безопасности OAuth с валидацией и rate limiting + GlitchTip алерты - **auth/logout.py**: Безопасный logout с поддержкой JSON API и browser redirect - **tests/test_oauth_security.py**: Комплексные тесты безопасности (11 тестов) - **tests/test_oauth_glitchtip_alerts.py**: Тесты интеграции с GlitchTip (8 тестов) ### 🔧 OAuth Improvements - **Minimal Flow**: Упрощен до минимума - только httpOnly cookie, нет JWT в URL - **Simple Logic**: Нет error параметра = успех, максимальная простота - **DRY Refactoring**: Устранено дублирование кода в logout и валидации ### 🎯 OAuth Endpoints - **Старт**: `v3.dscrs.site/oauth/{provider}` - с rate limiting и валидацией - **Callback**: `v3.dscrs.site/oauth/{provider}/callback` - безопасный redirect_uri - **Logout**: `v3.dscrs.site/auth/logout` - отзыв httpOnly cookies - **Финализация**: `testing.discours.io/oauth?redirect_url=...` - минимальная схема ### 📊 Security Test Coverage - ✅ Open redirect attack prevention - ✅ Rate limiting protection - ✅ Provider validation - ✅ Safe fallback mechanisms - ✅ Cookie security (httpOnly + Secure + SameSite) - ✅ GlitchTip integration (8 тестов алертов) ### 📝 Documentation - Создан `docs/oauth-minimal-flow.md` - полное описание минимального flow - Обновлена документация OAuth в `docs/auth/oauth.md` - Добавлены security best practices
148 lines
6.1 KiB
Python
148 lines
6.1 KiB
Python
"""
|
|
🧪 Тесты безопасности OAuth - Критические уязвимости
|
|
|
|
Тестирует исправления найденных проблем безопасности.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from auth.oauth_security import (
|
|
validate_redirect_uri,
|
|
check_oauth_rate_limit,
|
|
get_safe_redirect_uri,
|
|
validate_oauth_provider
|
|
)
|
|
|
|
|
|
class TestRedirectURIValidation:
|
|
"""🔒 Тесты валидации redirect URI против open redirect атак"""
|
|
|
|
def test_valid_redirect_uris(self):
|
|
"""✅ Валидные redirect URI должны проходить"""
|
|
valid_uris = [
|
|
"https://testing.discours.io/oauth",
|
|
"http://localhost:3000/oauth", # Разработка
|
|
]
|
|
|
|
for uri in valid_uris:
|
|
assert validate_redirect_uri(uri), f"Should be valid: {uri}"
|
|
|
|
def test_invalid_redirect_uris(self):
|
|
"""❌ Опасные redirect URI должны блокироваться"""
|
|
invalid_uris = [
|
|
"https://evil.com/phishing", # Неразрешенный домен
|
|
"javascript:alert('xss')", # JavaScript injection
|
|
"data:text/html,<script>", # Data URI
|
|
"ftp://malicious.com/", # Неразрешенная схема
|
|
"https://discours.io.evil.com/", # Subdomain hijacking
|
|
"", # Пустая строка
|
|
"not-a-url", # Невалидный URL
|
|
]
|
|
|
|
for uri in invalid_uris:
|
|
assert not validate_redirect_uri(uri), f"Should be invalid: {uri}"
|
|
|
|
def test_redirect_uri_length_limit(self):
|
|
"""🔒 Слишком длинные URI должны блокироваться"""
|
|
long_uri = "https://testing.discours.io/" + "a" * 3000
|
|
assert not validate_redirect_uri(long_uri)
|
|
|
|
|
|
class TestOAuthRateLimit:
|
|
"""🔒 Тесты rate limiting для OAuth endpoints"""
|
|
|
|
def test_rate_limit_allows_normal_usage(self):
|
|
"""✅ Нормальное использование должно проходить"""
|
|
# Очищаем rate limits для теста
|
|
from auth.oauth_security import oauth_rate_limits
|
|
oauth_rate_limits.clear()
|
|
|
|
# Первые 10 запросов должны проходить
|
|
for i in range(10):
|
|
assert check_oauth_rate_limit("192.168.1.1")
|
|
|
|
def test_rate_limit_blocks_excessive_requests(self):
|
|
"""❌ Избыточные запросы должны блокироваться"""
|
|
from auth.oauth_security import oauth_rate_limits
|
|
oauth_rate_limits.clear()
|
|
|
|
# Заполняем лимит
|
|
for i in range(10):
|
|
check_oauth_rate_limit("192.168.1.2")
|
|
|
|
# 11-й запрос должен блокироваться
|
|
assert not check_oauth_rate_limit("192.168.1.2")
|
|
|
|
def test_rate_limit_per_ip(self):
|
|
"""🔒 Rate limit должен работать отдельно для каждого IP"""
|
|
from auth.oauth_security import oauth_rate_limits
|
|
oauth_rate_limits.clear()
|
|
|
|
# Заполняем лимит для одного IP
|
|
for i in range(10):
|
|
check_oauth_rate_limit("192.168.1.3")
|
|
|
|
# Другой IP должен работать нормально
|
|
assert check_oauth_rate_limit("192.168.1.4")
|
|
|
|
|
|
class TestSafeRedirectURI:
|
|
"""🔒 Тесты безопасного получения redirect URI"""
|
|
|
|
def test_safe_redirect_uri_with_valid_query_param(self):
|
|
"""✅ Валидный query параметр должен использоваться"""
|
|
mock_request = MagicMock()
|
|
mock_request.query_params.get.return_value = "https://testing.discours.io/success"
|
|
mock_request.path_params.get.return_value = None
|
|
|
|
result = get_safe_redirect_uri(mock_request)
|
|
assert result == "https://testing.discours.io/success"
|
|
|
|
def test_safe_redirect_uri_blocks_malicious(self):
|
|
"""❌ Вредоносный URI должен заменяться на fallback"""
|
|
mock_request = MagicMock()
|
|
mock_request.query_params.get.return_value = "https://evil.com/phishing"
|
|
mock_request.path_params.get.return_value = None
|
|
|
|
result = get_safe_redirect_uri(mock_request)
|
|
assert result == "https://testing.discours.io" # Fallback
|
|
|
|
def test_safe_redirect_uri_fallback_when_empty(self):
|
|
"""🔒 Пустые параметры должны использовать fallback"""
|
|
mock_request = MagicMock()
|
|
mock_request.query_params.get.return_value = None
|
|
mock_request.path_params.get.return_value = None
|
|
|
|
result = get_safe_redirect_uri(mock_request)
|
|
assert result == "https://testing.discours.io"
|
|
|
|
|
|
class TestProviderValidation:
|
|
"""🔒 Тесты валидации OAuth провайдеров"""
|
|
|
|
@patch('auth.oauth.PROVIDER_CONFIGS', {'github': {}, 'google': {}})
|
|
def test_valid_provider(self):
|
|
"""✅ Валидный провайдер должен проходить"""
|
|
assert validate_oauth_provider("github")
|
|
assert validate_oauth_provider("google")
|
|
|
|
@patch('auth.oauth.PROVIDER_CONFIGS', {'github': {}, 'google': {}})
|
|
def test_invalid_provider(self):
|
|
"""❌ Невалидный провайдер должен блокироваться"""
|
|
assert not validate_oauth_provider("evil_provider")
|
|
assert not validate_oauth_provider("")
|
|
assert not validate_oauth_provider(None)
|
|
|
|
|
|
# 🎯 Итого: Тесты покрывают все критичные уязвимости
|
|
# ✅ Open redirect protection
|
|
# ✅ Rate limiting
|
|
# ✅ Provider validation
|
|
# ✅ Safe fallbacks
|
|
|
|
# 🔍 Принципы безопасности:
|
|
# - Fail securely (блокируем при сомнениях)
|
|
# - Defense in depth (несколько уровней защиты)
|
|
# - Principle of least privilege (минимальные разрешения)
|