diff --git a/docs/security/AUDIT-2026-07-19.md b/docs/security/AUDIT-2026-07-19.md index 5ba096fa..ae902c38 100644 --- a/docs/security/AUDIT-2026-07-19.md +++ b/docs/security/AUDIT-2026-07-19.md @@ -23,6 +23,8 @@ ### وضعیت رفع (۲۰۲۶-۰۷-۱۹) - **۱ — CSP:** `src/Shared/EventSubscriber/AdminCspSubscriber.php` افزوده شد؛ CSP فقط روی `/admin/*`. تأیید زنده: هدر `content-security-policy` روی `/admin` حاضر است. تست: `tests/Shared/AdminCspSubscriberTest.php` (۲ تست سبز). + - **پیگیری (۲۰۲۶-۰۷-۲۳):** دو خطای کنسول CSP رفع شد. (الف) فونت Vazirmatn از CDN گوگل حذف شد (`templates/admin/index.html.twig`)؛ فونت از قبل به‌صورت لوکال با `@fontsource` باندل می‌شود، پس CDN هم مسدود و هم زائد بود. (ب) هاست‌های نقشهٔ کلینیک به CSP اضافه شد: `img-src` += `*.tile.openstreetmap.org unpkg.com`، `connect-src` += `nominatim.openstreetmap.org *.tile.openstreetmap.org`. بدون این‌ها نقشهٔ Leaflet در `ClinicDetailPage` خالی می‌ماند. تست CSP به ۹ assertion رسید. + - **مرتبط (غیرامنیتی):** باگ Service Worker در `public/sw.js` رفع شد — هندلر navigate در حالت آفلاین وقتی shell کش نشده بود `undefined` به `respondWith` می‌داد → خطای `Failed to convert value to 'Response'`؛ حالا fallback واقعی (Response 503) برمی‌گرداند. - **۳ — APP_SECRET:** مقدار واقعی از `.env.dev` برداشته و placeholder شد (مقدار واقعی باید در `.env.local` خارج از git بماند + rotate). - **۴ — cookie flags:** `config/packages/framework.yaml` → `cookie_secure: true`، `cookie_samesite: lax`، `cookie_httponly: true`. لینت YAML سبز، ۵۶ تست Shared/Smoke سبز. - **۲ — deps:** @@ -97,9 +99,9 @@ final class AdminCspSubscriber implements EventSubscriberInterface "default-src 'self'; " . "script-src 'self'; " . "style-src 'self' 'unsafe-inline'; " // Tailwind/CKEditor inline styles - . "img-src 'self' data: blob:; " + . "img-src 'self' data: blob: https://*.tile.openstreetmap.org https://unpkg.com; " // OSM tiles + Leaflet markers (clinic map) . "font-src 'self' data:; " - . "connect-src 'self'; " + . "connect-src 'self' https://nominatim.openstreetmap.org https://*.tile.openstreetmap.org; " // nominatim geocode + OSM tiles . "frame-ancestors 'none'; base-uri 'self'; object-src 'none'" ); } diff --git a/public/sw.js b/public/sw.js index 9e125811..6408f07c 100644 --- a/public/sw.js +++ b/public/sw.js @@ -66,13 +66,24 @@ self.addEventListener('fetch', (event) => { caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); return response; }) - .catch(() => caches.match('/admin')) + // Offline: fall back to the cached shell. If it was never cached, return a + // real offline Response — respondWith(undefined) throws "Failed to convert + // value to 'Response'" and surfaces as a network error in the console. + .catch(async () => + (await caches.match('/admin')) || + new Response('آفلاین هستید', { + status: 503, + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }) + ) ); return; } // Everything else → Network First with cache fallback event.respondWith( - fetch(request).catch(() => caches.match(request)) + fetch(request).catch(async () => + (await caches.match(request)) || Response.error() + ) ); }); diff --git a/src/Shared/EventSubscriber/AdminCspSubscriber.php b/src/Shared/EventSubscriber/AdminCspSubscriber.php index e9b264a5..50817172 100644 --- a/src/Shared/EventSubscriber/AdminCspSubscriber.php +++ b/src/Shared/EventSubscriber/AdminCspSubscriber.php @@ -40,9 +40,11 @@ final class AdminCspSubscriber implements EventSubscriberInterface . "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:; " + // OpenStreetMap tiles + unpkg Leaflet marker icons render the clinic-location map + . "img-src 'self' data: blob: https://*.tile.openstreetmap.org https://unpkg.com; " . "font-src 'self' data:; " - . "connect-src 'self'; " + // nominatim geocoding + OSM tile fetches for the clinic-location map + . "connect-src 'self' https://nominatim.openstreetmap.org https://*.tile.openstreetmap.org; " . "frame-ancestors 'none'; " . "base-uri 'self'; " . "object-src 'none'" diff --git a/templates/admin/index.html.twig b/templates/admin/index.html.twig index e4773dbd..34e17c30 100644 --- a/templates/admin/index.html.twig +++ b/templates/admin/index.html.twig @@ -13,9 +13,7 @@ - - - + {# Vazirmatn is bundled locally via @fontsource in styles.css; no external font CDN (CSP-safe). #} {{ encore_entry_link_tags('admin') }} diff --git a/tests/Shared/AdminCspSubscriberTest.php b/tests/Shared/AdminCspSubscriberTest.php index 59eac1c8..5674cb82 100644 --- a/tests/Shared/AdminCspSubscriberTest.php +++ b/tests/Shared/AdminCspSubscriberTest.php @@ -23,6 +23,11 @@ class AdminCspSubscriberTest extends WebTestCase // 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); + // The clinic-location map needs OSM tiles (img/connect), unpkg marker icons (img) + // and nominatim geocoding (connect); without these the map is blank + console errors. + self::assertStringContainsString('https://*.tile.openstreetmap.org', $csp); + self::assertStringContainsString('https://unpkg.com', $csp); + self::assertStringContainsString('https://nominatim.openstreetmap.org', $csp); } public function testNonAdminResponseDoesNotGetTheAdminCsp(): void