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 (минимальные разрешения)
|