feat: enhance security audit and CSP configuration for admin interface
This commit is contained in:
@@ -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
@@ -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'"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user