Files
lokses 3d8f88b61e security: CSP-middleware (Фаза 1, Report-Only, выкл. по умолчанию)
ContentSecurityPolicyMiddleware ставит заголовок CSP при CSP_ENABLED=True.
По умолчанию выключен; CSP_REPORT_ONLY=True — только отчёты, ничего не
блокирует (для сбора нарушений перед боевым режимом).

Политика (дефолт под текущий стек): default-src 'self'; object-src 'none';
base-uri 'self'; frame-ancestors 'none'; form-action 'self';
script/style-src с 'unsafe-inline' (пока есть инлайн onclick/<script> —
Фаза 2: вынести и перейти на nonce) + CDN (jsdelivr/unpkg/cdnjs) + тайлы карт.
CSP_EXTRA_HOSTS — внешние хосты (напр. домен OnlyOffice Document Server).

htmx CSP-чистый (allowEval=false, нет hx-on) — 'unsafe-eval' не нужен.

Тесты: выкл по умолчанию, Report-Only/enforce заголовки, extra-hosts. 795 ок.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-11-01 16:49:07 +02:00

55 lines
2.2 KiB
Python

from datetime import date
from django.test import TestCase, override_settings
from apps.employees.models import Employee
from core.test_utils import login_2fa
def _user():
from django.contrib.auth.models import Group
emp = Employee.objects.create(
username='csp.user', email='csp.user@company.local',
first_name='CSP', last_name='Тест', hire_date=date(2026, 1, 1),
user_location_type='office', mattermost_sync_enabled=False, ad_sync_enabled=False,
)
emp.set_unusable_password()
emp.groups.add(Group.objects.get_or_create(name='Сотрудники (тест)')[0])
emp.set_password('x')
emp.save()
return emp
class CspMiddlewareTests(TestCase):
def setUp(self):
self.user = _user()
login_2fa(self.client, self.user)
def test_off_by_default(self):
resp = self.client.get('/dashboard/', HTTP_HX_REQUEST='true')
self.assertNotIn('Content-Security-Policy', resp)
self.assertNotIn('Content-Security-Policy-Report-Only', resp)
@override_settings(CSP_ENABLED=True, CSP_REPORT_ONLY=True)
def test_report_only_header(self):
resp = self.client.get('/dashboard/', HTTP_HX_REQUEST='true')
self.assertIn('Content-Security-Policy-Report-Only', resp)
self.assertNotIn('Content-Security-Policy', resp)
policy = resp['Content-Security-Policy-Report-Only']
for part in ("default-src 'self'", "object-src 'none'",
"frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'"):
self.assertIn(part, policy)
@override_settings(CSP_ENABLED=True, CSP_REPORT_ONLY=False)
def test_enforced_header(self):
resp = self.client.get('/dashboard/', HTTP_HX_REQUEST='true')
self.assertIn('Content-Security-Policy', resp)
self.assertIn("script-src 'self'", resp['Content-Security-Policy'])
@override_settings(CSP_ENABLED=True, CSP_REPORT_ONLY=True,
CSP_EXTRA_HOSTS=['https://docs.example.com'])
def test_extra_hosts_included(self):
resp = self.client.get('/dashboard/', HTTP_HX_REQUEST='true')
policy = resp['Content-Security-Policy-Report-Only']
self.assertIn('https://docs.example.com', policy) # хост OnlyOffice DS