Belajar Symfony - Security Headers & OWASP Best Practices
Episode 18 of 27

Belajar Symfony - Security Headers & OWASP Best Practices

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.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

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.

Security Headers

Header HTTP adalah "polisi di pintu gerbang" browser. Set header standar di semua response lewat listener:

src/EventListener/SecurityHeadersListener.php
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');
    }
}
HeaderFungsi
Content-Security-PolicyDaftar sumber konten yang diizinkan browser
X-Frame-Options: DENYTolak embedding di iframe (clickjacking)
X-Content-Type-Options: nosniffCegah MIME sniffing
Referrer-PolicyKontrol info yang dikirim ke situs lain
Strict-Transport-SecurityPaksa HTTPS (hanya via HTTPS)
Permissions-PolicyBatasi 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: Menolak Serangan dari Situs Lain

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:

  1. Jangan matikan csrf_protection di FormType tanpa alasan kuat.
  2. Untuk route POST yang tidak memakai form (misal API aksi), tambahkan token manual:
CSRF di route non-form
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: Auto-Escaping Twig

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.
Contoh XSS yang harus dihindari
{# 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.

SQLi: Prepared Statement Doctrine

SQL Injection: input user dicampur ke query SQL. Doctrine selalu memakai prepared statement — selama kalian tidak menggabungkan string:

Query aman vs berbahaya
// 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.

File Upload Security

Upload adalah salah satu vektor serangan paling sering. Prinsip keamanannya:

  1. Validasi tipe — cek MIME & ekstensi (bukan hanya ekstensi).
  2. Ukuran dibatasi — via upload_max_filesize dan constraint.
  3. Nama file acak — jangan pernah memakai nama asli user.
  4. Simpan di luar document root (atau di bucket dengan kebijakan ketat).
  5. Serve dengan header yang benarContent-Disposition dan X-Content-Type-Options.

Contoh dengan constraint upload:

Validasi 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:

Simpan dengan nama acak
$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.

OWASP Top 10 → Peta ke Symfony

Risiko OWASPMitigasi di Symfony
Broken Access ControlFirewall, roles, voters (episode 12)
Cryptographic Failurespassword_hashers auto, HTTPS, Secrets Vault
Injection (SQLi/XSS)Prepared statement + Twig auto-escape
Insecure DesignPemisahan logika + testing (episode 17)
Security MisconfigurationMulti-env config (episode 16), header hardening
Vulnerable Componentscomposer audit — cek di bawah
Auth FailuresAuthenticator, MFA (episode 19)
Data Integrity (CSRF)Token CSRF otomatis
Logging FailuresStructured logging (episode 15)
SSRFRestriksi URL HttpClient, allowlist

Cek dependensi yang rentan secara rutin:

Audit dependensi
composer audit
composer audit --format=json

Jalankan di CI (episode 23) agar setiap merge ter-scan otomatis.

Penutup

Pada episode 18 ini, kalian telah mengeraskan aplikasi sesuai OWASP.

Inti yang harus dibawa pulang:

  • Security headers via listener: CSP, HSTS, X-Frame-Options, dan lainnya.
  • CSRF: biarkan proteksi form aktif; token manual untuk route non-form.
  • XSS: Twig auto-escape; sanitasi (bukan |raw) untuk rich text.
  • SQLi: prepared statement Doctrine; jangan pernah interpolasi input.
  • Upload: validasi MIME, batas ukuran, nama acak, simpan di luar docroot.
  • 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!