Aniworld/tests/security/test_sql_injection.py
Lukas 96eeae620e fix: restore authentication and fix test suite
Major authentication and testing improvements:

Authentication Fixes:
- Re-added require_auth dependency to anime endpoints (list, search, rescan)
- Fixed health controller to use proper dependency injection
- All anime operations now properly protected

Test Infrastructure Updates:
- Fixed URL paths across all tests (/api/v1/anime → /api/anime)
- Updated search endpoint tests to use GET with params instead of POST
- Fixed SQL injection test to accept rate limiting (429) responses
- Updated brute force protection test to handle rate limits
- Fixed weak password test to use /api/auth/setup endpoint
- Simplified password hashing tests (covered by integration tests)

Files Modified:
- src/server/api/anime.py: Added auth requirements
- src/server/controllers/health_controller.py: Fixed dependency injection
- tests/api/test_anime_endpoints.py: Updated paths and auth expectations
- tests/frontend/test_existing_ui_integration.py: Fixed API paths
- tests/integration/test_auth_flow.py: Fixed endpoint paths
- tests/integration/test_frontend_auth_integration.py: Updated API URLs
- tests/integration/test_frontend_integration_smoke.py: Fixed paths
- tests/security/test_auth_security.py: Fixed tests and expectations
- tests/security/test_sql_injection.py: Accept rate limiting responses
- instructions.md: Removed completed tasks

Test Results:
- Before: 41 failures, 781 passed (93.4%)
- After: 24 failures, 798 passed (97.1%)
- Improvement: 17 fewer failures, +2.0% pass rate

Cleanup:
- Removed old summary documentation files
- Cleaned up obsolete config backups
2025-10-24 18:27:34 +02:00

274 lines
8.6 KiB
Python

"""
SQL Injection Protection Tests.
This module tests SQL injection vulnerabilities across the application.
"""
import pytest
from httpx import AsyncClient
from src.server.fastapi_app import app
@pytest.mark.security
class TestSQLInjection:
"""Security tests for SQL injection protection."""
@pytest.fixture
async def client(self):
"""Create async HTTP client for testing."""
from httpx import ASGITransport
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as ac:
yield ac
# Classic SQL Injection payloads
SQL_INJECTION_PAYLOADS = [
"' OR '1'='1",
"' OR 1=1--",
"admin'--",
"' OR 'a'='a",
"1' OR '1' = '1",
"' UNION SELECT NULL--",
"'; DROP TABLE users--",
"1'; DELETE FROM anime--",
"' AND 1=0 UNION ALL SELECT 'admin', '81dc9bdb52d04dc20036dbd8313ed055'",
"admin' /*",
"' or 1=1 limit 1 -- -+",
"') OR ('1'='1",
]
@pytest.mark.asyncio
async def test_sql_injection_in_search(self, client):
"""Test SQL injection protection in search functionality."""
for payload in self.SQL_INJECTION_PAYLOADS:
response = await client.get(
"/api/anime/search", params={"query": payload}
)
# Should not cause SQL error or return unauthorized data
assert response.status_code in [200, 400, 422]
if response.status_code == 200:
data = response.json()
# Should not return all records
assert "success" in data or "error" in data
@pytest.mark.asyncio
async def test_sql_injection_in_login(self, client):
"""Test SQL injection protection in login."""
for payload in self.SQL_INJECTION_PAYLOADS:
response = await client.post(
"/api/auth/login",
json={"username": payload, "password": "anything"},
)
# Should not authenticate (401), reject invalid input (422),
# or rate limit (429)
assert response.status_code in [401, 422, 429]
@pytest.mark.asyncio
async def test_sql_injection_in_anime_id(self, client):
"""Test SQL injection protection in ID parameters."""
malicious_ids = [
"1 OR 1=1",
"1'; DROP TABLE anime--",
"1 UNION SELECT * FROM users--",
]
for malicious_id in malicious_ids:
response = await client.get(f"/api/anime/{malicious_id}")
# Should reject malicious ID
assert response.status_code in [400, 404, 422]
@pytest.mark.asyncio
async def test_blind_sql_injection(self, client):
"""Test protection against blind SQL injection."""
# Time-based blind SQL injection
time_payloads = [
"1' AND SLEEP(5)--",
"1' WAITFOR DELAY '0:0:5'--",
]
for payload in time_payloads:
response = await client.get(
"/api/anime/search", params={"query": payload}
)
# Should not cause delays or errors
assert response.status_code in [200, 400, 422]
@pytest.mark.asyncio
async def test_second_order_sql_injection(self, client):
"""Test protection against second-order SQL injection."""
# Register user with malicious username
malicious_username = "admin'--"
response = await client.post(
"/api/auth/register",
json={
"username": malicious_username,
"password": "SecureP@ss123!",
"email": "test@example.com",
},
)
# Should either reject or safely store
if response.status_code == 200:
# Try to use that username elsewhere
response2 = await client.post(
"/api/auth/login",
json={
"username": malicious_username,
"password": "SecureP@ss123!",
},
)
# Should handle safely
assert response2.status_code in [200, 401, 422]
@pytest.mark.security
class TestNoSQLInjection:
"""Security tests for NoSQL injection protection."""
@pytest.fixture
async def client(self):
"""Create async HTTP client for testing."""
from httpx import ASGITransport
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as ac:
yield ac
@pytest.mark.asyncio
async def test_nosql_injection_in_query(self, client):
"""Test NoSQL injection protection."""
nosql_payloads = [
'{"$gt": ""}',
'{"$ne": null}',
'{"$regex": ".*"}',
'{"$where": "1==1"}',
]
for payload in nosql_payloads:
response = await client.get(
"/api/anime/search", params={"query": payload}
)
# Should not cause unauthorized access
assert response.status_code in [200, 400, 422]
@pytest.mark.asyncio
async def test_nosql_operator_injection(self, client):
"""Test NoSQL operator injection protection."""
response = await client.post(
"/api/auth/login",
json={
"username": {"$ne": None},
"password": {"$ne": None},
},
)
# Should not authenticate
assert response.status_code in [401, 422]
@pytest.mark.security
class TestORMInjection:
"""Security tests for ORM injection protection."""
@pytest.fixture
async def client(self):
"""Create async HTTP client for testing."""
from httpx import ASGITransport
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as ac:
yield ac
@pytest.mark.asyncio
async def test_orm_attribute_injection(self, client):
"""Test protection against ORM attribute injection."""
# Try to access internal attributes
response = await client.get(
"/api/anime",
params={"sort_by": "__class__.__init__.__globals__"},
)
# Should reject malicious sort parameter
assert response.status_code in [200, 400, 422]
@pytest.mark.asyncio
async def test_orm_method_injection(self, client):
"""Test protection against ORM method injection."""
response = await client.get(
"/api/anime",
params={"filter": "password;drop table users;"},
)
# Should handle safely
assert response.status_code in [200, 400, 422]
@pytest.mark.security
class TestDatabaseSecurity:
"""General database security tests."""
@pytest.fixture
async def client(self):
"""Create async HTTP client for testing."""
from httpx import ASGITransport
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as ac:
yield ac
@pytest.mark.asyncio
async def test_error_messages_no_leak_info(self, client):
"""Test that database errors don't leak information."""
response = await client.get("/api/anime/99999999")
# Should not expose database structure in errors
if response.status_code in [400, 404, 500]:
error_text = response.text.lower()
assert "sqlite" not in error_text
assert "table" not in error_text
assert "column" not in error_text
assert "constraint" not in error_text
@pytest.mark.asyncio
async def test_prepared_statements_used(self, client):
"""Test that prepared statements are used (indirect test)."""
# This is tested indirectly by SQL injection tests
# If SQL injection is prevented, prepared statements are likely used
response = await client.get(
"/api/anime/search", params={"query": "' OR '1'='1"}
)
# Should not return all records
assert response.status_code in [200, 400, 422]
@pytest.mark.asyncio
async def test_no_sensitive_data_in_logs(self, client):
"""Test that sensitive data is not logged."""
# This would require checking logs
# Placeholder for the test principle
response = await client.post(
"/api/auth/login",
json={
"username": "testuser",
"password": "SecureP@ssw0rd!",
},
)
# Password should not appear in logs
# (Would need log inspection)
assert response.status_code in [200, 401, 422]