ข้ามไปยังเนื้อหา
เซิร์ฟเวอร์และ .htaccess

WordPress ขึ้น mixed content หลังย้ายมาใช้ https

เคลียร์คำเตือน mixed content ของ WordPress หลังย้ายมา https: ค้นหา URL แบบ http:// ที่ตกค้างในฐานข้อมูล แทนที่อย่างปลอดภัย และบังคับ https ใน .htaccess

เผยแพร่แล้ว

ใบรับรองติดตั้งเรียบร้อย เว็บโหลดผ่าน https แล้ว แต่แม่กุญแจก็ยังไม่ยอมโผล่มาสักที นั่นคือ mixed content: ตัวหน้าเว็บมาทาง https ก็จริง แต่บางอย่างข้างในหน้านั้น ไม่ว่าจะเป็นรูปภาพ สไตล์ชีต หรือสคริปต์ ยังถูกร้องขอผ่าน http:// ธรรมดาอยู่ บทความนี้จะพาไล่ตั้งแต่การค้นหา URL เหล่านั้น การแทนที่โดยไม่ทำให้ข้อมูล serialized เสียหาย การแก้ siteurl กับ home ไปจนถึงการบังคับ https ที่ระดับเซิร์ฟเวอร์เพื่อไม่ให้ปัญหากลับมาอีก

mixed content คืออะไรกันแน่

การติดตั้งใบรับรองเปลี่ยนวิธีเข้ารหัสการเชื่อมต่อ แต่ไม่ได้เปลี่ยนสิ่งที่หน้าเว็บของคุณ ร้องขอ ทุก http://yoursite.com/wp-content/uploads/logo.png ที่ถูกเขียนลงในโพสต์ วิดเจ็ต หรือการตั้งค่าธีมก่อนการย้าย ยังคงนอนอยู่ในฐานข้อมูล และเบราว์เซอร์ก็ร้องขอมันผ่านการเชื่อมต่อที่ไม่ปลอดภัยอย่างว่าง่าย

เบราว์เซอร์ปฏิบัติกับสองประเภทนี้ต่างกัน และความต่างนี้สำคัญตอนที่คุณกำลังชั่งใจว่าเรื่องนี้เร่งด่วนแค่ไหน:

  • mixed content แบบแอ็กทีฟ — สคริปต์ สไตล์ชีต iframe และ XHR ถูกบล็อกทิ้งไปเลย นี่คือเหตุผลที่เว็บอาจดูพังหลังย้ายมา https โดยไม่มีข้อความแจ้งข้อผิดพลาดที่ไหนเลย เพราะสไตล์ชีตถูกปฏิเสธไปอย่างเงียบ ๆ
  • mixed content แบบพาสซีฟ — รูปภาพ เสียง วิดีโอ ปกติยังโหลดได้อยู่ แต่แม่กุญแจถูกลดระดับ และบางเบราว์เซอร์แสดงสัญลักษณ์ว่าไม่ปลอดภัย

ทั้งสองแบบคุ้มค่าที่จะแก้ แต่มีเพียงแบบแรกที่ทำให้ของพัง

ขั้นที่ 1: หาให้เจอว่าอะไรยังไม่ปลอดภัย

เริ่มที่เบราว์เซอร์ เปิดหน้าเว็บ เปิดเครื่องมือนักพัฒนา แล้วอ่านคอนโซล ทุกคำขอที่ถูกบล็อกหรือถูกลดระดับจะถูกระบุไว้ที่นั่นพร้อม URL เต็ม สิ่งนี้บอกว่า อะไร ไม่ปลอดภัย แต่ไม่ได้บอกว่ามันถูกเก็บไว้ที่ไหน

สำหรับเรื่องนั้นต้องดูที่ฐานข้อมูลโดยตรง ส่งออกมันแล้วค้นหาในไฟล์ดัมป์:

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

ถ้าใช้ WP-CLI ไม่ได้ mysqldump ก็สร้างไฟล์เดียวกันนี้ได้:

mysqldump -u USER -p DBNAME > dump.sql

อ่าน URL ที่ไม่ซ้ำกันซึ่งได้กลับมา พาธที่เป็น uploads ชี้ไปที่เนื้อหาโพสต์และ meta ส่วนพาธของไฟล์ธีมมักชี้ไปที่ options ส่วนอะไรก็ตามที่เป็นโดเมนซึ่งคุณจำไม่ได้ นั่นคือ embed จากภายนอก ซึ่งต้องแก้คนละแบบ (กล่าวถึงด้านล่าง)

ขั้นที่ 2: แก้ siteurl กับ home ก่อน

siteurl และ home คือสองตัวเลือกที่ WordPress ใช้ประกอบเป็น URL ภายในแทบทุกตัวที่มันสร้างขึ้น ถ้าตัวใดตัวหนึ่งยังเป็น http:// อยู่ WordPress ก็จะยังปล่อย URL ที่ไม่ปลอดภัยออกมาต่อไป ไม่ว่าฐานข้อมูลส่วนที่เหลือจะสะอาดแค่ไหน

ตรวจสอบด้วย:

wp option get siteurl
wp option get home

หรือใน sql:

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

สองค่านี้เป็นสตริงธรรมดา ไม่ใช่อาร์เรย์แบบ serialized การอัปเดตด้วย sql ตรง ๆ ตรงนี้จึงปลอดภัยจริง:

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

มีกับดักหนึ่งอย่างที่ควรรู้ก่อนลงแรงกับเรื่องนี้ ถ้า wp-config.php กำหนด WP_HOME หรือ WP_SITEURL ไว้ ค่าคงที่เหล่านั้นจะทับค่าในฐานข้อมูลทั้งหมด และการอัปเดตของคุณจะดูเหมือนไม่เกิดอะไรขึ้นเลย:

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

ให้อัปเดตที่นั่น หรือลบออกแล้วปล่อยให้ฐานข้อมูลเป็นตัวกำหนด ตรวจเรื่องนี้ก่อนเป็นอย่างแรก มันอธิบายเคสประเภทเปลี่ยนแล้วไม่เห็นมีอะไรเกิดขึ้นได้เยอะทีเดียว

ขั้นที่ 3: การแทนที่ต้องรองรับ serialization

ทุกอย่างที่เหลือในฐานข้อมูลคือจุดที่ความเสี่ยงจริงซ่อนอยู่ และมันไม่ใช่ความเสี่ยงแบบที่คนส่วนใหญ่คาดคิด อันตรายไม่ใช่การที่การแทนที่พลาด URL ไปบางตัว แต่คือการที่มันสำเร็จในระดับข้อความแล้วทำลายข้อมูลรอบ ๆ ไปด้วย

WordPress เก็บอาร์เรย์และอ็อบเจกต์เป็นสตริงที่ผ่านการ serialize ของ php และรูปแบบนั้นบันทึกความยาวเป็นไบต์ของทุกสตริงที่มันบรรจุไว้:

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

ถ้าเปลี่ยน http:// เป็น https:// ด้วยคำสั่ง REPLACE แบบ sql ดิบ ข้อความจะยาวขึ้นหนึ่งไบต์ ขณะที่ความยาวที่ประกาศไว้ยังเป็นค่าเดิม PHP อ่านความยาวนั้น เดินหน้าไปตามจำนวนไบต์ที่ระบุ แล้วไม่พบตัวปิดท้ายตรงจุดที่คาดไว้ จึงปฏิเสธที่จะ unserialize อาร์เรย์ทั้งก้อน จากนั้น WordPress ก็ยื่นค่าที่ทำตัวราวกับว่าไม่เคยมีการบันทึกการตั้งค่านั้นเลยให้ธีมหรือปลั๊กอิน

อาการที่เห็นไม่ใช่ข้อความแจ้งข้อผิดพลาด แต่คือวิดเจ็ตหายไป การตั้งค่าใน customizer รีเซ็ต และเลย์เอาต์ของ page builder แสดงผลเป็นหน้าว่าง โดยไม่มีอะไรในหน้าแอดมินบ่งบอกว่ามีอะไรผิดพลาด การกู้คืนจากสภาพนั้นโดยไม่มีข้อมูลสำรองเจ็บปวดจริง ๆ ฉะนั้นสำรองไว้ก่อน:

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

จากนั้นใช้เครื่องมือที่ unserialize ก่อน แล้วแทนที่ภายในโครงสร้างที่ถอดรหัสแล้ว และ serialize กลับพร้อมแก้ความยาวให้ถูกต้อง ลองรันแบบ dry run ก่อน:

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

อ่านรายงานให้ดี ถ้าตารางที่คุณจำไม่ได้แสดงผลลัพธ์เป็นหลักพัน ให้หยุดแล้วตรวจสอบก่อนลงมือจริง จากนั้นค่อยรันของจริง:

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

--skip-columns=guid สำคัญมาก เพราะ guid เป็นตัวระบุถาวรสำหรับโปรแกรมอ่านฟีด ไม่ใช่ URL ที่ใช้งานจริง การเขียนทับมันอาจทำให้ผู้ติดตามเห็นคลังบทความทั้งหมดของคุณกลายเป็นโพสต์ใหม่ทั้งหมด หากต้องการดูก่อนลงมือว่าคอลัมน์ไหนปลอดภัยสำหรับ sql ธรรมดา และคอลัมน์ไหนต้องจัดการแบบรู้เรื่อง serialization ให้สร้างคำสั่งด้วย เครื่องมือ search and replace sql สำหรับ WordPress

ถ้าใช้บรรทัดคำสั่งไม่ได้ ปลั๊กอินย้ายเว็บที่ระบุชัดเจนว่ารองรับข้อมูลแบบ serialized ก็ทำงานถอดรหัสแบบเดียวกันนี้ให้ผ่านหน้าแอดมินได้ ถ้าเครื่องมือไหนไม่ได้ระบุไว้ ให้ถือว่ามันทำไม่ได้

ขั้นที่ 4: บังคับ https ที่ระดับเซิร์ฟเวอร์

การล้างฐานข้อมูลหยุดไม่ให้หน้าเว็บของคุณ ร้องขอ ทรัพยากรที่ไม่ปลอดภัย แต่ไม่ได้หยุดผู้เข้าชมจากการเข้ามาทาง http:// ตั้งแต่แรก นั่นคือการ redirect ที่ระดับเซิร์ฟเวอร์ และบน Apache มันควรอยู่ใน .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

มีสองเรื่องเกี่ยวกับตำแหน่ง หนึ่งคือวางไว้ เหนือ มาร์กเกอร์ # BEGIN WordPress เพราะอะไรก็ตามที่อยู่ข้างในนั้นจะถูกทิ้งทุกครั้งที่มีคนบันทึก permalinks และสองคือมันจะทำงานได้ก็ต่อเมื่อเปิดใช้ mod_rewrite และ virtual host อนุญาตให้ override ได้ (AllowOverride All หรืออย่างน้อย FileInfo) ส่วนบน nginx นั้น .htaccess ถูกเมินทั้งไฟล์ และต้องย้ายไปใส่ใน server block แทน

RedirectMatch ทำงานนี้ไม่ได้ เพราะมันจับคู่เฉพาะพาธของคำขอ และไม่มีทางตรวจสอบว่าคำขอปัจจุบันปลอดภัยอยู่แล้วหรือไม่ มันจึง redirect คำขอ https กลับมาที่ตัวมันเอง RewriteRule แบบมีเงื่อนไขข้างบนคือเครื่องมือที่ถูกต้อง

redirect วนซ้ำ และสาเหตุที่มันเกิดขึ้น

ถ้าเว็บเริ่ม redirect วนไม่รู้จบทันทีที่คุณเพิ่มกฎนั้น แปลว่า TLS ของคุณสิ้นสุดอยู่ที่ไหนสักแห่งเหนือขึ้นไป ไม่ว่าจะเป็นโหลดบาลานเซอร์ รีเวิร์สพร็อกซี หรือ CDN ซึ่งจากนั้นก็ส่งต่อ http ธรรมดามายัง Apache ตัว Apache เห็นคำขอที่ไม่ปลอดภัย จึง redirect ไป https พร็อกซีก็ส่งต่อเป็น http อีก แล้ววนอยู่อย่างนั้น

ให้เปลี่ยนไปตรวจสอบเฮดเดอร์ที่ถูกส่งต่อมาแทน:

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

ในสถานการณ์แบบนั้น ตัว WordPress เองก็มีจุดบอดเดียวกัน เพราะ is_ssl() อ่านค่า $_SERVER['HTTPS'] ซึ่งพร็อกซีไม่เคยตั้งค่าให้ URL ของหน้าแอดมินจึงออกมาเป็น http:// ให้เพิ่มโค้ดนี้ลงใน wp-config.php เหนือบรรทัดที่บอกให้หยุดแก้ไข:

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

ขอพูดถึงความเสี่ยงกันตรง ๆ X-Forwarded-Proto เป็นเฮดเดอร์ที่ไคลเอนต์ส่งมาเอง การเชื่อถือมันจะถูกต้องก็ต่อเมื่อมีพร็อกซีที่คุณควบคุมเขียนทับค่านั้นเสมอ บนเซิร์ฟเวอร์ที่เข้าถึงได้โดยตรงจากอินเทอร์เน็ต ค่านี้ถูกปลอมได้

สิ่งที่การแทนที่ในฐานข้อมูลแก้ไม่ได้

  • URL ที่ฝังไว้ในไฟล์ อะไรก็ตามที่ถูกเขียนไว้ใน functions.php ไฟล์เทมเพลตของ child theme หรือพาธของไฟล์ที่ใส่ไว้ในบรรทัดโค้ด ไม่ได้อยู่ในฐานข้อมูล ต้อง grep ในไดเรกทอรีธีมแยกต่างหาก
  • JSON ที่ escape ไว้ บิลเดอร์บางตัวเก็บ URL เป็น https:\/\/example.com การค้นหาด้วยรูปแบบปกติจะพลาดมันไปทั้งหมด จึงอาจต้องค้นหารอบที่สองด้วยสตริงแบบ escape
  • ทรัพยากรภายนอก สคริปต์หรือ embed จากบุคคลที่สามซึ่งมีให้ใช้เฉพาะบน http เท่านั้น แก้จากฝั่งคุณไม่ได้ ให้หาเวอร์ชันที่เป็น https หรือเลิกใช้มันไป
  • แคช แคชหน้าเว็บ แคชอ็อบเจกต์ และสำเนาบน CDN จะยังเสิร์ฟ markup เดิมต่อไปแม้แทนที่ถูกต้องแล้ว ให้ล้างทั้งสามอย่างก่อนจะสรุปว่าการแทนที่ล้มเหลว

มีอย่างหนึ่งที่ควรข้ามไป คือเฮดเดอร์ Content Security Policy ตัว upgrade-insecure-requests ซึ่งจะทำให้คำเตือนเงียบลงด้วยการเขียนคำขอที่ไม่ปลอดภัยใหม่ในเบราว์เซอร์ มันแก้ที่อาการและปล่อยให้ URL ที่ผิดยังอยู่ในฐานข้อมูลของคุณ ซึ่งการส่งออก การย้ายเว็บ หรือฟีดครั้งถัดไปจะพามันติดไปด้วย ให้แก้ที่ข้อมูลก่อน แล้วค่อยใช้มันเป็นตาข่ายนิรภัยถ้าคุณต้องการ

ยังติดอยู่ใช่ไหม

ตรวจ siteurl กับ home ซ้ำหลังทำทุกขั้นตอน เพราะปลั๊กอินและเครื่องมือย้ายเว็บบางครั้งเขียนทับค่าเหล่านั้นลับหลังคุณ ถ้าคอนโซลยังระบุ URL ที่ไม่ปลอดภัยซึ่งคุณหาไม่เจอในไฟล์ดัมป์ ให้ดูซอร์สโค้ดของหน้านั้นแล้วค้นหาที่นั่น ถ้ามันปรากฏใน markup ที่ถูกสร้างขึ้นแต่ไม่มีในฐานข้อมูล แปลว่ามันถูกประกอบขึ้นใน php และธีมหรือปลั๊กอินที่ผลิตมันออกมาคือสิ่งที่ต้องแก้

FAQ

คำถาม

ทำไมเว็บ WordPress ของผมยังขึ้นคำเตือน mixed content ทั้งที่ติดตั้งใบรับรอง SSL แล้ว

ใบรับรองเปลี่ยนแค่วิธีเข้ารหัสการเชื่อมต่อ ไม่ได้เปลี่ยนสิ่งที่หน้าเว็บของคุณร้องขอ URL แบบ http:// เดิมยังถูกเก็บอยู่ในฐานข้อมูล ทั้งในเนื้อหาโพสต์ ตัวเลือกของวิดเจ็ต และการตั้งค่าธีม เบราว์เซอร์โหลดหน้าผ่าน https แล้วเห็นว่ามีรูปภาพหรือสคริปต์ถูกร้องขอผ่าน http ธรรมดา จึงรายงานว่าเป็น mixed content บนหน้าที่ปลอดภัยอยู่แล้ว

จะค้นหา URL แบบ http:// ที่ยังตกค้างในฐานข้อมูล WordPress ได้อย่างไร

เปิดหน้าเว็บในเบราว์เซอร์แล้วอ่านคอนโซลของเครื่องมือนักพัฒนา ซึ่งจะระบุทุกคำขอที่ไม่ปลอดภัยพร้อม URL จากนั้นส่งออกฐานข้อมูลแล้วค้นหาโดเมนของคุณที่ขึ้นต้นด้วย http ในไฟล์ดัมป์ การนับจำนวนที่พบแยกตามตารางจะบอกได้ว่าปัญหาอยู่ในเนื้อหาโพสต์ อยู่ใน wp_options หรืออยู่ในตารางของปลั๊กอินที่คุณคาดไม่ถึง

แก้ mixed content ด้วย search and replace แบบ sql ธรรมดาได้ไหม

ได้เฉพาะค่าเดี่ยว ๆ อย่าง siteurl และ home เท่านั้น สิ่งที่ WordPress เก็บเป็นอาร์เรย์แบบ serialized ซึ่งครอบคลุมค่า option และ meta ส่วนใหญ่ จะบันทึกความยาวเป็นไบต์ของทุกสตริงข้างในไว้ด้วย การแทนที่แบบดิบเปลี่ยนแค่ข้อความแต่ทิ้งความยาวเดิมไว้ ค่านั้นจึง unserialize ไม่ผ่าน และการตั้งค่าก็ย้อนกลับไปเป็นค่าเริ่มต้นอย่างเงียบ ๆ

กฎ .htaccess แบบไหนที่บังคับ https ใน WordPress

RewriteRule ที่คุมด้วยเงื่อนไขบนตัวแปร https และวางไว้เหนือมาร์กเกอร์ BEGIN WordPress เพื่อไม่ให้การบันทึก permalinks ลบมันทิ้ง หากอยู่หลังพร็อกซีหรือ CDN ตัวแปร https จะอ่านค่าเป็น off แม้ในคำขอที่ปลอดภัย จึงต้องเปลี่ยนไปตรวจสอบเฮดเดอร์ X-Forwarded-Proto แทน มิฉะนั้นกฎจะ redirect วนไม่รู้จบ

ทำไมเว็บของผมถึงเข้าสู่ redirect วนซ้ำหลังบังคับ https

แทบจะแน่นอนว่า TLS ของคุณสิ้นสุดที่พร็อกซีหรือ CDN ซึ่งส่งต่อ http ธรรมดามายัง Apache ตัว rewrite จึงเห็นคำขอที่ไม่ปลอดภัยแล้ว redirect ไป https พร็อกซีก็ส่งต่อเป็น http อีก แล้ววนซ้ำอย่างนี้เรื่อยไป ให้เปลี่ยนเงื่อนไขไปใช้เฮดเดอร์ X-Forwarded-Proto และตั้งค่าตัวแปรเซิร์ฟเวอร์ https ใน wp-config.php

mixed content ทำให้ทั้งหน้าพัง หรือแค่ทำให้แม่กุญแจหาย

ขึ้นอยู่กับทรัพยากรนั้น เบราว์เซอร์บล็อก mixed content แบบแอ็กทีฟ เช่น สคริปต์ สไตล์ชีต และ iframe ทิ้งไปเลย ซึ่งทำให้เลย์เอาต์หรือการทำงานพังโดยไม่มีคำอธิบายให้เห็น ส่วน mixed content แบบพาสซีฟ เช่น รูปภาพและวิดีโอ มักยังโหลดได้อยู่ แต่แม่กุญแจจะถูกลดระดับและผู้เข้าชมอาจเห็นสัญลักษณ์ว่าไม่ปลอดภัย ทั้งสองแบบคุ้มค่าที่จะแก้