Files
wehub-resource-sync cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

460 lines
17 KiB
Python

"""Security tests for multiuser authentication system.
This module tests various security aspects including:
- SQL injection prevention
- Authorization bypass attempts
- Session security
- Input validation
"""
import os
from pathlib import Path
from typing import Any
import pytest
from fastapi.testclient import TestClient
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api_app import app
from invokeai.app.services.invoker import Invoker
from invokeai.app.services.users.users_common import UserCreateRequest
@pytest.fixture(autouse=True, scope="module")
def client(invokeai_root_dir: Path) -> TestClient:
"""Create a test client for the FastAPI app."""
os.environ["INVOKEAI_ROOT"] = invokeai_root_dir.as_posix()
return TestClient(app)
@pytest.fixture(autouse=True)
def enable_multiuser_for_auth_tests(mock_invoker: Invoker) -> None:
"""Enable multiuser mode for auth tests.
Auth tests need multiuser mode enabled since the login/setup endpoints
return 403 when multiuser is disabled.
"""
mock_invoker.services.configuration.multiuser = True
class MockApiDependencies(ApiDependencies):
"""Mock API dependencies for testing."""
invoker: Invoker
def __init__(self, invoker) -> None:
self.invoker = invoker
def setup_test_user(mock_invoker: Invoker, email: str = "test@example.com", password: str = "TestPass123") -> str:
"""Helper to create a test user and return user_id."""
user_service = mock_invoker.services.users
user_data = UserCreateRequest(
email=email,
display_name="Test User",
password=password,
is_admin=False,
)
user = user_service.create(user_data)
return user.user_id
def setup_test_admin(mock_invoker: Invoker, email: str = "admin@example.com", password: str = "AdminPass123") -> str:
"""Helper to create a test admin user and return user_id."""
user_service = mock_invoker.services.users
user_data = UserCreateRequest(
email=email,
display_name="Admin User",
password=password,
is_admin=True,
)
user = user_service.create(user_data)
return user.user_id
class TestSQLInjectionPrevention:
"""Tests to ensure SQL injection attacks are prevented."""
def test_login_sql_injection_in_email(self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient):
"""Test that SQL injection in email field is prevented."""
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
# Create a legitimate user first
setup_test_user(mock_invoker, "legitimate@example.com", "TestPass123")
# Try SQL injection in email field
sql_injection_attempts = [
"' OR '1'='1",
"admin' --",
"' OR 1=1 --",
"'; DROP TABLE users; --",
"' UNION SELECT * FROM users --",
]
for injection_attempt in sql_injection_attempts:
response = client.post(
"/api/v1/auth/login",
json={
"email": injection_attempt,
"password": "TestPass123",
"remember_me": False,
},
)
# Should return 401 (invalid credentials) or 422 (validation error)
# Both are acceptable - the important thing is no SQL injection occurs
assert response.status_code in [401, 422], f"SQL injection attempt should be rejected: {injection_attempt}"
# Should NOT return 200 (success) or 500 (server error)
assert response.status_code != 200, f"SQL injection should not succeed: {injection_attempt}"
assert response.status_code != 500, f"SQL injection should not cause server error: {injection_attempt}"
def test_login_sql_injection_in_password(self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient):
"""Test that SQL injection in password field is prevented."""
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
# Create a legitimate user
setup_test_user(mock_invoker, "test@example.com", "TestPass123")
# Try SQL injection in password field
sql_injection_attempts = [
"' OR '1'='1",
"anything' OR '1'='1' --",
"' OR 1=1; DROP TABLE users; --",
]
for injection_attempt in sql_injection_attempts:
response = client.post(
"/api/v1/auth/login",
json={
"email": "test@example.com",
"password": injection_attempt,
"remember_me": False,
},
)
# Should fail authentication
assert response.status_code == 401, f"SQL injection attempt should be rejected: {injection_attempt}"
def test_user_service_sql_injection_in_email(self, mock_invoker: Invoker):
"""Test that user service prevents SQL injection in email lookups."""
user_service = mock_invoker.services.users
# Create a test user
setup_test_user(mock_invoker, "test@example.com", "TestPass123")
# Try SQL injection in get_by_email
sql_injection_attempts = [
"test@example.com' OR '1'='1",
"' OR 1=1 --",
"test@example.com'; DROP TABLE users; --",
]
for injection_attempt in sql_injection_attempts:
# Should return None (not found), not raise an error or return wrong user
user = user_service.get_by_email(injection_attempt)
assert user is None, f"SQL injection should not return a user: {injection_attempt}"
class TestAuthorizationBypass:
"""Tests to ensure authorization cannot be bypassed."""
def test_cannot_access_protected_endpoint_without_token(self, client: TestClient):
"""Test that protected endpoints require authentication."""
# Try to access protected endpoint without token
response = client.get("/api/v1/auth/me")
assert response.status_code == 401
def test_cannot_access_protected_endpoint_with_invalid_token(
self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient
):
"""Test that invalid tokens are rejected."""
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
invalid_tokens = [
"invalid_token",
"Bearer invalid_token",
"",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.invalid.signature",
]
for token in invalid_tokens:
response = client.get("/api/v1/auth/me", headers={"Authorization": f"Bearer {token}"})
assert response.status_code == 401, f"Invalid token should be rejected: {token}"
def test_cannot_forge_admin_token(self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient):
"""Test that admin privileges cannot be forged by modifying tokens."""
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
# Create a regular user and login
setup_test_user(mock_invoker, "regular@example.com", "TestPass123")
login_response = client.post(
"/api/v1/auth/login",
json={
"email": "regular@example.com",
"password": "TestPass123",
"remember_me": False,
},
)
token = login_response.json()["token"]
# Try to modify the token to gain admin privileges
# (In practice, this should fail signature verification)
parts = token.split(".")
if len(parts) == 3:
# Decode the payload, modify it, and re-encode
import base64
import json
# Add padding if necessary
payload_b64 = parts[1]
padding = 4 - len(payload_b64) % 4
if padding != 4:
payload_b64 += "=" * padding
# Decode payload
try:
payload_bytes = base64.urlsafe_b64decode(payload_b64)
payload_data = json.loads(payload_bytes)
# Modify is_admin to true
payload_data["is_admin"] = True
# Re-encode
modified_payload_bytes = json.dumps(payload_data).encode()
modified_payload_b64 = base64.urlsafe_b64encode(modified_payload_bytes).decode().rstrip("=")
# Create forged token with modified payload but original signature
modified_token = f"{parts[0]}.{modified_payload_b64}.{parts[2]}"
# Attempt to use modified token
response = client.get("/api/v1/auth/me", headers={"Authorization": f"Bearer {modified_token}"})
# Should be rejected (invalid signature)
assert response.status_code == 401
except Exception:
# If we can't decode/modify the token, that's fine - just skip this part of the test
pass
def test_regular_user_cannot_create_admin(self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient):
"""Test that regular users cannot create admin users."""
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
# This test would require user management endpoints to be implemented
# For now, we test at the service level
user_service = mock_invoker.services.users
# Create a regular user
regular_user_data = UserCreateRequest(
email="regular@example.com",
display_name="Regular User",
password="TestPass123",
is_admin=False,
)
user_service.create(regular_user_data)
# Try to create an admin user (should only be possible through setup or by existing admin)
# The create_admin method checks if an admin already exists
admin_data = UserCreateRequest(
email="sneaky@example.com",
display_name="Sneaky Admin",
password="TestPass123",
)
# First create an actual admin
setup_test_admin(mock_invoker, "realadmin@example.com", "AdminPass123")
# Now trying to create another admin should fail
with pytest.raises(ValueError, match="already exists"):
user_service.create_admin(admin_data)
class TestSessionSecurity:
"""Tests for session and token security."""
def test_token_expires_after_time(self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient):
"""Test that tokens expire after their validity period."""
from datetime import timedelta
from invokeai.app.services.auth.token_service import TokenData, create_access_token
# Create a token that expires quickly
token_data = TokenData(
user_id="user123",
email="test@example.com",
is_admin=False,
)
# Create token with 10 millisecond expiration
expired_token = create_access_token(token_data, expires_delta=timedelta(milliseconds=10))
# Wait for expiration (wait longer than expiration time)
import time
time.sleep(0.02)
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
# Try to use expired token
response = client.get("/api/v1/auth/me", headers={"Authorization": f"Bearer {expired_token}"})
assert response.status_code == 401
def test_logout_invalidates_session(self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient):
"""Test that logout invalidates the session.
Note: Current implementation uses JWT which is stateless.
This test documents expected behavior for future server-side session tracking.
"""
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
# Create user and login
setup_test_user(mock_invoker, "test@example.com", "TestPass123")
login_response = client.post(
"/api/v1/auth/login",
json={
"email": "test@example.com",
"password": "TestPass123",
"remember_me": False,
},
)
token = login_response.json()["token"]
# Logout
logout_response = client.post("/api/v1/auth/logout", headers={"Authorization": f"Bearer {token}"})
assert logout_response.status_code == 200
# Note: With JWT, the token is still technically valid until expiration
# For true session invalidation, server-side session tracking would be needed
class TestInputValidation:
"""Tests for input validation and sanitization."""
def test_email_validation_on_login(self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient):
"""Test that email validation is enforced on login."""
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
# Invalid email formats should be rejected by pydantic validation
invalid_emails = [
"not_an_email",
"@example.com",
"user@",
"user @example.com", # space in email
"../../../etc/passwd", # path traversal attempt
]
for invalid_email in invalid_emails:
response = client.post(
"/api/v1/auth/login",
json={
"email": invalid_email,
"password": "TestPass123",
"remember_me": False,
},
)
# Should return 422 (validation error) or 401 (invalid credentials)
assert response.status_code in [401, 422], f"Invalid email should be rejected: {invalid_email}"
def test_xss_prevention_in_user_data(self, mock_invoker: Invoker):
"""Test that XSS attempts in user data are handled safely.
Note: Database storage uses parameterized queries which prevent XSS.
This test ensures data is stored and retrieved without executing scripts.
"""
user_service = mock_invoker.services.users
# Try to create user with XSS payload in display name
xss_payloads = [
"<script>alert('xss')</script>",
"'; alert('xss'); //",
"<img src=x onerror=alert('xss')>",
]
for payload in xss_payloads:
user_data = UserCreateRequest(
email=f"xss{hash(payload)}@example.com", # unique email
display_name=payload,
password="TestPass123",
is_admin=False,
)
# Should not raise an error - data is stored as-is
user = user_service.create(user_data)
# Verify data is stored exactly as provided (not executed or modified)
assert user.display_name == payload
# Cleanup
user_service.delete(user.user_id)
def test_path_traversal_prevention(self, mock_invoker: Invoker):
"""Test that path traversal attempts in user input are handled."""
user_service = mock_invoker.services.users
# Path traversal attempts
path_traversal_attempts = [
"../../../etc/passwd",
"..\\..\\..\\windows\\system32",
"user/../../../secret",
]
for attempt in path_traversal_attempts:
# These should be stored as literal strings, not interpreted as paths
user_data = UserCreateRequest(
email=f"path{hash(attempt)}@example.com",
display_name=attempt,
password="TestPass123",
is_admin=False,
)
user = user_service.create(user_data)
assert user.display_name == attempt
# Cleanup
user_service.delete(user.user_id)
class TestRateLimiting:
"""Tests for rate limiting and brute force protection.
Note: Rate limiting is not currently implemented in the codebase.
These tests document expected behavior for future implementation.
"""
@pytest.mark.skip(reason="Rate limiting not yet implemented")
def test_login_rate_limiting(self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient):
"""Test that excessive login attempts are rate limited."""
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
setup_test_user(mock_invoker, "test@example.com", "TestPass123")
# Try many login attempts with wrong password
for i in range(20):
response = client.post(
"/api/v1/auth/login",
json={
"email": "test@example.com",
"password": "WrongPassword",
"remember_me": False,
},
)
if i < 10:
# First attempts should return 401
assert response.status_code == 401
else:
# After many attempts, should be rate limited (429)
# This is expected behavior for future implementation
pass