0

Công cụ kiểm tra security header chấm google.com 0/9: hóa ra mình chấm nhầm trang chặn HTTP 429

Mình nhập http://google.com vào công cụ kiểm tra security header của mình, và nó trả về 0 trên 9 header bảo mật. Không HSTS, không CSP, đến X-Frame-Options cũng không. Nếu tin con số đó thì Google gần như chẳng cấu hình header bảo mật nào. Thực tế công cụ đã chấm điểm một trang chặn trả về HTTP 429, chứ không phải website của Google.

Mình là người làm ra MyIPScan, nên lỗi này là của mình. Bài này kể lỗi nằm ở đâu và mình sửa thế nào. Nó không chỉ là chuyện của checker header: công cụ nào fetch trang của người khác từ server cũng có thể dính.

Con số sai nhưng trông rất chắc chắn

Một checker header chỉ có giá trị khi người ta tin con số của nó. Con số sai mà trông chắc chắn còn tệ hơn một thông báo lỗi, vì không ai nghi ngờ nó.

Công cụ kiểm tra 9 header:

  • Strict-Transport-Security
  • Content-Security-Policy
  • X-Frame-Options
  • X-Content-Type-Options
  • Referrer-Policy
  • Permissions-Policy
  • Cross-Origin-Opener-Policy
  • Cross-Origin-Embedder-Policy
  • Cross-Origin-Resource-Policy

Với một domain như google.com, kết quả 0/9 lẽ ra phải làm mình dừng lại ngay.

Hai cách giải thích dễ nghĩ đến

Parser đọc header bị hỏng? Đây là nghi ngờ đầu tiên ai cũng có: tên header không phân biệt hoa thường, một phép so sánh chuỗi sai là đủ ra 0/9. Nhưng 0/9 ở đây không đến từ parser.

Redirect không được follow? http://google.com trả 301 chứ không trả trang. Nhưng response mà checker chấm không phải 301, mà là 429.

Nguyên nhân thật: trang /sorry/ với HTTP 429

Response mà server của công cụ nhận được không phải trang chủ Google, mà là trang chặn /sorry/ với status 429.

429 Too Many Requests được định nghĩa trong RFC 6585, mục 4. Server của MyIPScan chạy trên Cloudflare Workers, nên request đi ra từ một địa chỉ IP cloud, và địa chỉ đó nhận về trang /sorry/ kèm 429. Mình không biết chính xác logic nào phía Google quyết định việc này, nên không đoán thêm. Điều mình biết chắc: thứ checker chấm điểm không phải website.

Từ một IP bình thường, curl cho thấy chuỗi response khác hẳn:

http://google.com  ->  301  ->  http://www.google.com/  ->  200

Đây là tóm tắt chuỗi redirect, không phải output nguyên văn của curl. Một redirect, rồi tới trang thật. Từ cloud thì ra trang chặn. Cùng một URL, hai response khác nhau, và checker cũ chấm cả hai như nhau.

Phần khó chịu nhất: code đã làm đúng nhiệm vụ "đọc header của response". Nó chỉ chưa bao giờ hỏi response đó có phải của website hay không.

Cách sửa: không chấm điểm khi chưa chắc mình đang nhìn vào cái gì

Mình thêm một bước kiểm tra trước khi đánh giá header. Nếu response là 403, 429 hoặc trông giống trang challenge, công cụ báo "không kiểm tra được" thay vì đưa ra điểm.

403 Forbidden được mô tả trong RFC 9110, mục 15.5.4. Dấu hiệu trang challenge mà guard tìm: đường dẫn /sorry/ trong URL cuối cùng, chuỗi cf-chl (Cloudflare challenge) và từ captcha trong phần đầu của body.

Dưới đây là ví dụ minh họa ý tưởng, đã rút gọn, không phải code nguyên bản của công cụ:

// Ví dụ minh họa (rút gọn), không phải code production của MyIPScan.
const HEADERS = [
  "strict-transport-security", "content-security-policy",
  "x-frame-options", "x-content-type-options", "referrer-policy",
  "permissions-policy", "cross-origin-opener-policy",
  "cross-origin-embedder-policy", "cross-origin-resource-policy",
];
const BLOCK_STATUSES = new Set([403, 429]);

function gradeHeaders(headers) {
  const present = HEADERS.filter((h) => headers.has(h));
  return { present, score: `${present.length}/${HEADERS.length}` };
}

// Chỉ đọc chunk đầu tiên của body, tối đa `limit` ký tự, rồi hủy stream.
async function readHead(res, limit = 4096) {
  if (!res.body) return "";
  const reader = res.body.getReader();
  const { value } = await reader.read();
  await reader.cancel();
  return new TextDecoder().decode(value ?? new Uint8Array()).slice(0, limit);
}

async function blockReason(res) {
  // 1. Status trước tiên.
  if (BLOCK_STATUSES.has(res.status)) return `status ${res.status}`;
  // 2. URL cuối cùng sau redirect.
  if (new URL(res.url).pathname.startsWith("/sorry/")) return "challenge URL";
  // 3. Dấu hiệu trong phần đầu body.
  const head = await readHead(res);
  if (/cf-chl|captcha/i.test(head)) return "challenge marker in body";
  return null;
}

async function checkHeaders(url) {
  try {
    const res = await fetch(url, { redirect: "follow" });
    const reason = await blockReason(res);
    if (reason) {
      return { ok: false, verdict: "could_not_check", reason, finalUrl: res.url };
    }
    return { ok: true, finalUrl: res.url, status: res.status, ...gradeHeaders(res.headers) };
  } catch (e) {
    return { ok: false, verdict: "could_not_check", reason: String(e) };
  }
}

Ba chỗ cần để ý trong đoạn code:

  • Thứ tự kiểm tra là status, rồi URL cuối cùng (res.url sau redirect), rồi mới tới body. Trang chặn có thể nằm sau một hoặc vài redirect, nên URL người dùng nhập không nói lên gì.
  • Body chỉ được đọc một chunk đầu, tối đa 4096 ký tự, rồi stream bị hủy. Guard cần dấu hiệu, không cần cả trang. Riêng captcha có thể bắt nhầm một trang có form reCAPTCHA thật, nên đây là heuristic, không phải phép thử chắc chắn.
  • could_not_check là một trạng thái riêng, không phải điểm 0. Với người dùng, "không kiểm tra được" và "0/9" mang nghĩa hoàn toàn khác nhau.

Kết quả kiểm tra security header bây giờ

Ngày 3/11/2026 mình kiểm tra lại trên công cụ đang chạy thật, nhập http://google.com:

Trường Giá trị
Final URL http://www.google.com/
Status 200 OK
Redirects 1
Kết quả 1 trên 9 header có mặt
Header tìm thấy X-Frame-Options: SAMEORIGIN

8 header còn lại không có. Lần này chuỗi response khớp với những gì curl từ IP thường cho thấy: một redirect 301, rồi trang thật với status 200.

Thiếu HSTS ở đây là đúng chuẩn

Nhìn bảng trên, có người sẽ hỏi: Google mà không có Strict-Transport-Security? Ở đây thì đúng là không nên có, vì response cuối cùng đi qua http://, không phải HTTPS.

Theo RFC 6797, mục 7.2, HSTS Host không được (MUST NOT) gửi header STS trong response đi qua kênh không an toàn. Còn theo mục 8.1, nếu nhận response qua kênh không an toàn, trình duyệt phải (MUST) bỏ qua header đó.

Công cụ của mình hiện vẫn tính HSTS là "không có" trong trường hợp này, nên 1/9 ở bảng trên cần đọc kèm chú thích đó. Checker tốt không nên trừ điểm HSTS khi response cuối đi qua HTTP, và đây là việc tiếp theo mình phải sửa.

Quy tắc mình rút ra

Trước khi đánh giá header, hãy chắc rằng bạn đang đánh giá chính website đó:

  1. Xem status code. 403 hoặc 429 nghĩa là bạn không nhận được trang thật, nên không có gì để chấm.
  2. Xem URL cuối cùng sau redirect. /sorry/ hay một trang challenge không phải trang chủ.
  3. Tìm dấu hiệu challenge trong phần đầu body: cf-chl, captcha.
  4. Khi không chắc, trả về "không kiểm tra được" chứ không trả về "0 trên 9".

Mình gặp chuyện này vì server chạy trên Cloudflare Workers. Nếu công cụ của bạn cũng fetch từ IP cloud, hãy kiểm tra từ chính môi trường production, không phải từ laptop.

Guard này đang chạy trong công cụ kiểm tra header của MyIPScan. Nếu bạn gặp trang chặn mà nó chưa nhận ra, để lại comment kèm URL, mình sẽ thêm dấu hiệu đó.

Gói lại thành một quy tắc: checker không được tự tin hơn dữ liệu nó nhận được. Chưa chắc đang nhìn vào chính website thì đừng chấm điểm.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí