Chuyện gì đang xảy ra
Trang của bạn tải qua HTTPS, nhưng một hoặc nhiều tài nguyên — script, hình ảnh, stylesheet, iframe, hoặc API call — vẫn đang được tải qua HTTP thông thường. Trình duyệt coi đây là lỗ hổng bảo mật. Kẻ tấn công man-in-the-middle có thể thay thế tài nguyên HTTP đó bằng nội dung độc hại, dù trang chính của bạn đã được mã hóa.
Chrome 86+ tự động chặn toàn bộ mixed content, kể cả tài nguyên passive như hình ảnh. Các trình duyệt cũ chỉ chặn tài nguyên active (scripts, iframes) và chỉ cảnh báo với tài nguyên passive. Dù sao đi nữa, tài nguyên sẽ không tải được và bạn sẽ thấy thông báo này trong console:
Mixed Content: The page was loaded over HTTPS, but requested an insecure resource. This request has been blocked.
Với hình ảnh, cảnh báo nhẹ hơn trông như thế này:
Mixed Content: The page at 'https://example.com' was loaded over HTTPS, but contains a link to an insecure image 'http://example.com/photo.jpg'. This content should also be served over HTTPS.
Tìm tất cả tài nguyên vi phạm
Cách nhanh nhất để xác định vị trí: mở DevTools (F12), vào tab Console và lọc theo "Mixed Content". Mỗi URL bị chặn sẽ hiển thị tại đó kèm dòng code đã kích hoạt nó. Kiểm tra thêm tab Network — lọc cột URL request theo http://.
Để quét toàn bộ site mà không cần click từng trang, công cụ CLI mixed-content-scanner sẽ crawl toàn bộ site và báo cáo mọi URL vi phạm:
npx mixed-content-scanner https://example.com
Hoặc kéo HTML thô và grep trực tiếp các tham chiếu HTTP:
curl -s https://example.com | grep -Eo 'http://[^"\x27 >]+' | sort -u
Sửa nhanh — bật CSP header upgrade-insecure-requests
Thêm header Content-Security-Policy vào server. Header này yêu cầu trình duyệt tự động chuyển http:// thành https:// cho mọi sub-resource trước khi gửi request — không cần thay đổi code.
Nginx
add_header Content-Security-Policy "upgrade-insecure-requests;";
Apache (.htaccess)
Header always set Content-Security-Policy "upgrade-insecure-requests;"
HTML meta tag (khi không thể can thiệp vào server)
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
Đặt thẻ này bên trong <head> trước tất cả các thẻ tài nguyên khác. Các tài nguyên xuất hiện trước thẻ này trong HTML sẽ không được chuyển đổi.
Lưu ý quan trọng: upgrade-insecure-requests chỉ viết lại URL chứ không thể tạo ra một HTTPS endpoint từ không có gì. Nếu server từ xa không hỗ trợ HTTPS, request vẫn sẽ thất bại — chỉ là lỗi khác. Hãy sửa hẳn những tài nguyên đó bằng các phương pháp bên dưới.
Sửa triệt để — cập nhật tất cả tham chiếu HTTP trong code
1. URL hardcode trong HTML/templates
Chạy find-and-replace trên toàn bộ codebase. Cách này xử lý phần lớn trường hợp chỉ trong vài giây:
# Tìm tất cả tham chiếu http:// trong templates
grep -r 'http://' ./src --include='*.html' --include='*.jsx' --include='*.tsx' --include='*.vue'
# Thay thế http://example.com bằng https://example.com trong tất cả file
find ./src -type f \( -name '*.html' -o -name '*.jsx' -o -name '*.tsx' \) \
-exec sed -i 's|http://example.com|https://example.com|g' {} +
2. Protocol-relative URLs (cho embed từ bên thứ ba)
Bỏ scheme đi và để trình duyệt kế thừa protocol của trang hiện tại — HTTPS trên site thật, HTTP trên local dev:
<!-- Trước -->
<script src="http://cdn.example.com/lib.js"></script>
<!-- Sau -->
<script src="//cdn.example.com/lib.js"></script>
3. WordPress sites
WordPress lưu URL trong database, nên find-and-replace ở cấp file là chưa đủ. Chạy database search-replace — WP-CLI là cách an toàn nhất:
-- Dùng WP-CLI (xử lý đúng dữ liệu serialized)
wp search-replace 'http://example.com' 'https://example.com' --all-tables
-- Hoặc trực tiếp trong MySQL
UPDATE wp_posts SET post_content = REPLACE(post_content, 'http://example.com', 'https://example.com');
UPDATE wp_postmeta SET meta_value = REPLACE(meta_value, 'http://example.com', 'https://example.com');
UPDATE wp_options SET option_value = REPLACE(option_value, 'http://example.com', 'https://example.com');
Sau khi cập nhật database, cài plugin Really Simple SSL. Plugin này vá mixed content ở tầng PHP output buffer theo thời gian thực, bắt những gì database update bỏ sót — theme files, widgets, dynamic output.
4. Fetch / XHR trong JavaScript
Mọi lệnh gọi fetch() hoặc XMLHttpRequest với URL HTTP tuyệt đối đều bị chặn. Dùng URL tương đối khi có thể — ngắn hơn và luôn kế thừa đúng protocol:
// Sai
fetch('http://api.example.com/data')
// Đúng — URL tương đối (tự động kế thừa protocol)
fetch('/api/data')
// Đúng — HTTPS tường minh cho external APIs
fetch('https://api.example.com/data')
5. CSS background images và @import
/* Sai */
.hero {
background-image: url('http://example.com/hero.jpg');
}
/* Đúng */
.hero {
background-image: url('https://example.com/hero.jpg');
}
Khi tài nguyên không hỗ trợ HTTPS
Một số CDN cũ của bên thứ ba hoặc asset tự host chỉ phục vụ qua HTTP. Bạn có ba cách xử lý:
- Tự host — tải file về và phục vụ từ domain HTTPS của chính bạn. Phù hợp với font, thư viện, mọi thứ tĩnh.
- Chuyển sang HTTPS CDN — jsDelivr, cdnjs, và unpkg phục vụ mọi package qua HTTPS. Đổi URL là xong.
- Proxy qua server của bạn — định tuyến request qua HTTPS endpoint của chính bạn để fetch và chuyển tiếp tài nguyên HTTP. Tốn công hơn, nhưng đôi khi không tránh khỏi với live data feed.
Kiểm tra sau khi sửa
- Hard-reload trang (
Ctrl+Shift+R/Cmd+Shift+R) để bỏ qua cache. - Mở DevTools → Console. Không còn cảnh báo mixed content nào là xong.
- Kiểm tra biểu tượng ổ khóa trên thanh địa chỉ — không có tam giác cảnh báo, chỉ có ổ khóa đóng sạch sẽ.
- Trong Chrome, click ổ khóa → Connection is secure → Certificate is valid.
- Chạy lại crawl:
npx mixed-content-scanner https://example.com— kết quả phải báo 0 vấn đề.

