diff --git a/docs/api/captcha.md b/docs/api/captcha.md index 2693fa1d..48852b4a 100644 --- a/docs/api/captcha.md +++ b/docs/api/captcha.md @@ -84,3 +84,4 @@ endpointهایی که وقتی `ALTCHA_ENABLED=true` است فیلد `altcha` ر - انقضا (`ALTCHA_EXPIRE_SECONDS`) داخل salt امضاشده است. - استفاده‌ی یک‌باره: signature هر challenge در pool اختصاصی Redis (`altcha.pool`) تا زمان انقضا نگه‌داری می‌شود → ضدِ replay. - کپچا مکملِ rate-limit موجود است، نه جایگزین آن. +- **CSP:** widget حل proof-of-work را داخل Web Workerهای `blob:` انجام می‌دهد؛ CSP پنل ادمین (`AdminCspSubscriber`) باید `worker-src 'self' blob:` داشته باشد، وگرنه widget با `state=error` می‌ماند و کپچا هرگز حل نمی‌شود. diff --git a/src/Shared/EventSubscriber/AdminCspSubscriber.php b/src/Shared/EventSubscriber/AdminCspSubscriber.php index 65d57abb..e9b264a5 100644 --- a/src/Shared/EventSubscriber/AdminCspSubscriber.php +++ b/src/Shared/EventSubscriber/AdminCspSubscriber.php @@ -38,6 +38,7 @@ final class AdminCspSubscriber implements EventSubscriberInterface 'Content-Security-Policy', "default-src 'self'; " . "script-src 'self'; " + . "worker-src 'self' blob:; " // ALTCHA proof-of-work solver runs in blob: Web Workers . "style-src 'self' 'unsafe-inline'; " // Tailwind / CKEditor inline styles . "img-src 'self' data: blob:; " . "font-src 'self' data:; " diff --git a/tests/Shared/AdminCspSubscriberTest.php b/tests/Shared/AdminCspSubscriberTest.php index 448f86fd..59eac1c8 100644 --- a/tests/Shared/AdminCspSubscriberTest.php +++ b/tests/Shared/AdminCspSubscriberTest.php @@ -20,6 +20,9 @@ class AdminCspSubscriberTest extends WebTestCase self::assertStringContainsString("default-src 'self'", $csp); self::assertStringContainsString("object-src 'none'", $csp); self::assertStringContainsString("frame-ancestors 'none'", $csp); + // ALTCHA solves its proof-of-work inside blob: Web Workers; without this + // directive the login captcha errors out (worker-src falls back to script-src). + self::assertStringContainsString("worker-src 'self' blob:", $csp); } public function testNonAdminResponseDoesNotGetTheAdminCsp(): void