Bali, Indonesia
Insights

Pindah ke Nginx, Aturan .htaccess Pemblokir PHP di Folder uploads Tidak Berlaku

October 30, 2026
0:00
Situs WordPress baru saja dipindah dari Apache ke Nginx, semua halaman tampil normal, lalu beberapa waktu kemudian muncul file PHP asing di wp-content/uploads yang bisa dipanggil langsung dari browser. Padahal file .htaccess yang dulu memblokir PHP di folder itu masih ada di tempatnya. Rule-nya tidak rusak. Nginx memang tidak pernah membacanya. Tulisan ini disusun seperti post-mortem untuk pola insiden yang cukup sering muncul setelah migrasi semacam ini. Ini bukan catatan satu kejadian tertentu, tapi urutan yang biasanya dilewati saat menelusuri masalahnya: dampak awal, jalur investigasi, titik temuan, mitigasi cepat, lalu perbaikan yang lebih permanen.
Folder wp-content/uploads harus bisa ditulisi oleh WordPress, karena di situlah gambar dan file media disimpan. Artinya, kalau ada plugin atau form upload yang punya celah, penyerang bisa menaruh file .php di sana. Selama PHP tidak dieksekusi di folder itu, file tersebut cuma teks yang tidak berbuat apa-apa. Begitu PHP bisa jalan, file itu berubah jadi webshell, yaitu skrip kecil yang memberi penyerang akses ke server lewat browser. Webshell yang jalan memakai user PHP-FPM yang sama dengan situs. Dari situ dia bisa membaca wp-config.php beserta kredensial database, mengubah file tema dan plugin, menyisipkan redirect, atau mengirim spam. Latar belakang kenapa folder ini berisiko sudah dibahas di artikel Mengira Folder uploads WordPress Aman Meski PHP Masih Bisa Dijalankan di Sana. Yang membuat kasus ini licin adalah tidak ada tanda error sama sekali. Situs tetap cepat, nginx -t lolos, dan tidak ada warning soal .htaccess. Dari sisi support, gejala yang biasanya dilaporkan malah efek sampingnya: pengunjung dilempar ke situs lain, ada laporan abuse karena spam, CPU naik tanpa trafik yang jelas, atau scanner keamanan menandai file asing.
Urutan pengecekan yang masuk akal biasanya begini:
  1. Konfirmasi gejala dan catat kapan mulai terlihat.
  2. Cari file PHP di folder yang seharusnya hanya berisi media.
  3. Cek access log untuk melihat apakah file itu pernah dipanggil, dan apa status responsnya.
  4. Lihat apakah .htaccess pemblokir masih ada di folder uploads.
  5. Uji langsung apakah PHP di folder itu dieksekusi oleh server yang sekarang.
Langkah 2 sampai 5 bisa dikerjakan dengan perintah berikut. Ganti path WordPress dan domain sesuai setup kamu. Lokasi access log juga bisa berbeda tergantung distro dan konfigurasi vhost.
BASH
# Cari file PHP di folder uploads (read-only)
find /path/ke/wordpress/wp-content/uploads -type f -iname '*.php'

# File yang berubah dalam 7 hari terakhir
find /path/ke/wordpress/wp-content/uploads -type f -mtime -7 -ls

# Request ke file PHP di uploads yang tercatat di access log
grep -E 'wp-content/uploads/.*\.php' /var/log/nginx/access.log

# Uji eksekusi: buat file uji yang tidak berbahaya, panggil, lalu hapus
printf '<?php echo 7*6;' > /path/ke/wordpress/wp-content/uploads/uji-eksekusi.php
curl -i https://domain-kamu.tld/wp-content/uploads/uji-eksekusi.php
rm /path/ke/wordpress/wp-content/uploads/uji-eksekusi.php
Cara membaca hasil uji eksekusinya sederhana. Kalau body respons berisi 42, PHP dieksekusi di folder uploads dan itu masalahnya. Kalau yang muncul isi kode mentahnya, PHP tidak dieksekusi, tapi ada konfigurasi lain yang perlu dicek. Kalau statusnya 403, aksesnya sudah diblokir. Pastikan file uji langsung dihapus setelah tes, jangan ditinggal. Di access log, perhatikan request POST atau GET ke file .php di bawah uploads dengan status 200. Status 200 di path itu adalah tanda yang perlu ditindaklanjuti, karena artinya server menjalankan file tersebut dan mengembalikan respons.
Di Apache, .htaccess adalah konfigurasi per direktori. Kalau AllowOverride mengizinkan, Apache mengecek file .htaccess di setiap folder sepanjang path yang diminta, di setiap request. Itulah kenapa satu file kecil di uploads cukup untuk memblokir PHP di folder itu saja, tanpa menyentuh config server. Nginx sengaja tidak punya mekanisme itu. Seluruh konfigurasi dibaca terpusat saat Nginx start atau reload, lalu dipakai apa adanya. Tidak ada override per folder, dan file .htaccess diperlakukan seperti file biasa. Pilihan desain ini membuat Nginx lebih ringan dan perilakunya lebih mudah diprediksi, tapi konsekuensinya semua aturan di .htaccess hilang begitu saja saat migrasi kalau tidak diterjemahkan manual. Soal ini juga dibahas lebih luas di Mengira File .htaccess Masih Berguna Saat Pindah dari Apache ke Nginx. Lalu kenapa file di uploads bisa jalan? Config WordPress di Nginx umumnya punya satu blok seperti location ~ \.php$ yang meneruskan semua request berakhiran .php ke PHP-FPM. Blok itu tidak peduli foldernya. Selama path-nya berakhir .php dan file-nya ada, request akan dieksekusi. Rule pemblokir yang dulu ada di .htaccess tidak punya padanan di config baru, jadi tidak ada yang menghalangi. Ada satu pengecualian yang perlu dipastikan. Kalau Nginx hanya berperan sebagai reverse proxy di depan Apache dan request PHP tetap diteruskan ke Apache, .htaccess masih dibaca oleh Apache. Jadi sebelum menyimpulkan, cek dulu siapa yang benar-benar menangani PHP: Nginx langsung ke PHP-FPM, atau Nginx ke Apache.
Prioritas pertama adalah menghentikan eksekusi, baru membersihkan. Tambahkan blok berikut di dalam blok server situs tersebut:
NGINX
# Taruh di dalam blok server, SEBELUM location ~ \.php$
location ~* ^/wp-content/uploads/.*\.(php|phtml|phar)$ {
    deny all;
}
Posisi blok ini penting. Nginx mengevaluasi location bertipe regex (~ dan ~*) sesuai urutan kemunculannya di config, dan yang pertama cocok yang dipakai. Kalau blok location ~ \.php$ ada di atasnya, request ke uploads akan lebih dulu cocok dengan blok PHP umum dan rule pemblokir tidak pernah tersentuh. Kalau WordPress kamu ada di subfolder atau path uploads-nya diubah, sesuaikan regex-nya. Setelah itu, validasi config sebelum reload, lalu ulangi uji eksekusi tadi:
BASH
sudo nginx -t && sudo systemctl reload nginx

# Ulangi uji: buat file uji, harapkan status 403, lalu hapus
printf '<?php echo 7*6;' > /path/ke/wordpress/wp-content/uploads/uji-eksekusi.php
curl -i https://domain-kamu.tld/wp-content/uploads/uji-eksekusi.php
rm /path/ke/wordpress/wp-content/uploads/uji-eksekusi.php
Kalau hasilnya sudah 403, eksekusi sudah tertutup. Tapi file yang sudah terunggah masih ada, dan kalau sempat dijalankan, kemungkinan penyerang sudah meninggalkan jejak di tempat lain. Beberapa hal yang sebaiknya dilakukan setelah blok ini aktif:
  • Pindahkan file PHP asing dari uploads ke folder karantina di luar webroot. Jangan langsung dihapus, karena isinya berguna untuk melihat apa saja yang bisa dilakukan file itu.
  • Cek apakah ada file yang berubah di tema, plugin, dan root WordPress pada rentang waktu yang sama.
  • Anggap wp-config.php sudah terbaca. Ganti password database, regenerasi salt WordPress, dan reset password akun admin.
  • Cari tahu jalur masuknya, biasanya plugin atau tema dengan celah upload. Selama celahnya masih terbuka, file baru bisa terus datang.
Mitigasi tadi menyelesaikan satu situs. Masalah dasarnya adalah aturan keamanan yang dulu tersebar di file .htaccess sekarang harus hidup di config Nginx, dan itu perlu jadi bagian dari proses, bukan perbaikan dadakan. Kalau server menampung banyak situs WordPress, simpan rule pemblokir itu di satu file snippet, lalu include dari setiap blok server. Di Debian dan Ubuntu, folder /etc/nginx/snippets/ biasa dipakai untuk ini, tapi lokasinya bebas selama konsisten. Dengan begitu, setiap vhost baru otomatis mendapat perlindungan yang sama, dan perubahan cukup dilakukan di satu tempat. Kalau config Nginx dibuat oleh control panel, hati-hati menyunting file vhost langsung. Banyak panel menulis ulang file itu saat ada perubahan domain atau SSL, sehingga rule tambahan bisa hilang. Cari mekanisme custom include yang disediakan panelnya, karena caranya berbeda di tiap panel. Untuk migrasi berikutnya, jadikan inventaris .htaccess sebagai langkah wajib sebelum cutover. Cari semua file .htaccess di webroot, baca isinya satu per satu, lalu tentukan padanannya di Nginx. Rule rewrite biasanya langsung ketahuan kalau tidak diterjemahkan karena halaman jadi 404. Rule keamanan seperti blok PHP di uploads, proteksi folder, atau pembatasan akses file sensitif tidak memberi tanda apa pun saat hilang, jadi kelompok inilah yang paling perlu dicek dengan uji akses seperti di atas. Sebagai lapisan tambahan, cek juga security.limit_extensions di pool PHP-FPM. Directive ini membatasi ekstensi file yang mau dieksekusi oleh PHP-FPM, sehingga file seperti gambar.jpg tidak ikut dijalankan sebagai PHP kalau ada salah konfigurasi di Nginx. Nilai default-nya bisa berbeda antar versi dan paket distro, jadi lihat langsung di config pool kamu. Intinya, .htaccess di server Nginx hanyalah file teks biasa. Setiap kali selesai memindahkan WordPress dari Apache, jalankan uji eksekusi PHP di folder uploads dan pastikan hasilnya 403, bukan 42. Tes itu cuma butuh beberapa detik, dan hasilnya langsung menjawab apakah perlindungan lama ikut pindah atau tertinggal di server sebelumnya.