Add CSRF tokens to all cookie-based POST endpoints. Set Secure and SameSite=Strict on auth cookie. Rate limit login to 5 attempts per 15min per IP. Escape HTML in signing error messages (XSS fix). Remove duplicate get_user_from_cookie definition.
60 lines
2.0 KiB
Python
60 lines
2.0 KiB
Python
import os
|
|
import sys
|
|
import unittest
|
|
from unittest import mock
|
|
from httpx import AsyncClient, ASGITransport
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "api"))
|
|
|
|
|
|
class TestAuthSecurity(unittest.TestCase):
|
|
|
|
def test_rate_limiting_exists(self):
|
|
import main
|
|
self.assertTrue(hasattr(main, '_check_rate_limit'))
|
|
self.assertEqual(main._LOGIN_MAX_ATTEMPTS, 5)
|
|
self.assertEqual(main._LOGIN_WINDOW_SECONDS, 900)
|
|
|
|
def test_csrf_token_generation(self):
|
|
import main
|
|
t1 = main._generate_csrf_token()
|
|
t2 = main._generate_csrf_token()
|
|
self.assertNotEqual(t1, t2)
|
|
self.assertEqual(len(t1), 64)
|
|
|
|
def test_csrf_verify_rejects_empty(self):
|
|
import main
|
|
req = mock.MagicMock()
|
|
req.cookies.get.return_value = None
|
|
self.assertFalse(main._verify_csrf_token(req, "any-token"))
|
|
|
|
def test_csrf_verify_rejects_mismatch(self):
|
|
import main
|
|
req = mock.MagicMock()
|
|
req.cookies.get.return_value = "stored-token"
|
|
self.assertFalse(main._verify_csrf_token(req, "different-token"))
|
|
|
|
def test_csrf_verify_accepts_match(self):
|
|
import main
|
|
req = mock.MagicMock()
|
|
req.cookies.get.return_value = "matching-token"
|
|
self.assertTrue(main._verify_csrf_token(req, "matching-token"))
|
|
|
|
def test_no_duplicate_get_user_from_cookie(self):
|
|
import main, inspect
|
|
sources = inspect.getsourcelines(main)[0]
|
|
count = sum(1 for line in sources if 'def get_user_from_cookie' in line)
|
|
self.assertEqual(count, 1, "get_user_from_cookie must be defined exactly once")
|
|
|
|
def test_xss_escaped_in_sign_error(self):
|
|
"""Check that HTML in error messages gets escaped."""
|
|
import html as h
|
|
err = '<script>alert("xss")</script>'
|
|
escaped = h.escape(err)
|
|
self.assertNotIn("<script>", escaped)
|
|
self.assertIn("<script>", escaped)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|