Bỏ qua tới nội dung
Máy chủ & .htaccess

Lỗi mixed content trong WordPress sau khi chuyển sang https

Dọn sạch cảnh báo mixed content của WordPress sau khi lên https: tìm các URL http:// còn sót trong database, thay thế an toàn, và ép https ngay trong .htaccess.

Đã đăng

Chứng chỉ đã cài xong, site đã tải qua https, mà biểu tượng ổ khóa vẫn nhất định không chịu xuất hiện. Đó chính là mixed content: bản thân trang đến qua https, nhưng một thứ gì đó bên trong nó — một ảnh, một stylesheet, một script — vẫn đang được yêu cầu qua http:// thuần. Bài này đi qua các bước tìm những URL đó, thay thế chúng mà không làm hỏng dữ liệu serialized, chỉnh lại siteurlhome, rồi ép https ngay tại server để vấn đề không quay lại được nữa.

Mixed content thực chất là gì

Cài một chứng chỉ sẽ thay đổi cách kết nối được mã hóa. Nó không thay đổi những gì trang của bạn yêu cầu tải về. Mỗi http://yoursite.com/wp-content/uploads/logo.png từng được ghi vào một bài viết, một widget hay một thiết lập theme trước khi chuyển đổi thì vẫn đang nằm y nguyên trong database, và trình duyệt ngoan ngoãn yêu cầu nó qua một kết nối không an toàn.

Trình duyệt đối xử với hai nhóm khác nhau, và sự phân biệt này quan trọng khi bạn cân nhắc mức độ cấp bách:

  • Mixed content chủ động — script, stylesheet, iframe, XHR. Bị chặn thẳng. Đây là lý do một site có thể trông như vỡ nát sau khi lên https mà chẳng có thông báo lỗi ở đâu cả: một stylesheet đã bị từ chối trong im lặng.
  • Mixed content bị động — ảnh, âm thanh, video. Thường vẫn tải được, nhưng ổ khóa bị hạ cấp và một số trình duyệt hiện chỉ báo “không an toàn”.

Cả hai đều đáng sửa. Chỉ có loại thứ nhất là làm hỏng mọi thứ.

Bước 1: tìm xem còn gì đang không an toàn

Bắt đầu từ trình duyệt. Mở trang, mở developer tools, và đọc console. Mọi request bị chặn hoặc bị hạ cấp đều được nêu tên ở đó kèm URL đầy đủ. Nó cho bạn biết cái gì không an toàn; nó không cho bạn biết cái đó được lưu ở đâu.

Muốn biết chỗ lưu thì phải nhìn thẳng vào database. Export nó ra rồi tìm trong file dump:

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

Nếu không có WP-CLI, mysqldump cũng tạo ra đúng file đó:

mysqldump -u USER -p DBNAME > dump.sql

Hãy đọc danh sách URL duy nhất trả về. Các đường dẫn uploads trỏ về nội dung bài viết và meta. Các đường dẫn tài nguyên theme thường trỏ về options. Bất cứ tên miền nào bạn không nhận ra đều là một embed từ bên ngoài, và nó cần cách xử lý khác (nói ở phần dưới).

Bước 2: sửa siteurl và home trước đã

siteurlhome là hai option mà WordPress dùng để dựng gần như mọi URL nội bộ nó sinh ra. Nếu một trong hai vẫn còn ghi http://, WordPress sẽ tiếp tục phát ra URL không an toàn bất kể phần còn lại của database sạch đến đâu.

Kiểm tra chúng:

wp option get siteurl
wp option get home

Hoặc bằng sql:

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

Hai giá trị này là chuỗi thuần, không phải mảng serialized, nên ở đây một lệnh update sql trực tiếp thực sự an toàn:

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

Một cái bẫy trước khi bạn tốn thời gian cho việc này. Nếu wp-config.php có định nghĩa WP_HOME hoặc WP_SITEURL, các hằng số đó ghi đè hoàn toàn lên database và lệnh update của bạn sẽ trông như chẳng có tác dụng gì:

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

Hãy sửa chúng ngay tại đó, hoặc bỏ chúng đi và để database quyết định. Kiểm tra chỗ này trước tiên — nó giải thích được rất nhiều trường hợp kiểu “tôi đổi rồi mà chẳng thấy gì thay đổi”.

Bước 3: lệnh thay thế phải an toàn với dữ liệu serialized

Mọi thứ còn lại trong database mới là nơi rủi ro thật sự nằm, và nó không phải là loại rủi ro mà đa số người ta hình dung. Nguy hiểm không nằm ở chuyện lệnh thay thế bỏ sót URL. Nguy hiểm nằm ở chỗ nó thành công ở mức văn bản và phá hủy dữ liệu xung quanh.

WordPress lưu mảng và object dưới dạng chuỗi php-serialized, và định dạng đó ghi kèm độ dài byte của từng chuỗi mà nó chứa:

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

Đổi http:// thành https:// bằng một lệnh REPLACE sql thô thì phần văn bản dài thêm một byte trong khi độ dài được khai báo vẫn giữ nguyên giá trị cũ. php đọc độ dài đó, đi tới đúng số byte ấy, không tìm thấy ký tự kết thúc ở chỗ nó mong đợi, và từ chối unserialize cả mảng. WordPress sau đó trao cho theme hoặc plugin một giá trị hành xử y như thể thiết lập kia chưa từng được lưu.

Triệu chứng không phải là một thông báo lỗi. Triệu chứng là widget biến mất, thiết lập customizer trở về mặc định, và bố cục dựng bằng page builder hiển thị trắng trơn — mà trong trang quản trị không có gì báo hiệu rằng có chuyện đã xảy ra. Khôi phục từ tình huống đó mà không có bản sao lưu thì thật sự khổ sở, nên hãy sao lưu:

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

Rồi dùng một công cụ biết unserialize, thay thế bên trong cấu trúc đã giải mã, và serialize lại với độ dài đã chỉnh đúng. Chạy thử trước:

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

Đọc báo cáo. Nếu một bảng bạn không nhận ra lại hiện ra hàng nghìn lượt khớp, hãy dừng lại và xem cho kỹ trước khi làm thật. Sau đó chạy thật:

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

--skip-columns=guid là quan trọng: guid là một định danh vĩnh viễn dành cho trình đọc feed, không phải một URL sống, và viết lại nó có thể khiến người theo dõi thấy toàn bộ kho lưu trữ của bạn như là bài mới. Nếu bạn cần biết cột nào an toàn với sql thuần và cột nào đòi hỏi xử lý hiểu được dữ liệu serialized trước khi chạy bất cứ thứ gì, hãy dựng câu lệnh bằng công cụ tạo sql tìm và thay thế cho WordPress.

Nếu bạn không dùng được dòng lệnh, một plugin migration có ghi rõ là nó xử lý được dữ liệu serialized sẽ làm đúng phần việc giải mã đó ngay trong màn hình quản trị. Nếu một công cụ không nói rõ điều này, cứ mặc định là nó không làm được.

Bước 4: ép https ngay tại server

Dọn sạch database sẽ ngăn các trang của bạn yêu cầu tài nguyên không an toàn. Nó không ngăn được một khách truy cập vào bằng http:// ngay từ đầu. Đó là việc của một chuyển hướng ở tầng server, và trên Apache thì nó thuộc về .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

Hai điều về vị trí đặt. Hãy đặt nó phía trên các marker # BEGIN WordPress — mọi thứ nằm bên trong chúng đều bị bỏ đi mỗi lần có ai đó lưu permalink. Và nó chỉ chạy được nếu mod_rewrite đã bật và virtual host cho phép override (AllowOverride All, hoặc ít nhất là FileInfo). Trên nginx, .htaccess bị phớt lờ hoàn toàn và phần này phải chuyển vào một server block.

Một RedirectMatch không làm được việc này. Nó chỉ khớp trên đường dẫn request và không có cách nào kiểm tra xem request hiện tại đã an toàn hay chưa, nên nó sẽ chuyển hướng các request https về lại chính chúng. RewriteRule có điều kiện ở trên mới là công cụ đúng.

Vòng lặp chuyển hướng, và vì sao nó xảy ra

Nếu site bắt đầu chuyển hướng vô tận ngay khoảnh khắc bạn thêm rule đó, thì TLS của bạn đang kết thúc ở đâu đó phía trên — một load balancer, một reverse proxy, hoặc một CDN — và nơi đó chuyển tiếp http thuần về Apache. Apache thấy một request không an toàn, chuyển hướng sang https, proxy lại chuyển tiếp http lần nữa, và cứ thế quay vòng.

Hãy kiểm tra header được chuyển tiếp thay vì kiểm tra biến kia:

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

Bản thân WordPress cũng có đúng điểm mù đó trong thiết lập này — is_ssl() đọc $_SERVER['HTTPS'], thứ mà proxy không bao giờ set, nên các URL quản trị bị sinh ra dưới dạng http://. Hãy thêm đoạn này vào wp-config.php, phía trên dòng “stop editing”:

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

Cũng nên nói thẳng về rủi ro: X-Forwarded-Proto là một header do phía client gửi lên. Tin vào nó chỉ đúng khi có một proxy do bạn kiểm soát luôn ghi đè lên nó. Trên một server có thể truy cập trực tiếp từ internet, header đó có thể bị giả mạo.

Những thứ mà thay thế trong database sẽ không sửa được

  • URL viết cứng trong file. Bất cứ thứ gì được viết vào functions.php, một template của child theme, hay một đường dẫn tài nguyên nội tuyến đều không nằm trong database. Hãy grep riêng thư mục theme.
  • JSON có escape. Một số page builder lưu URL dưới dạng https:\/\/example.com. Tìm theo dạng thông thường sẽ bỏ sót chúng hoàn toàn, nên có thể cần thêm một lượt chạy nữa cho chuỗi đã escape.
  • Tài nguyên bên ngoài. Một script hay embed của bên thứ ba chỉ có sẵn qua http thì bạn không sửa được từ phía mình. Hãy tìm bản chạy được qua https, hoặc bỏ nó đi.
  • Cache. Cache trang, cache object và bản sao trên CDN vẫn tiếp tục phục vụ markup cũ sau một lần thay thế đã đúng. Hãy xả sạch cả ba trước khi kết luận rằng lệnh thay thế thất bại.

Có một thứ đáng bỏ qua: header Content Security Policy upgrade-insecure-requests sẽ dập tắt các cảnh báo bằng cách viết lại request không an toàn ngay trong trình duyệt. Nó chữa triệu chứng và để nguyên các URL sai trong database, nơi mà lần export, lần migration hay cái feed kế tiếp sẽ mang chúng đi tiếp. Hãy sửa dữ liệu trước, rồi dùng nó như một tấm lưới an toàn nếu bạn muốn.

Vẫn bí?

Hãy kiểm tra lại siteurlhome sau mỗi bước — plugin và công cụ migration đôi khi ghi đè chúng sau lưng bạn. Nếu console vẫn nêu tên một URL không an toàn mà bạn không tìm thấy trong file dump, hãy xem mã nguồn trang và tìm nó ở đó: nếu nó xuất hiện trong markup được sinh ra mà không có trong database, thì nó đang được dựng ra bằng php, và cái theme hoặc plugin tạo ra nó mới là thứ cần sửa.

FAQ

Câu hỏi

Vì sao site WordPress của tôi vẫn báo mixed content dù đã cài chứng chỉ SSL?

Chứng chỉ chỉ thay đổi cách kết nối được mã hóa, chứ không thay đổi những gì trang của bạn yêu cầu tải về. Các URL http:// cũ vẫn nằm nguyên trong database, bên trong nội dung bài viết, tùy chọn widget và thiết lập theme. Trình duyệt tải trang qua https, thấy một ảnh hoặc một script đang được yêu cầu qua http thuần, và báo mixed content trên một trang lẽ ra đã an toàn.

Làm sao tìm những URL http:// còn sót lại trong database WordPress?

Mở một trang trên trình duyệt rồi đọc console của developer tools, nơi liệt kê từng request không an toàn kèm URL. Sau đó export database ra và tìm tên miền của bạn kèm tiền tố http trong file dump. Đếm số lần xuất hiện theo từng bảng sẽ cho bạn biết vấn đề nằm ở nội dung bài viết, ở wp_options, hay ở những bảng plugin mà bạn không ngờ tới.

Tôi có thể sửa mixed content bằng một lệnh tìm và thay thế sql thuần không?

Chỉ được với các giá trị đơn như siteurl và home. Bất cứ thứ gì WordPress lưu dưới dạng mảng serialized — vốn chiếm phần lớn các giá trị option và meta — đều ghi kèm độ dài byte của từng chuỗi bên trong. Một lệnh thay thế thô sẽ đổi phần văn bản nhưng để nguyên độ dài cũ, giá trị đó không unserialize được nữa, và thiết lập âm thầm quay về mặc định.

Rule .htaccess nào ép được https trong WordPress?

Một RewriteRule có kèm điều kiện kiểm tra biến HTTPS, đặt phía trên các marker BEGIN WordPress để việc lưu permalink không xóa mất nó. Phía sau một proxy hoặc CDN, biến HTTPS lại đọc ra là off ngay cả trên request an toàn, nên hãy kiểm tra header X-Forwarded-Proto thay thế, nếu không rule sẽ chuyển hướng mãi không dứt.

Vì sao site của tôi rơi vào vòng lặp chuyển hướng sau khi ép https?

Gần như chắc chắn là TLS của bạn đang kết thúc ở một proxy hoặc CDN, rồi nơi đó chuyển tiếp http thuần về Apache. Rewrite thấy một request không an toàn nên chuyển hướng sang https, proxy lại chuyển tiếp http lần nữa, và vòng lặp cứ thế lặp lại. Hãy đổi điều kiện sang header X-Forwarded-Proto và set biến server HTTPS trong wp-config.php.

Mixed content làm hỏng cả trang hay chỉ mất biểu tượng ổ khóa?

Tùy vào loại tài nguyên. Trình duyệt chặn thẳng tay mixed content chủ động như script, stylesheet và iframe, và điều đó có thể làm vỡ bố cục hoặc hỏng chức năng mà không hề giải thích gì. Mixed content bị động như ảnh và video thì thường vẫn tải được, nhưng ổ khóa bị hạ cấp và khách có thể thấy chỉ báo không an toàn. Cả hai đều đáng sửa.