Lompat ke konten
Server & .htaccess

Mixed Content WordPress Setelah Pindah ke HTTPS

Bersihkan peringatan mixed content WordPress setelah pindah ke https: temukan URL http:// yang tertinggal di database, ganti dengan aman, dan paksa https lewat .htaccess.

Dipublikasikan

Sertifikat kamu sudah terpasang, situsnya sudah dimuat lewat https, tapi ikon gemboknya tetap tidak mau muncul. Itulah mixed content: halamannya sendiri datang lewat https, tapi ada sesuatu di dalamnya — gambar, stylesheet, skrip — yang masih diminta lewat http:// polos. Panduan ini menelusuri cara menemukan URL-URL itu, menggantinya tanpa merusak data terserialisasi, membetulkan siteurl dan home, lalu memaksa https di level server supaya masalahnya tidak bisa kembali.

Apa itu mixed content sebenarnya

Memasang sertifikat mengubah cara koneksinya dienkripsi. Itu tidak mengubah apa yang diminta oleh halamanmu. Setiap http://yoursite.com/wp-content/uploads/logo.png yang pernah ditulis ke sebuah postingan, widget, atau pengaturan tema sebelum perpindahan masih duduk manis di database, dan browser dengan patuh memintanya lewat koneksi yang tidak aman.

Browser memperlakukan dua kategori ini secara berbeda, dan bedanya penting saat kamu menilai seberapa mendesak masalah ini:

  • Active mixed content — skrip, stylesheet, iframe, XHR. Diblokir total. Inilah kenapa sebuah situs bisa terlihat rusak setelah pindah ke https tanpa pesan error di mana pun: sebuah stylesheet ditolak diam-diam.
  • Passive mixed content — gambar, audio, video. Biasanya masih dimuat, tapi ikon gemboknya diturunkan dan sebagian browser menampilkan indikator “not secure”.

Keduanya layak diperbaiki. Hanya yang pertama yang benar-benar merusak.

Langkah 1: cari tahu apa yang masih tidak aman

Mulai dari browser. Buka halamannya, buka developer tools, lalu baca konsolnya. Setiap request yang diblokir atau diturunkan disebutkan di sana lengkap dengan URL penuhnya. Itu memberitahumu apa yang tidak aman; itu tidak memberitahumu di mana benda itu tersimpan.

Untuk itu, lihat langsung ke database. Ekspor lalu cari di dump-nya:

wp db export dump.sql
grep -o "http://example\.com[^\"']*" dump.sql | sort -u | head -50

Kalau WP-CLI tidak tersedia, mysqldump menghasilkan file yang sama:

mysqldump -u USER -p DBNAME > dump.sql

Baca URL unik yang muncul. Path uploads mengarah ke konten postingan dan meta. Path aset tema biasanya mengarah ke opsi. Apa pun dengan domain yang tidak kamu kenali adalah embed eksternal, yang butuh perbaikan berbeda (dibahas di bawah).

Langkah 2: perbaiki siteurl dan home lebih dulu

siteurl dan home adalah dua opsi yang dipakai WordPress untuk membangun hampir semua URL internal yang dia hasilkan. Kalau salah satunya masih berisi http://, WordPress akan terus memancarkan URL tidak aman sebersih apa pun sisa database-nya.

Cek keduanya:

wp option get siteurl
wp option get home

Atau lewat sql:

SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('siteurl', 'home');

Keduanya adalah string biasa, bukan array terserialisasi, jadi update sql langsung memang benar-benar aman di sini:

UPDATE wp_options
SET option_value = REPLACE(option_value, 'http://example.com', 'https://example.com')
WHERE option_name IN ('siteurl', 'home');

Satu jebakan sebelum kamu menghabiskan waktu untuk ini. Kalau wp-config.php mendefinisikan WP_HOME atau WP_SITEURL, konstanta itu menimpa database sepenuhnya dan update kamu akan terlihat seperti tidak melakukan apa-apa:

define( 'WP_HOME', 'https://example.com' );
define( 'WP_SITEURL', 'https://example.com' );

Ubah di sana, atau hapus keduanya dan biarkan database yang mengendalikan. Cek ini duluan — ini menjelaskan banyak sekali kasus “saya sudah ubah tapi tidak ada yang berubah”.

Langkah 3: replace-nya harus aman terhadap serialisasi

Sisa isi database inilah tempat risiko sesungguhnya berada, dan risikonya bukan yang kebanyakan orang bayangkan. Bahayanya bukan replace-nya melewatkan URL. Bahayanya adalah replace-nya berhasil di level teks lalu menghancurkan data di sekitarnya.

WordPress menyimpan array dan objek sebagai string terserialisasi php, dan format itu mencatat panjang byte dari setiap string yang dikandungnya:

a:1:{s:4:"logo";s:38:"http://example.com/img/logo.png";}

Ubah http:// menjadi https:// dengan REPLACE sql mentah dan teksnya bertambah satu byte sementara panjang yang dideklarasikan tetap di nilai lama. PHP membaca panjangnya, melangkah maju sebanyak byte itu, tidak menemukan terminator di tempat yang dia harapkan, lalu menolak meng-unserialize seluruh array-nya. WordPress kemudian menyerahkan ke tema atau plugin sebuah nilai yang berperilaku seolah pengaturannya tidak pernah disimpan.

Gejalanya bukan pesan error. Gejalanya adalah widget yang hilang, pengaturan customizer yang ter-reset, dan tata letak page builder yang tampil kosong — tanpa apa pun di admin yang menandakan ada yang salah. Pemulihan dari kondisi itu tanpa backup benar-benar menyakitkan, jadi ambil backup dulu:

wp db export backup-before-https-replace.sql

Setelah itu pakai alat yang meng-unserialize, mengganti di dalam struktur yang sudah didekode, lalu men-serialize ulang dengan panjang yang sudah dikoreksi. Jalankan dry run dulu:

wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid

Baca laporannya. Kalau ada tabel yang tidak kamu kenali menunjukkan ribuan kecocokan, berhenti dan periksa dulu sebelum menjalankannya beneran. Lalu jalankan yang sungguhan:

wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only

--skip-columns=guid itu penting: guid adalah identitas permanen untuk pembaca feed, bukan URL yang hidup, dan menulis ulangnya bisa membuat pelanggan feed melihat seluruh arsipmu sebagai postingan baru. Kalau kamu perlu melihat kolom mana yang aman untuk sql biasa dan mana yang butuh penanganan sadar-serialisasi sebelum menjalankan apa pun, susun perintahnya dengan alat search and replace sql WordPress.

Kalau kamu tidak bisa memakai command line, plugin migrasi yang secara eksplisit menyatakan menangani data terserialisasi melakukan pekerjaan dekode yang sama lewat layar admin. Kalau sebuah alat tidak menyatakan itu, anggap saja dia tidak melakukannya.

Langkah 4: paksa https di level server

Membersihkan database menghentikan halamanmu dari meminta resource yang tidak aman. Itu tidak menghentikan pengunjung datang lewat http:// sejak awal. Itu urusan redirect di level server, dan di Apache tempatnya ada di .htaccess:

# BEGIN Force HTTPS
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>
# END Force HTTPS

Dua hal soal penempatan. Taruh ini di atas penanda # BEGIN WordPress — apa pun yang ada di dalamnya dibuang setiap kali ada yang menyimpan permalink. Dan ini hanya berfungsi kalau mod_rewrite aktif dan virtual host-nya mengizinkan override (AllowOverride All, atau setidaknya FileInfo). Di nginx, .htaccess diabaikan sepenuhnya dan aturan ini harus ditaruh di server block.

RedirectMatch tidak bisa melakukan tugas ini. Dia hanya mencocokkan path request dan tidak punya cara untuk menguji apakah request saat ini sudah aman, jadi dia me-redirect request https kembali ke dirinya sendiri. RewriteRule bersyarat di atas adalah alat yang tepat.

Redirect loop, dan kenapa itu terjadi

Kalau situsnya mulai me-redirect tanpa henti begitu kamu menambahkan aturan itu, berarti TLS kamu diterminasi di suatu tempat di hulu — load balancer, reverse proxy, atau CDN — yang lalu meneruskan http polos ke Apache. Apache melihat request yang tidak aman, me-redirect ke https, proxy meneruskan http lagi, dan begitu seterusnya.

Uji header forwarded sebagai gantinya:

RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

WordPress sendiri punya titik buta yang sama di setup seperti itu — is_ssl() membaca $_SERVER['HTTPS'], yang tidak pernah di-set oleh proxy, jadi URL admin keluar sebagai http://. Tambahkan ini ke wp-config.php, di atas baris “stop editing”:

if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
	$_SERVER['HTTPS'] = 'on';
}

Perlu jujur soal risikonya: X-Forwarded-Proto adalah header yang dikirim klien. Mempercayainya baru benar kalau ada proxy yang kamu kendalikan dan selalu menimpanya. Di server yang bisa dijangkau langsung dari internet, header itu bisa dipalsukan.

Yang tidak akan diperbaiki oleh replace database

  • URL yang di-hardcode di file. Apa pun yang ditulis ke dalam functions.php, template child theme, atau path aset inline tidak ada di database. Grep direktori tema secara terpisah.
  • JSON yang di-escape. Sebagian builder menyimpan URL sebagai https:\/\/example.com. Pencarian dengan bentuk normal sama sekali melewatkannya, jadi mungkin perlu satu pass tambahan untuk string yang di-escape.
  • Resource eksternal. Skrip atau embed pihak ketiga yang hanya tersedia lewat http tidak bisa diperbaiki dari sisimu. Cari versi https-nya atau buang saja.
  • Cache. Page cache, object cache, dan salinan CDN akan terus menyajikan markup lama setelah replace yang benar sekalipun. Flush ketiganya sebelum menyimpulkan bahwa replace-nya gagal.

Satu hal yang sebaiknya dilewati: header Content Security Policy upgrade-insecure-requests akan membungkam peringatannya dengan menulis ulang request tidak aman di sisi browser. Itu mengobati gejalanya dan meninggalkan URL yang salah di database, di mana ekspor, migrasi, atau feed berikutnya akan membawanya terus. Perbaiki datanya dulu, baru pakai ini sebagai jaring pengaman kalau memang kamu mau.

Masih buntu?

Cek ulang siteurl dan home setelah setiap langkah — plugin dan alat migrasi kadang menulis ulang keduanya di belakangmu. Kalau konsolnya masih menyebut URL tidak aman yang tidak bisa kamu temukan di dump, lihat source halamannya lalu cari di situ: kalau muncul di markup yang dihasilkan tapi tidak ada di database, berarti dia dibangun di php, dan tema atau plugin yang memproduksinya itulah yang harus diperbaiki.

FAQ

Pertanyaan

Kenapa situs WordPress saya masih menampilkan peringatan mixed content setelah memasang sertifikat SSL?

Sertifikat hanya mengubah cara koneksinya dienkripsi, bukan apa yang diminta oleh halamanmu. URL http:// lama tetap tersimpan di database, di dalam konten postingan, opsi widget, dan pengaturan tema. Browser memuat halaman lewat https, melihat ada gambar atau skrip yang diminta lewat http polos, lalu melaporkan mixed content pada halaman yang sebenarnya sudah aman.

Bagaimana cara menemukan URL http:// yang masih tersisa di database WordPress?

Buka satu halaman di browser lalu baca konsol developer, yang menyebutkan setiap request tidak aman beserta URL-nya. Setelah itu ekspor database dan cari domainmu dengan prefix http di dalam dump-nya. Menghitung jumlah kecocokan per tabel memberitahumu apakah masalahnya ada di konten postingan, di wp_options, atau di tabel plugin yang tidak kamu duga.

Bisakah mixed content diperbaiki dengan search and replace sql biasa?

Hanya untuk nilai skalar seperti siteurl dan home. Apa pun yang disimpan WordPress sebagai array terserialisasi, yang mencakup sebagian besar nilai opsi dan meta, mencatat panjang byte dari setiap string di dalamnya. Replace mentah mengubah teksnya tapi meninggalkan panjang lamanya, nilainya lalu gagal di-unserialize, dan pengaturannya diam-diam kembali ke nilai bawaan.

Aturan .htaccess apa yang memaksa https di WordPress?

Sebuah RewriteRule yang dijaga oleh kondisi pada variabel HTTPS, ditaruh di atas penanda BEGIN WordPress supaya menyimpan permalink tidak menghapusnya. Di belakang proxy atau CDN, variabel HTTPS terbaca off bahkan pada request yang aman, jadi uji header X-Forwarded-Proto sebagai gantinya atau aturannya akan me-redirect selamanya.

Kenapa situs saya masuk ke redirect loop setelah memaksa https?

Hampir bisa dipastikan TLS kamu diterminasi di proxy atau CDN yang kemudian meneruskan http polos ke Apache. Rewrite melihat request tidak aman, me-redirect ke https, proxy meneruskan http lagi, dan loop-nya berulang. Ganti kondisinya ke header X-Forwarded-Proto lalu set variabel server HTTPS di wp-config.php.

Apakah mixed content merusak seluruh halaman atau hanya ikon gemboknya?

Tergantung jenis resource-nya. Browser memblokir active mixed content seperti skrip, stylesheet, dan iframe secara total, yang bisa merusak tata letak atau fungsionalitas tanpa penjelasan apa pun. Passive mixed content seperti gambar dan video biasanya masih dimuat, tapi ikon gemboknya diturunkan dan pengunjung bisa melihat indikator not secure. Keduanya layak diperbaiki.