feat: enhance security audit and CSP configuration for admin interface

This commit is contained in:
hamed
2026-07-23 14:12:05 +03:30
parent 9a43dcf798
commit f218bc17ef
5 changed files with 27 additions and 9 deletions
+4 -2
View File
@@ -23,6 +23,8 @@
### وضعیت رفع (۲۰۲۶-۰۷-۱۹) ### وضعیت رفع (۲۰۲۶-۰۷-۱۹)
- **۱ — CSP:** `src/Shared/EventSubscriber/AdminCspSubscriber.php` افزوده شد؛ CSP فقط روی `/admin/*`. تأیید زنده: هدر `content-security-policy` روی `/admin` حاضر است. تست: `tests/Shared/AdminCspSubscriberTest.php` (۲ تست سبز). - **۱ — 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). - **۳ — 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 سبز. - **۴ — cookie flags:** `config/packages/framework.yaml``cookie_secure: true`، `cookie_samesite: lax`، `cookie_httponly: true`. لینت YAML سبز، ۵۶ تست Shared/Smoke سبز.
- **۲ — deps:** - **۲ — deps:**
@@ -97,9 +99,9 @@ final class AdminCspSubscriber implements EventSubscriberInterface
"default-src 'self'; " "default-src 'self'; "
. "script-src 'self'; " . "script-src 'self'; "
. "style-src 'self' 'unsafe-inline'; " // Tailwind/CKEditor inline styles . "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:; " . "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'" . "frame-ancestors 'none'; base-uri 'self'; object-src 'none'"
); );
} }
+13 -2
View File
@@ -66,13 +66,24 @@ self.addEventListener('fetch', (event) => {
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
return response; 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; return;
} }
// Everything else → Network First with cache fallback // Everything else → Network First with cache fallback
event.respondWith( event.respondWith(
fetch(request).catch(() => caches.match(request)) fetch(request).catch(async () =>
(await caches.match(request)) || Response.error()
)
); );
}); });
@@ -40,9 +40,11 @@ final class AdminCspSubscriber implements EventSubscriberInterface
. "script-src 'self'; " . "script-src 'self'; "
. "worker-src 'self' blob:; " // ALTCHA proof-of-work solver runs in blob: Web Workers . "worker-src 'self' blob:; " // ALTCHA proof-of-work solver runs in blob: Web Workers
. "style-src 'self' 'unsafe-inline'; " // Tailwind / CKEditor inline styles . "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:; " . "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'; " . "frame-ancestors 'none'; "
. "base-uri 'self'; " . "base-uri 'self'; "
. "object-src 'none'" . "object-src 'none'"
+1 -3
View File
@@ -13,9 +13,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="ClinicPro"> <meta name="apple-mobile-web-app-title" content="ClinicPro">
<link rel="apple-touch-icon" href="/logo.svg"> <link rel="apple-touch-icon" href="/logo.svg">
<link rel="preconnect" href="https://fonts.googleapis.com"> {# Vazirmatn is bundled locally via @fontsource in styles.css; no external font CDN (CSP-safe). #}
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100..900&display=swap" rel="stylesheet">
{{ encore_entry_link_tags('admin') }} {{ encore_entry_link_tags('admin') }}
</head> </head>
<body> <body>
+5
View File
@@ -23,6 +23,11 @@ class AdminCspSubscriberTest extends WebTestCase
// ALTCHA solves its proof-of-work inside blob: Web Workers; without this // 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). // directive the login captcha errors out (worker-src falls back to script-src).
self::assertStringContainsString("worker-src 'self' blob:", $csp); 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 public function testNonAdminResponseDoesNotGetTheAdminCsp(): void