iframe “đã từ chối kết nối”: cách sửa X-Frame-Options

Điểm chính

“Đã từ chối kết nối” trong iframe không phải lỗi mạng. Trang được nhúng đã gửi header X-Frame-Options hoặc CSP frame-ancestors yêu cầu trình duyệt không hiển thị nó bên trong một trang khác. Cách sửa nằm ở máy chủ của trang được nhúng, không bao giờ nằm trong thẻ iframe của bạn: giữ SAMEORIGIN cho toàn bộ site và chỉ mở một đường dẫn embed với frame-ancestors *.

Kiểm tra ngay

X-Frame-Options giải thích: iframe báo “đã từ chối kết nối” và cách sửa — SAMEORIGIN cho toàn site, CSP frame-ancestors * chỉ cho đường dẫn embed
X-Frame-Options giải thích: iframe báo “đã từ chối kết nối” và cách sửa — SAMEORIGIN cho toàn site, CSP frame-ancestors * chỉ cho đường dẫn embed
Bắt đầu đọc
  1. “Đã từ chối kết nối” trong iframe thực sự có nghĩa là gì
  2. X-Frame-Options hoạt động như thế nào
  3. DENY hay SAMEORIGIN: nên gửi giá trị nào?
  4. ALLOW-FROM đã chết — đừng dùng
  5. frame-ancestors: chỉ thị CSP thay thế nó
  6. Chuyện gì xảy ra khi đặt cả hai header
  7. Bước 1: tìm header đang chặn bạn
  8. Bước 2: quyết định ai được phép nhúng trang
  9. Mẫu đúng: khóa site, mở một đường dẫn embed
  10. Nginx (và cái bẫy add_header)
  11. Apache
  12. Next.js và Express
  13. Nhúng widget của bên thứ ba một cách an toàn
  14. sandbox: nên cho phép gì với widget máy tính
  15. Vì sao không có cách sửa ở phía bên nhúng
  16. Kiểm tra lại header sau mỗi thay đổi
  17. Triệu chứng → nguyên nhân → cách sửa
  18. Câu hỏi thường gặp

“Đã từ chối kết nối” trong iframe thực sự có nghĩa là gì

Bạn thêm một <iframe> trỏ tới site khác, tải lại, và thay vì trang web bạn thấy một ô màu xám với biểu tượng mặt buồn; rê chuột lên sẽ hiện example.com đã từ chối kết nối. Phản xạ đầu tiên là nghĩ đến sự cố mạng, vì đó chính xác là thứ Chrome hiển thị khi máy chủ chết. Nhưng mở DevTools lên, console sẽ kể một câu chuyện khác:

text
Refused to display 'https://emicalcs.com/' in a frame because it set 'X-Frame-Options' to 'sameorigin'.

Không có gì hỏng trên đường truyền mạng. DNS đã phân giải, kết nối TCP đã mở, TLS đã hoàn tất, và máy chủ trả về một phản hồi 200 hoàn toàn bình thường. Trình duyệt đã tải trang, đọc một header phản hồi, rồi cố ý từ chối hiển thị nó bên trong trang của bạn. Header đó là X-Frame-Options, và nó tồn tại để chặn clickjacking — kiểu tấn công mà trang độc hại tải một biểu mẫu đăng nhập hoặc thanh toán vào một iframe vô hình và lừa người truy cập bấm vào những nút họ không nhìn thấy.

Điều này quan trọng cho cách bạn xử lý sự cố. Một lỗi kết nối thật — ERR_CONNECTION_REFUSED khi bạn mở URL trực tiếp trong tab — là vấn đề của máy chủ hoặc tường lửa, được trình bày trong bài ERR_CONNECTION_REFUSED. Phiên bản iframe chỉ xuất hiện khi trang bị nhúng, còn cùng URL đó vẫn chạy tốt trong tab riêng. Nếu site tải được khi mở trực tiếp nhưng không tải được trong khung của bạn, bạn đang gặp chính sách nhúng khung chứ không phải sự cố ngừng hoạt động.

Xem header X-Frame-Options và CSP của một trang

X-Frame-Options hoạt động như thế nào

X-Frame-Options là một header phản hồi HTTP, được mô tả lần đầu trong RFC 7034 — một RFC mang tính tham khảo mà chuẩn HTML hiện đã thay thế — và được mọi trình duyệt hỗ trợ từ Internet Explorer 8. Máy chủ gắn nó vào một trang; trình duyệt đọc nó trước khi hiển thị và quyết định trang đó có được xuất hiện bên trong <frame>, <iframe>, <embed> hoặc <object> trên trang khác hay không.

Nó nhận một trong hai giá trị còn quan trọng, cộng thêm một giá trị không còn quan trọng:

Giá trịAi được nhúng trangTrường hợp dùng thường gặp
DENYKhông ai cả — kể cả chính site đóTrang đăng nhập, trang quản trị, mọi thứ có phiên đăng nhập
SAMEORIGINChỉ các trang có cùng origin chính xác (scheme + host + port)Mặc định hợp lý cho hầu hết site; cho phép bạn nhúng các trang của chính mình
ALLOW-FROM https://a.comĐã lỗi thời — bị mọi trình duyệt hiện đại bỏ quaKhông dùng; xem bên dưới

Header áp dụng theo từng phản hồi, không phải theo từng site. Đó là chi tiết mà hầu hết hướng dẫn bỏ qua, và cũng là chi tiết giúp cách sửa trong bài này khả thi: máy chủ có thể gửi SAMEORIGIN trên 99% số trang và một chính sách khác trên đúng một đường dẫn dành để nhúng.

DENY hay SAMEORIGIN: nên gửi giá trị nào?

Hãy gửi SAMEORIGIN trừ khi bạn có lý do không làm vậy. Nó chặn mọi khung của bên thứ ba nhưng vẫn để site của bạn tự do nhúng các trang của mình — widget trợ giúp, bản xem trước, bảng điều khiển tải các trang nội bộ khác.

Hãy gửi DENY trên các trang mà việc nhúng không có mục đích chính đáng và hậu quả nghiêm trọng: biểu mẫu đăng nhập, trang thanh toán, các thao tác một cú bấm như “xóa tài khoản”. DENY cũng bao quát một trường hợp tinh vi khi kẻ tấn công chèn được một khung vào một trong các trang của bạn — với SAMEORIGIN, khung bị chèn đó vẫn được cho phép.

Dù chọn giá trị nào, hãy đặt nó một lần ở cấp máy chủ web hoặc CDN để áp dụng cho mọi phản hồi, kể cả trang lỗi và tài nguyên tĩnh. Header đặt trong mã ứng dụng thường bỏ sót trang 404 và trang bảo trì, vì vậy hãy đặt chúng ở nơi mọi phản hồi đều đi qua.

ALLOW-FROM đã chết — đừng dùng

Các bài viết cũ vẫn khuyên dùng X-Frame-Options: ALLOW-FROM https://partner.com để cho phép một site nhúng duy nhất. Nó chưa từng hoạt động trên Chrome hay Safari, và Firefox đã bỏ nó từ phiên bản 70 (tháng 10/2019). Tệ hơn, trình duyệt hiện đại khi thấy ALLOW-FROM sẽ coi cả header là không hợp lệ và bỏ qua hoàn toàn, khiến trang có thể bị bất kỳ ai nhúng — ngược hẳn với ý định của bạn.

Nếu cần cho phép một số site cụ thể, câu trả lời là CSP frame-ancestors, được trình bày ngay sau đây. X-Frame-Options chỉ biết nói “không ai” hoặc “chỉ mình tôi”.

frame-ancestors: chỉ thị CSP thay thế nó

Content Security Policy Level 2 giới thiệu frame-ancestors, một chỉ thị làm được mọi việc của X-Frame-Options và bổ sung điều mà header kia không làm được: một danh sách cho phép thực sự. Nó nằm trong header Content-Security-Policy tiêu chuẩn và nhận một hoặc nhiều nguồn:

http
# Nobody may frame this (same as X-Frame-Options: DENY)
Content-Security-Policy: frame-ancestors 'none'

# Only my own origin (same as SAMEORIGIN)
Content-Security-Policy: frame-ancestors 'self'

# My origin plus two named partners, one with a subdomain wildcard
Content-Security-Policy: frame-ancestors 'self' https://partner.example https://*.trusted.example

# Anyone may frame this — for a public embed endpoint only
Content-Security-Policy: frame-ancestors *

Có ba chi tiết đáng biết trước khi bạn chuyển sang dùng:

  • Nó kiểm tra mọi tổ tiên, không chỉ trang cha. Nếu trang A nhúng trang B và B nhúng trang của bạn, cả A lẫn B đều phải được cho phép (CSP). Chuẩn HTML hiện cũng áp dụng X-Frame-Options cho mọi tổ tiên (WHATWG), nhưng các trình duyệt cũ chỉ kiểm tra trang cha hoặc trang trên cùng.
  • Nó chỉ hoạt động dưới dạng header HTTP. frame-ancestors bị bỏ qua một cách rõ ràng khi CSP được gửi qua thẻ <meta http-equiv> — trình duyệt phải biết chính sách trước khi quyết định có hiển thị tài liệu hay không. X-Frame-Options cũng vậy. Không thể đặt cái nào trong số chúng từ HTML.
  • Ký tự đại diện áp dụng theo từng host, không phải toàn cục. https://*.example.com khớp với mọi subdomain của một site; một dấu * trần khớp với mọi origin trên internet. Ổn với widget máy tính, nhưng sai với bất cứ thứ gì mang phiên đăng nhập.

Chuyện gì xảy ra khi đặt cả hai header

Hầu hết các site cuối cùng gửi cả hai trong quá trình chuyển đổi, và thứ tự ưu tiên đã được quy định: nếu có frame-ancestors, trình duyệt thực thi nó và bỏ qua X-Frame-Options. Đặc tả CSP Level 2 nói thẳng như vậy, và Chrome, Firefox, Safari cùng Edge đều tuân theo.

Điều đó làm lộ trình chuyển đổi an toàn trở nên đơn giản. Giữ X-Frame-Options: SAMEORIGIN cho số ít trình duyệt cổ có trước CSP 2, thêm Content-Security-Policy: frame-ancestors 'self' bên cạnh, và mọi trình duyệt hiện đại sẽ dùng quy tắc CSP. Hai header chỉ gây rắc rối nếu bạn cho chúng nói những điều khác nhau — chẳng hạn X-Frame-Options: DENY cùng frame-ancestors * — khi đó trình duyệt hiện đại cho phép nhúng còn chỉ trình duyệt rất cũ mới chặn. Giữ chúng nhất quán thì không có gì phải gỡ lỗi.

Bước 1: tìm header đang chặn bạn

Trước khi thay đổi bất cứ điều gì, hãy xác nhận chính sách nào đang áp dụng. Console của trình duyệt đã cho bạn biết tên header; curl cho bạn thấy giá trị chính xác mà máy chủ gửi. Ví dụ này dùng site trong ví dụ thực tế ở phần sau — các trang chính của nó được khóa có chủ ý:

bash
curl -sI https://emicalcs.com/ | grep -i -E 'x-frame-options|content-security-policy'

# Output:
# x-frame-options: SAMEORIGIN

Kết quả sẽ là một trong ba trường hợp:

  • x-frame-options: SAMEORIGIN hoặc DENY — chính sách nhúng khung cổ điển. Chỉ một thay đổi trên máy chủ đó mới gỡ được.
  • content-security-policy: … frame-ancestors … — một chính sách CSP. Hãy đọc danh sách nguồn; nếu origin của bạn không có trong đó, bạn bị chặn. Chỉ thị này thường nằm lẫn trong một header CSP dài cùng script-src và các chỉ thị khác, nên hãy đọc toàn bộ giá trị.
  • Không có header nào: site được nhúng cho phép khung, và “đã từ chối kết nối” có nguyên nhân khác: một chuyển hướng tới trang đang bị khóa (thường là trang đăng nhập; theo dõi bằng curl -sIL), hoặc header Cross-Origin-Embedder-Policy: require-corp trên chính trang của bạn, vốn chặn mọi khung không chủ động đồng ý và hiện cùng một ô xám.

Bước 2: quyết định ai được phép nhúng trang

Mọi thứ từ đây trở đi phụ thuộc vào một câu hỏi: bạn có kiểm soát site được nhúng không?

Nếu không, hãy dừng lại ở đây. Không có cách lách phía máy khách — không phải thuộc tính sandbox, không phải src khác, không phải một cờ trình duyệt mà khách truy cập của bạn sẽ có. Chủ site đã quyết định trang của họ không được nhúng, và trình duyệt đang thực thi quyết định đó thay họ. Lựa chọn của bạn là đặt liên kết tới trang đó, hỏi chủ site xem họ có cung cấp endpoint để nhúng không, hoặc dùng widget chính thức nếu có. Nhiều dịch vụ khóa site chính của mình vẫn có cung cấp; ví dụ ở phần sau cho thấy nó trông như thế nào.

Nếu bạn kiểm soát nó, hãy cưỡng lại cám dỗ gỡ header trên toàn site. Làm vậy mở lại clickjacking trên mọi trang — kể cả những trang có phiên đăng nhập và biểu mẫu — chỉ để sửa việc nhúng ở một trang. Thay vào đó, hãy quyết định những đường dẫn cụ thể nào dành để nhúng, và nhúng bởi ai:

Tình huốngHeader cần gửi trên đường dẫn đó
Công cụ nội bộ được bảng điều khiển của chính bạn nhúngframe-ancestors 'self' (kèm X-Frame-Options: SAMEORIGIN)
Widget được một vài đối tác đã biết nhúngframe-ancestors 'self' https://partner-a.com https://partner-b.com
Widget công khai cho phép nhúng (máy tính, bản đồ, trình phát)frame-ancestors * chỉ trên đường dẫn embed
Mọi thứ còn lạiframe-ancestors 'self' kèm X-Frame-Options: SAMEORIGIN

Mẫu đúng: khóa site, mở một đường dẫn embed

Ví dụ thực tế gọn gàng nhất về mẫu này mà tôi từng gặp là EMICalcs, một bộ sưu tập miễn phí gồm 45 máy tính tài chính của Ấn Độ — trả góp EMI, SIP, thuế thu nhập, GST và nhiều loại khác — mời các site khác nhúng. Site chính bị khóa; một đường dẫn /embed/ riêng được mở. So sánh header của hai bên:

bash
# The calculator page a visitor normally uses — locked
curl -sI https://emicalcs.com/income-tax-calculator/ | grep -i -E 'x-frame|frame-ancestors'
# x-frame-options: SAMEORIGIN

# The same calculator on its embed path — open to any site
curl -sI https://emicalcs.com/embed/income-tax-calculator/ | grep -i -E 'x-frame|frame-ancestors'
# content-security-policy: frame-ancestors *

Hãy để ý điều mà phản hồi embed không chứa: không có header X-Frame-Options nào trên /embed/. Đó là cố ý. Vì chỉ thị CSP được ưu tiên, để nguyên SAMEORIGIN sẽ vô hại trên các trình duyệt hiện đại — nhưng nó sẽ gây nhầm lẫn cho kỹ sư tiếp theo đọc header, và vẫn chặn vài trình duyệt cũ chỉ hiểu X-Frame-Options. Đường dẫn embed gửi một chính sách duy nhất, và nó nói đúng một điều.

Mọi thứ còn lại trên site giữ SAMEORIGIN, nên trang chủ, các bài viết và các trang máy tính đầy đủ vẫn được bảo vệ khỏi clickjacking. Các phiên bản embed là tài liệu rút gọn, không có điều hướng và không có gì đáng để chiếm quyền, vì vậy mở chúng cho * là một đánh đổi hợp lý.

Hướng dẫn nhúng của họ đáng đọc như một mẫu nếu bạn đang xuất bản widget của riêng mình: một URL ổn định cho mỗi máy tính, chiều cao cố định vừa với hầu hết, loading="lazy" trong đoạn mã, và cam kết rằng các URL embed đã ngừng sẽ chuyển hướng thay vì trả 404 — vì mọi site nhúng bạn đều tin rằng URL của bạn sẽ tiếp tục hoạt động.

Nginx (và cái bẫy add_header)

Đây là cùng một mẫu ở ba nơi mà hầu hết site đặt header. Ở mỗi nơi, một quy tắc toàn site khóa mọi thứ và một quy tắc hẹp hơn ghi đè nó cho đường dẫn embed. Nginx trước, vì nó có cái bẫy:

nginx
# Site-wide default (server block)
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Content-Type-Options "nosniff" always;

location /embed/ {
    # WARNING: any add_header here stops ALL add_header inheritance from the
    # server block — re-declare every header you still want on this path.
    add_header Content-Security-Policy "frame-ancestors *" always;
    add_header X-Content-Type-Options "nosniff" always;
}

Phần chú thích này không phải để bỏ qua. add_header không được gộp giữa các cấp: ngay khi một khối location khai báo bất kỳ add_header nào, nó ngừng kế thừa mọi add_header từ khối server. Các nhóm thường xuyên mất header HSTS hay nosniff trên đường dẫn embed theo cách này và chỉ phát hiện qua một lần quét bảo mật nhiều tháng sau. Hãy khai báo lại những gì bạn cần, và bỏ X-Frame-Options khỏi khối embed để đường dẫn gửi một chính sách rõ ràng. Từ nginx 1.29.3, add_header_inherit merge có thể bật lại việc kế thừa (nginx), nhưng đừng dùng nó trong khối embed: nó sẽ thêm lại X-Frame-Options và frame-ancestors 'self' của toàn site vào đường dẫn đó, và với hai header CSP, trình duyệt thực thi cả hai, nên khung vẫn bị chặn.

Apache

Chỉ thị Header của Apache được gộp giữa các phạm vi, nên bạn có thể đặt mặc định một lần và chỉ động tới hai header thay đổi trên đường dẫn embed:

apache
# Site-wide default (needs mod_headers)
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'"

<LocationMatch "^/embed/">
    Header always unset X-Frame-Options
    Header always set Content-Security-Policy "frame-ancestors *"
</LocationMatch>

Next.js và Express

Trong Next.js, header được khai báo trong next.config.js. Khi hai quy tắc khớp cùng một đường dẫn và đặt cùng một header, quy tắc sau thắng — nhưng header chỉ do quy tắc trước đặt vẫn được áp dụng. Để giữ X-Frame-Options hoàn toàn khỏi đường dẫn embed, hãy loại nó khỏi quy tắc toàn site bằng negative lookahead:

javascript
// next.config.js
module.exports = {
  async headers() {
    return [
      {
        // Every path EXCEPT /embed/... gets the locked defaults
        source: '/((?!embed/).*)',
        headers: [
          { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
          { key: 'Content-Security-Policy', value: "frame-ancestors 'self'" },
        ],
      },
      {
        source: '/embed/:path*',
        headers: [
          { key: 'Content-Security-Policy', value: 'frame-ancestors *' },
        ],
      },
    ]
  },
}

Trên Express, middleware helmet phổ biến mặc định đã gửi X-Frame-Options: SAMEORIGIN và một CSP có frame-ancestors 'self'. Hãy gắn nó ở phạm vi toàn cục, rồi trong router embed gọi res.removeHeader('X-Frame-Options') và đặt Content-Security-Policy thành frame-ancestors * trước khi render. Thứ tự rất quan trọng: phần ghi đè của bạn phải chạy sau helmet, nếu không helmet sẽ ghi đè lên nó.

Nhúng widget của bên thứ ba một cách an toàn

Giờ là phía bên kia: bạn là site đi nhúng, nhà cung cấp có sẵn một đường dẫn embed mở, và bạn muốn khung hoạt động đúng. Một đoạn mã widget làm tốt trông như sau; đây là đoạn EMICalcs xuất bản trên mỗi trang máy tính:

html
<iframe src="https://emicalcs.com/embed/income-tax-calculator/"
        title="Income Tax Calculator FY 2026-27 — EMICalcs"
        width="100%" height="760"
        style="max-width:680px;border:0;border-radius:16px;overflow:hidden"
        loading="lazy"></iframe>
<p>Free <a href="https://emicalcs.com/income-tax-calculator/" target="_blank" rel="noopener">Income Tax Calculator FY 2026-27</a> by EMICalcs</p>

Mỗi thuộc tính đều có một nhiệm vụ:

  • title — đặt tên cho khung cho trình đọc màn hình. Không có nó, công nghệ hỗ trợ chỉ đọc “khung” và không gì khác, và các cuộc kiểm tra khả năng truy cập sẽ đánh trượt trang.
  • width="100%" kèm max-width trong style — lấp đầy cột hẹp trên di động mà không tràn ra ở cột rộng trên máy tính.
  • height cố định — trang nhúng không thể đọc chiều cao nội dung của khung khác origin, nên một giá trị đặt cứng là cách duy nhất để tránh dịch chuyển bố cục khi nó đang tải. Chỉ cần đặt một lần cho mỗi widget.
  • loading="lazy" — trì hoãn yêu cầu cho đến khi khung cuộn gần vùng nhìn thấy, nên widget ở cuối một bài viết dài không tốn gì khi tải ban đầu.
  • border:0; border-radius; overflow:hidden — bỏ viền lõm mặc định và cắt bo góc để widget trông liền mạch với trang của bạn.

Dòng ghi nguồn được đặt có chủ ý bên ngoài iframe. Mọi thứ bên trong khung thuộc về tài liệu được nhúng — các liên kết của nó là liên kết của nhà cung cấp, trên URL của nhà cung cấp — nên một liên kết bên trong khung không phải là một phần của trang bạn đối với độc giả hay trình thu thập dữ liệu của công cụ tìm kiếm. Nếu bạn muốn ghi nguồn, hoặc nhà cung cấp yêu cầu, hãy đặt một đoạn văn bình thường cạnh khung trong HTML của riêng bạn, như ở trên.

sandbox: nên cho phép gì với widget máy tính

Thuộc tính sandbox là biện pháp bảo vệ của chính bên nhúng. Thêm nó không kèm giá trị thì trang được nhúng không có gì cả: không script, không biểu mẫu, không cửa sổ bật lên, không điều hướng cửa sổ của bạn, và có một origin mờ vô hiệu hóa cookie và bộ nhớ của chính nó. Sau đó chỉ thêm lại những gì widget cần:

html
<iframe src="https://emicalcs.com/embed/loan-emi-calculator/"
        sandbox="allow-scripts allow-forms allow-same-origin allow-popups"
        title="Loan EMI Calculator — EMICalcs"
        width="100%" height="760" loading="lazy"></iframe>

allow-scripts và allow-forms là mức tối thiểu cho mọi thứ tương tác. allow-same-origin cho phép trang được nhúng giữ origin riêng để bộ nhớ và cookie của nó hoạt động — với embed khác origin điều này an toàn, vì “origin của nó” vẫn là của nhà cung cấp, không phải của bạn. allow-popups chỉ cần nếu widget mở liên kết trong tab mới, như liên kết ghi nguồn bên trong embed của EMICalcs.

Điều bạn không nên làm là kết hợp allow-scripts với allow-same-origin trên khung trỏ tới chính origin của bạn. Khi đó script trong khung có thể với lên và gỡ thuộc tính sandbox của chính nó, và bạn chẳng sandbox được gì.

Vì sao không có cách sửa ở phía bên nhúng

Tìm kiếm “iframe đã từ chối kết nối” cho ra ba cách lách phổ biến. Cả ba đều tệ, và nên biết chính xác vì sao.

  • Tiện ích mở rộng trình duyệt gỡ X-Frame-Options — chúng chỉ hoạt động trên trình duyệt của riêng bạn. Mọi khách truy cập site của bạn vẫn thấy ô xám, và trong lúc đó bạn đã tắt bảo vệ chống clickjacking trên mọi site bạn truy cập.
  • Chuyển tiếp trang qua máy chủ của bạn — bạn tải HTML của bên thứ ba ở phía máy chủ rồi phục vụ từ origin của mình, nên header không bao giờ tới trình duyệt. Cách này làm hỏng liên kết tương đối, cookie, đăng nhập và mọi thứ chạy bằng script; bạn phải chịu trách nhiệm về nội dung của họ; và nó thường vi phạm điều khoản của nhà cung cấp. Các site phát hiện ra sẽ chặn IP máy chủ của bạn.
  • Dịch vụ chụp ảnh màn hình hoặc “web-to-image” — bạn nhận một bức ảnh tĩnh, không phải một trang chạy được. Tốt cho ảnh thu nhỏ xem trước, vô dụng với một máy tính.

Header là một tuyên bố của chủ site, được trình duyệt thực thi, về những gì được phép xảy ra với trang của họ. Các cách hợp lệ duy nhất là hai cách đã nêu: chủ site mở một đường dẫn embed, hoặc bạn đặt liên kết ra ngoài. Nhà cung cấp muốn được nhúng sẽ làm việc này dễ dàng — những bên tốt công bố một đoạn mã, giữ URL ổn định và chỉ mở đường dẫn an toàn để mở.

Kiểm tra lại header sau mỗi thay đổi

Header nhúng khung được đặt trong một tệp cấu hình rồi bị lãng quên, và một CDN, reverse proxy hay plugin bảo mật có thiện chí có thể thêm, gỡ hoặc nhân đôi chúng bất cứ lúc nào. Sau khi triển khai, hãy kiểm tra từ bên ngoài mạng của bạn:

bash
# The locked pages should show SAMEORIGIN and/or frame-ancestors 'self'
curl -sI https://example.com/ | grep -i -E 'x-frame-options|content-security-policy'

# The embed path should show ONLY frame-ancestors, with the sources you intended
curl -sI https://example.com/embed/widget/ | grep -i -E 'x-frame-options|content-security-policy'

# Error pages should carry the header too
curl -sI https://example.com/this-does-not-exist | grep -i -E 'x-frame-options|content-security-policy'

Sau đó hãy thử theo cách trình duyệt làm: đặt URL embed vào một <iframe> trên trang được phục vụ từ một origin khác — trang file:// cục bộ sẽ không dùng được, vì trình duyệt xử lý nó đặc biệt — rồi mở console. Tải sạch nghĩa là không có dòng Refused to display hay Refused to frame.

Nếu site nằm sau Cloudflare hoặc CDN khác, một bản sao trong bộ nhớ đệm hoặc quy tắc ở rìa có thể mang header khác với origin của bạn. Công cụ kiểm tra HTTP Header yêu cầu URL từ máy chủ của chúng tôi, bên ngoài trình duyệt và mạng của bạn, và hiển thị X-Frame-Options cùng Content-Security-Policy kèm kết luận về nhúng khung (SAMEORIGIN hoặc DENY, qua CSP frame-ancestors, hoặc không gửi): một cách nhanh để bắt một quy tắc CDN đã âm thầm thêm lại SAMEORIGIN vào đường dẫn embed của bạn.

Triệu chứng → nguyên nhân → cách sửa

Dùng bảng này để nhảy thẳng tới cách sửa thay vì đọc lại toàn bộ hướng dẫn:

Bạn thấy gìĐiều đó có nghĩa là gìCách sửa
Ô xám “đã từ chối kết nối”, nhưng URL tải được trong tabX-Frame-Options hoặc frame-ancestors đã chặn khungChỉ chủ site mới thay đổi được — dùng đường dẫn embed của họ hoặc đặt liên kết ra ngoài
Console: Refused to display … 'X-Frame-Options' to 'sameorigin'Header cổ điển; chỉ cho phép khung cùng originTrên site được nhúng, mở đường dẫn embed với frame-ancestors * hoặc danh sách đối tác
Console: Refused to frame … violates … frame-ancestorsDanh sách cho phép của CSP không có origin của bạnThêm origin của bạn vào các nguồn frame-ancestors của site được nhúng
Hôm qua còn chạy, hôm nay hỏng, không đổi mãCDN hoặc plugin bảo mật bắt đầu chèn SAMEORIGINKiểm tra bằng curl; loại đường dẫn embed khỏi quy tắc của CDN hoặc plugin
Bảng điều khiển của chính bạn không nhúng được trang của chính bạnLệch subdomain hoặc scheme (www so với tên miền gốc, http so với https)Dùng frame-ancestors 'self' https://example.com https://*.example.com (ký tự đại diện không bao gồm tên miền gốc)
Embed chạy được, nhưng HSTS hoặc nosniff biến mất trên đường dẫn đóCơ chế kế thừa add_header của Nginx đã làm rơi chúngKhai báo lại mọi header bên trong khối location
ERR_CONNECTION_REFUSED khi mở trực tiếp URLMột lỗi mạng thật, không phải chính sách nhúng khungXem ERR_CONNECTION_REFUSED và port checker

Nếu URL cũng lỗi trong tab riêng của nó, thì khung chỉ là một manh mối giả. Hãy đi qua các tầng mạng theo thứ tự — DNS, cổng, TLS, header — bằng hướng dẫn kiểm thử endpoint công khai trước khi đụng tới bất kỳ header nhúng khung nào.

Xem mọi header bảo mật mà một URL gửi

Kiểm tra X-Frame-Options, Content-Security-Policy, HSTS và các header khác của bất kỳ trang nào, được tải từ máy chủ của chúng tôi, để tiện ích mở rộng trình duyệt hay proxy trong mạng của bạn không thể thay đổi những gì bạn thấy. Miễn phí, không cần đăng ký.

Nguồn

  1. RFC 7034: HTTP Header Field X-Frame-Options, IETF
  2. CSP Level 3: frame-ancestors, W3C
  3. HTML Standard: the X-Frame-Options header, WHATWG
  4. ngx_http_headers_module (add_header, add_header_inherit), nginx

Câu hỏi thường gặp

X-Frame-Options có lỗi thời không?

Về chức năng, nó đã bị chỉ thị CSP frame-ancestors thay thế, và trình duyệt ưu tiên chỉ thị này mỗi khi có cả hai header. Tuy vậy nó không vô dụng: mọi trình duyệt hiện tại vẫn tuân theo X-Frame-Options khi không có frame-ancestors, và nó bảo vệ số ít trình duyệt cũ có trước CSP Level 2. Lời khuyên thực tế là gửi cả hai và giữ chúng nhất quán — SAMEORIGIN cùng frame-ancestors 'self'.

X-Frame-Options: DENY nghĩa là gì?

DENY báo cho trình duyệt rằng trang không bao giờ được hiển thị bên trong phần tử frame, iframe, embed hay object trên bất kỳ trang nào — kể cả các trang cùng site. Đây là thiết lập nghiêm ngặt nhất và là lựa chọn đúng cho biểu mẫu đăng nhập, trang thanh toán và trang quản trị, nơi không có lý do chính đáng nào để nhúng trang và clickjacking sẽ rất nghiêm trọng.

Khác biệt giữa DENY và SAMEORIGIN là gì?

DENY chặn mọi việc nhúng khung. SAMEORIGIN chỉ cho phép các trang có cùng origin chính xác — cùng scheme, host và port — nhúng, và chặn tất cả những ai khác. SAMEORIGIN là mặc định hợp lý cho toàn site vì nó ngăn clickjacking của bên thứ ba mà vẫn để site của bạn nhúng các trang của chính mình; DENY dành cho số ít trang không bao giờ nên bị nhúng.

Làm sao vượt qua X-Frame-Options để nhúng một site?

Từ phía bên nhúng thì không thể. Header do site bạn định nhúng đặt ra và được trình duyệt của khách truy cập thực thi; không thuộc tính iframe, cờ sandbox hay mẹo phía máy khách nào thay đổi được điều đó. Tiện ích gỡ header chỉ ảnh hưởng đến trình duyệt của riêng bạn, còn chuyển tiếp trang qua máy chủ của bạn thì làm hỏng nó và thường vi phạm điều khoản của nhà cung cấp. Các cách hợp lệ là dùng endpoint embed chính thức nếu nhà cung cấp có, hoặc đặt liên kết tới trang.

Làm sao đặt X-Frame-Options thành DENY?

Thêm nó như một header phản hồi HTTP ở cấp máy chủ web để áp dụng cho mọi trang, kể cả trang lỗi. Trong Nginx: add_header X-Frame-Options "DENY" always; trong Apache: Header always set X-Frame-Options "DENY". Kết hợp với Content-Security-Policy: frame-ancestors 'none' cho các trình duyệt hiện đại. Không thể đặt nó bằng thẻ meta — chỉ header HTTP mới có tác dụng.

Vì sao iframe của tôi báo “đã từ chối kết nối” trong khi site chạy bình thường ở tab mới?

Vì kết nối không có vấn đề gì cả. Site đã trả trang bình thường, nhưng kèm header X-Frame-Options hoặc frame-ancestors cấm hiển thị nó bên trong trang khác. Chrome hiển thị cùng ô xám “đã từ chối kết nối” cho trường hợp này như cho lỗi mạng thật, nên mới gây khó hiểu; thông báo trong console “Refused to display … in a frame” chính là manh mối.

Tôi có thể đặt X-Frame-Options hoặc frame-ancestors trong thẻ meta không?

Không. Cả hai phải được gửi dưới dạng header phản hồi HTTP. Trình duyệt phải biết chính sách nhúng khung trước khi quyết định có hiển thị tài liệu hay không, và thẻ meta đến quá muộn. frame-ancestors bị bỏ qua một cách rõ ràng trong CSP qua meta http-equiv, còn X-Frame-Options trong thẻ meta bị mọi trình duyệt bỏ qua.

frame-ancestors * có làm site của tôi kém an toàn không?

Chỉ với những phản hồi gửi nó. Header được đánh giá theo từng tài liệu, nên gửi frame-ancestors * trên một đường dẫn /embed/ riêng vẫn để phần còn lại của site được bảo vệ đầy đủ bởi SAMEORIGIN hoặc frame-ancestors 'self'. Rủi ro chỉ giới hạn ở những gì kẻ tấn công có thể thu được khi nhúng chính trang embed — mà với một widget không lưu trạng thái, không có cookie phiên và không có thao tác nhạy cảm, thì là không gì cả.

Vì sao X-Frame-Options: ALLOW-FROM không hoạt động?

ALLOW-FROM chỉ từng được triển khai bởi Internet Explorer, Edge gốc (phiên bản 12 đến 18) và Firefox, vốn đã gỡ nó ở phiên bản 70. Chrome và Safari chưa bao giờ hỗ trợ. Trình duyệt hiện đại coi header có chứa ALLOW-FROM là không hợp lệ và bỏ qua hoàn toàn, khiến trang có thể bị bất kỳ ai nhúng. Hãy dùng chỉ thị CSP frame-ancestors, hỗ trợ một danh sách origin được phép đúng nghĩa.

Header nào thắng nếu cả X-Frame-Options và frame-ancestors cùng được đặt?

frame-ancestors. Chuẩn HTML yêu cầu trình duyệt bỏ qua X-Frame-Options mỗi khi một CSP có hiệu lực chứa frame-ancestors (CSP Level 2 đã khuyến nghị điều này), và mọi trình duyệt hiện tại đều làm vậy; chính sách Report-Only không được tính. Chỉ các trình duyệt quá cũ không hiểu CSP mới quay về X-Frame-Options, đó là lý do nó vẫn đáng gửi — miễn là hai header khớp nhau.

Về tác giả

Shaik Vahid

Nhà sáng lập DNS Robot. Vahid đã làm nhiều năm trong bộ phận hỗ trợ hosting, xử lý tên miền, bản ghi DNS, chứng chỉ SSL và việc gửi email cho khách hàng. Anh xây dựng các công cụ của DNS Robot và viết những hướng dẫn này từ các lỗi mà mọi người thực sự gặp.

Kiểm tra HTTP Header

Phân tích header phản hồi HTTP và chấm điểm header bảo mật