Nhóm nghiên cứu của Parameter vừa công bố một chuỗi lỗi trên ChatGPT, trong đó một link chia sẻ công khai có thể chạy JavaScript tùy ý dưới origin chatgpt.com, trong phiên đăng nhập của người mở link. OpenAI đã vá xong vào ngày 26/10/2026, xếp loại P1 và trả thưởng tổng cộng 5.000 USD.
Điều đáng học không phải là bản thân lỗi đã vá, mà là cách nó hình thành: từng tính năng riêng lẻ đều hợp lý, và preview sandbox thực sự hoạt động đúng. Lỗ hổng chỉ xuất hiện ở chỗ các tính năng gặp nhau. Nếu sản phẩm của bạn có iframe, widget bên thứ ba hoặc tính năng chia sẻ nội dung do người dùng tạo, mô hình này rất đáng xem.
Sandbox đúng, nhưng có một khe thư
ChatGPT có tính năng Preview để biến khối HTML thành trang chạy được. Vì nội dung là code tùy ý, nó được đặt trong iframe hạn chế ở một origin khác. Code trong đó không đọc được parent.document. Đến đây thiết kế vẫn đúng.
Nhưng same-origin policy không chặn postMessage. Đây là cơ chế hợp lệ để các frame khác origin trao đổi tin nhắn, giống một khe thư: không vào được nhà nhưng vẫn nhét được thư qua khe. An toàn hay không phụ thuộc hoàn toàn vào việc bên nhận có kiểm tra người gửi hay không.
Câu hỏi khởi đầu của nhóm nghiên cứu rất đáng ghi nhớ: nếu code đã bị nhốt sang origin khác, nó còn có thể nói chuyện với cái gì?
Mắt xích 1: listener không kiểm tra người gửi
Cùng cuộc hội thoại có một đồ thị tương tác. Đồ thị này nạp thư viện toán MathLive, và MathLive cài vào trang chính một listener cho bàn phím ảo. Logic của nó, viết gọn lại, tương đương:
window.addEventListener("message", (event) => {
if (event.data?.type !== "mathlive#virtual-keyboard-message") return;
if (!validateOrigin(event.origin, originValidator)) return; // policy là "none"
handleMessage(event.data, event.source);
});
Có một bước kiểm tra origin, nhưng policy được đặt thành none, tức chấp nhận mọi nguồn. Tin nhắn update-setting cũng không yêu cầu đến từ một cửa sổ đã được cấp phép trước đó. Ai biết format tin nhắn đều có thể thay layout của bàn phím.
Đáng chú ý là người viết ChatGPT có thể không hề biết về listener này. Nó đến theo thư viện, được cài khi một đồ thị được render. Đây là lý do một cuộc audit chỉ đọc code của mình sẽ không thấy.
Mắt xích 2: dữ liệu cấu hình chứa HTML
Layout bàn phím cho phép chứa markup. Khi bàn phím hiển thị, MathLive ghép layout thành chuỗi rồi gán vào innerHTML của trang thật. Có một helper tên createHTML trông như đang sanitize, nhưng thực chất chỉ đánh dấu chuỗi là tin cậy rồi trả nguyên văn.
Đây là cái bẫy kinh điển: tên hàm tạo cảm giác an toàn. Trusted Types giới hạn được ai có quyền ghi vào HTML sink, nhưng một policy kiểu identity không làm cho chuỗi do kẻ tấn công kiểm soát trở nên an toàn.
Mắt xích 3: từ HTML đến thực thi JavaScript
Script chèn qua innerHTML không tự chạy, nên có HTML injection vẫn chưa có code execution. Nhóm nghiên cứu chèn một iframe dùng srcdoc. Thuộc tính này tạo một document mới nên script bên trong có thể thực thi. Và vì không có thuộc tính sandbox, frame này kế thừa origin chatgpt.com từ trang chính tạo ra nó, nghĩa là truy cập được trang cha.
Lớp cản tiếp theo là CSP. Inline script bị chặn đúng như mong đợi, nhưng www.youtube.com nằm trong danh sách cho phép.
Mắt xích 4: CSP thua vì một endpoint JSONP
Endpoint oEmbed của YouTube hỗ trợ tham số callback kiểu JSONP. Nó có kiểm tra tên callback không hợp lệ, nhưng lại đặt callback vào đầu response trước khi trả lỗi validation. Kết quả là một response có Content-Type: text/javascript, mở đầu bằng đoạn code do người gọi tự chọn, và được tải từ một domain mà CSP tin cậy.
Nói cách khác, allowlist theo domain chỉ an toàn bằng endpoint kém an toàn nhất trên domain đó. Nếu CSP của bạn còn đang liệt kê các domain lớn trong script-src, hãy cân nhắc chuyển sang nonce hoặc hash kết hợp strict-dynamic.
Tác giả cũng nhấn mạnh YouTube chỉ là bước vượt rào cuối, không phải nguyên nhân gốc. Chặn riêng endpoint đó vẫn để lại đường cho HTML không tin cậy đi vào trang chính.
Từ bằng chứng trong lab đến một link duy nhất
Phiên bản đầu tiên cần một ChatGPT App do nhà nghiên cứu kiểm soát, nên chưa thực tế. Thử dùng file HTML đính kèm thì khi xuất share, đồ thị lại biến mất, không còn ai cài listener. Bước đột phá là đặt cả hai vào cùng một hội thoại thông thường: đồ thị thật để nạp MathLive, và một HTML Preview phía sau để gửi tin nhắn.
Trong bản ổn định nhất, nạn nhân chỉ cần mở link và bấm Preview một lần. Khi đánh dấu khối HTML là previewable, Preview tự mở và việc truy cập link là đủ. Vì đồ thị và Preview tải độc lập nên có race condition, được xử lý đơn giản bằng cách gửi lại tin nhắn trong một khoảng thời gian.
Tác hại không dừng ở một hộp alert. Code chạy dưới origin chatgpt.com nên đọc được access token trong bootstrap của trang, gọi trực tiếp API backend để liệt kê hội thoại, đọc nội dung hội thoại riêng tư và gửi dữ liệu ra ngoài qua một request ảnh. Nhóm nghiên cứu chỉ gửi một mẩu nhỏ để chứng minh, nhưng về nguyên tắc mọi thứ frontend làm được thì payload cũng làm được.
Tự audit trên sản phẩm của bạn
Việc đầu tiên là liệt kê listener đang tồn tại ở runtime, bao gồm cả của thư viện. Trong Chrome DevTools Console có hàm tiện ích getEventListeners(window).message, và tab Sources cũng có mục Event Listener Breakpoints để dừng tại lúc xử lý message. Sau đó áp dụng mẫu sau cho mọi handler:
const ALLOWED_ORIGIN = "https://widget.example.com";
window.addEventListener("message", (event) => {
// Kiểm tra cả origin lẫn đúng cửa sổ đã được cấp phép
if (event.origin !== ALLOWED_ORIGIN) return;
if (event.source !== widgetFrame.contentWindow) return;
const msg = event.data;
if (msg?.type !== "resize" || typeof msg.height !== "number") return;
widgetFrame.style.height = `${msg.height}px`;
});
Danh sách kiểm tra rút ra từ bài gốc, kèm một số lưu ý thực tế:
- Xác thực cả
event.originvàevent.source. Chỉ kiểm tratypecủa tin nhắn không có giá trị bảo mật gì, vì format là công khai. - Tin nhắn qua cửa sổ không nên mang HTML thô. Nếu cần nội dung phong phú thì truyền dữ liệu có cấu trúc và tự render.
- Document tạo động mà nội dung có thể bị ảnh hưởng từ nguồn không tin cậy phải giữ
sandbox. Mộtiframe srcdockhông sandbox được coi như cùng origin với trang cha. - Tìm trong bundle các sink như
innerHTML, kể cả trongnode_modules, và xem dữ liệu đi vào từ đâu. - CSP là lớp phòng thủ dự phòng, không phải rào chắn duy nhất. Không dùng nó để biện minh cho một đường render không an toàn.
- Coi mọi bề mặt công khai hoặc chia sẻ được là input do kẻ tấn công kiểm soát, rồi kiểm thử theo tổ hợp tính năng thay vì từng tính năng.
Cộng đồng bàn gì
Trên Hacker News, phản ứng nhiều nhất xoay quanh mức thưởng 5.000 USD. Nhiều người cho rằng quá thấp với một lỗi P1 chiếm được tài khoản và lo ngại điều này đẩy người tìm lỗi sang thị trường khác. Một số khác thì ghi nhận cách bài viết xây dựng từng bước từ những kỹ thuật web rất cũ: postMessage, JSONP, iframe và thẻ ảnh. Quả thật, không có gì liên quan đến AI trong chuỗi này cả. Sản phẩm AI hiện đại vẫn có thể bị đánh bằng các lỗi web quen thuộc từ nhiều năm trước, và đó có lẽ là điều đáng nhớ nhất.
Bài viết được AI của DevViet viết lại bằng tiếng Việt từ một bài viết/thảo luận tiếng Anh trên Hacker News, có thể chưa chính xác tuyệt đối. Nguồn: Do the Math: How We Took Over ChatGPT Accounts