Mengeraskan aplikasi sesuai OWASP: security headers (CSP, HSTS, X-Frame-Options), proteksi CSRF, XSS dengan auto-escaping Twig, SQLi dengan prepared statement Doctrine, serta keamanan file upload dari validasi MIME hingga nama file acak.

Setelah testing di episode 17, episode 18 masuk ke pertahanan aktif: keamanan aplikasi. Framework menyelesaikan banyak masalah, tapi kalian tetap harus memahami di mana celah bisa muncul — dan bagaimana menutupnya sesuai praktik terbaik OWASP.
Mengapa ini penting meskipun Symfony sudah aman secara default? Karena keamanan adalah tanggung jawab berlapis: framework mengamankan dasar (escaping, CSRF, prepared statement), tetapi header HTTP, kebijakan content, dan validasi upload adalah keputusan yang tetap di tangan kalian. Serangan yang berhasil di aplikasi Symfony biasanya bukan karena framework-nya, melainkan karena konfigurasi yang longgar.
Header HTTP adalah "polisi di pintu gerbang" browser. Set header standar di semua response lewat listener:
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
#[AsEventListener(event: ResponseEvent::class)]
final class SecurityHeadersListener
{
public function __invoke(ResponseEvent $event): void
{
$response = $event->getResponse();
$response->headers->set('Content-Security-Policy', "default-src 'self'");
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('Referrer-Policy', 'no-referrer');
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
}| Header | Fungsi |
|---|---|
Content-Security-Policy | Daftar sumber konten yang diizinkan browser |
X-Frame-Options: DENY | Tolak embedding di iframe (clickjacking) |
X-Content-Type-Options: nosniff | Cegah MIME sniffing |
Referrer-Policy | Kontrol info yang dikirim ke situs lain |
Strict-Transport-Security | Paksa HTTPS (hanya via HTTPS) |
Permissions-Policy | Batasi fitur browser (kamera, lokasi, dll) |
Warning
CSP bukan tanpa risiko: kebijakan yang terlalu ketat bisa memblokir script atau style yang sah. Mulailah dengan Content-Security-Policy-Report-Only untuk mengumpulkan laporan pelanggaran tanpa memblokir, perbaiki, lalu aktifkan penuh. Terapkan bertahap, bukan sekali jadi.
CSRF (Cross-Site Request Forgery): pengguna yang sedang login dibuat mengirim request oleh situs jahat. Form Symfony sudah menangani ini — token CSRF ter-render otomatis di form_start (episode 7). Yang perlu diperhatikan:
csrf_protection di FormType tanpa alasan kuat.use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
public function toggle(
Request $request,
CsrfTokenManagerInterface $csrf,
): Response
{
$token = $request->request->get('_token');
if (!$csrf->isTokenValid(new CsrfToken('toggle', (string) $token))) {
throw $this->createAccessDeniedException('Token CSRF tidak valid.');
}
// ...
}Di API stateless (episode 13/19), CSRF diganti mekanisme token — karena tidak ada session, konsep CSRF tidak berlaku sama.
XSS (Cross-Site Scripting): input user dieksekusi sebagai HTML/script di browser orang lain. Pertahanan utamanya sudah ada: Twig auto-escape semua output secara default. Aturan yang harus dipegang:
{{ user.name }} → otomatis di-escape. Aman.{{ user.bio|raw }} → mati auto-escape. Berbahaya jika bio dari user.{{ form_widget(form) }} → aman, form Symfony meng-escape otomatis.{# BERBAHAYA: bio dari user dirender mentah #}
{{ user.bio|raw }}
{# AMAN: escape otomatis #}
{{ user.bio }}Jika memang harus menampilkan HTML dari user (rich text), sanitasi dulu dengan library seperti html-sanitizer (Symfony HTML Sanitizer) — bukan |raw.
SQL Injection: input user dicampur ke query SQL. Doctrine selalu memakai prepared statement — selama kalian tidak menggabungkan string:
// AMAN — parameter terikat (prepared statement)
return $this->createQueryBuilder('a')
->where('a.title LIKE :term')
->setParameter('term', "%{$term}%")
->getQuery()
->getResult();
// BERBAHAYA — jangan pernah lakukan ini
// "SELECT * FROM article WHERE title LIKE '%{$_GET['q']}%'"Aturan mutlak: jangan pernah menulis query dengan interpolasi input user, apa pun alasannya. setParameter() adalah satu-satunya jalan.
Upload adalah salah satu vektor serangan paling sering. Prinsip keamanannya:
upload_max_filesize dan constraint.Content-Disposition dan X-Content-Type-Options.Contoh dengan constraint upload:
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;
#[Assert\File(
maxSize: '2M',
extensions: ['pdf', 'png', 'jpg'],
mimeTypes: ['application/pdf', 'image/png', 'image/jpeg'],
)]
private ?UploadedFile $document = null;Rename sebelum simpan:
$safeName = bin2hex(random_bytes(16)) . '.' . $file->guessExtension();
$file->move($uploadDir, $safeName);bin2hex(random_bytes(16)) menghasilkan nama yang tidak bisa ditebak — mencegah path traversal & overwrite. Untuk SVG: hati-hati, SVG bisa membawa script — sanitasi atau blokir.
| Risiko OWASP | Mitigasi di Symfony |
|---|---|
| Broken Access Control | Firewall, roles, voters (episode 12) |
| Cryptographic Failures | password_hashers auto, HTTPS, Secrets Vault |
| Injection (SQLi/XSS) | Prepared statement + Twig auto-escape |
| Insecure Design | Pemisahan logika + testing (episode 17) |
| Security Misconfiguration | Multi-env config (episode 16), header hardening |
| Vulnerable Components | composer audit — cek di bawah |
| Auth Failures | Authenticator, MFA (episode 19) |
| Data Integrity (CSRF) | Token CSRF otomatis |
| Logging Failures | Structured logging (episode 15) |
| SSRF | Restriksi URL HttpClient, allowlist |
Cek dependensi yang rentan secara rutin:
composer audit
composer audit --format=jsonJalankan di CI (episode 23) agar setiap merge ter-scan otomatis.
Pada episode 18 ini, kalian telah mengeraskan aplikasi sesuai OWASP.
Inti yang harus dibawa pulang:
|raw) untuk rich text.composer audit rutin untuk dependensi rentan.Di episode 19 selanjutnya kita masuk ke Auth Lanjutan: API Tokens & OAuth2 — JWT access/refresh, integrasi OAuth2 dengan league/oauth2-server, API-Key auth, dan alur auth untuk SPA dengan PKCE. Sampai jumpa di episode 19!