Token lộ trong thông báo lỗi: SDK Node mặc định gửi đi những gì
Mình cài một bí mật giả vào 14 lỗi Node.js thật rồi báo bằng SDK Node của Sentry, để nguyên tuỳ chọn mặc định. 13/15 bí mật đã rời khỏi tiến trình, qua message, breadcrumb hoặc các dòng mã nguồn.
Andrew Babo7 phút đọc
Trong bài này

API key, token và URL webhook vẫn lọt vào message của exception, và tuỳ chọn riêng tư mặc định của công cụ theo dõi lỗi không gỡ chúng ra. sendDefaultPii: false chỉ quản dữ liệu request như cookie, header, địa chỉ IP. Nó không đọc chữ trong lỗi.
Mình thử ngày 26/10/2026 với 14 lỗi Node.js thật. Mỗi lỗi mang một bí mật giả đúng định dạng ngoài đời, được báo bằng SDK Node chính thức của Sentry (@sentry/node 10.76.2) với tuỳ chọn mặc định. 13 trong 15 bí mật đã rời khỏi tiến trình: 12 cái nguyên vẹn, một cái lộ 10 ký tự đầu. Sau đó cho từng event chạy qua hai bộ lọc của Belvador và tìm ra những dạng chúng bỏ sót.
Bí mật đến từ đâu#
Có lỗi do chính Node viết ra, có lỗi đến từ kiểu mã ứng dụng ai cũng từng viết:
TypeError: Request cannot be constructed from a URL that includes credentials: https://lab:Pa55-word-91@api.example.test/v1/ordersError: Command failed: curl -sf -H "Authorization: token ghp_LabTok3n…" http://127.0.0.1:9/reposError: GET https://api.example.test/v1/orders?api_key=lab_key_8f3a91c2d4e5&page=2 returned 401Error: S3 client config invalid: {"region":"eu-central-1","accessKeyId":"AKIALABEXAMPLE7Q2W3E",…}SyntaxError: Unexpected token 's', "sk_live_51"... is not valid JSONKhi từ chối một URL có mật khẩu, fetch của Node trích nguyên URL, mật khẩu đi kèm. execSync đưa cả dòng lệnh vào message. Token truyền qua header của curl vì thế cũng đi theo. JSON.parse của V8 trích 10 ký tự đầu của chuỗi đầu vào, đủ để nhận ra một khoá Stripe live. Phần còn lại là mã ứng dụng: URL request có tham số api_key, object request bị stringify, chuỗi kết nối database, bản dump cấu hình, URL webhook, khoá OpenAI.

| Bí mật nằm ở đâu | @sentry/node có gửi không |
|---|---|
Message: 9 bí mật trong 8 lỗi (mật khẩu trong URL, lệnh curl, api_key, request dạng JSON, chuỗi kết nối, webhook, cặp khoá AWS, khoá OpenAI) | có, nguyên vẹn |
Breadcrumb từ console.log một URL có ?token= | có, trong message và tham số |
Mã nguồn quanh frame: khoá viết cứng ngay dòng trên throw (2 ca) | có, trong các dòng SDK gửi kèm mỗi frame |
Lỗi JSON.parse trên khoá Stripe | 10 ký tự đầu |
error.input của new URL() | không |
error.config.headers của lỗi kiểu axios | không |
Ba đường lộ trong bảng rất dễ quên. SDK Node mặc định ghi các lệnh console thành breadcrumb, nên dòng log debug ngay trước lỗi sẽ đi cùng lỗi. SDK cũng gửi vài dòng mã nguồn quanh mỗi frame của stack. Vì vậy khoá viết cứng cạnh throw vẫn nằm trong event, dù message không nhắc tới. Hai bí mật không rời máy là vì nằm trong thuộc tính của lỗi, không nằm trong message. SDK chỉ gửi các thuộc tính này khi bạn bật integration dữ liệu lỗi mở rộng, phần mình chưa thử ở đây.
sendDefaultPii thật ra lo phần nào#
Hướng dẫn quản lý dữ liệu của Sentry viết rõ: mặc định của SDK nhắm vào dữ liệu cá nhân trong ngữ cảnh request, còn lọc mọi thứ khác là việc của bạn, trong beforeSend hoặc ở phía máy chủ. Thí nghiệm chỉ xác nhận lại điều đó: không bí mật nào trong 13 cái kia bị SDK đụng tới, vì không cái nào nằm trong cookie, header hay trường user.
Đây không phải lỗi riêng của một SDK. Message của exception là chữ tự do do mã của bạn và thư viện viết ra. SDK không thể biết đoạn nào là bí mật nếu không đoán, mà đoán thì có giá: bài lọc PII cho thấy những trường vô hại bị mất vì luật nghiêng về phía lọc mạnh tay.
Hai bộ lọc, và những dạng bị bỏ sót#
Belvador lọc hai lần: một lần trong các SDK @belvador/* trước khi event rời ứng dụng, một lần trên node trước khi lưu bất cứ thứ gì, dù event do SDK nào gửi. Hai bộ lọc được viết riêng, chỉ dùng chung bộ ca kiểm thử. Mình cho từng event thô chạy qua cả hai.

Với bộ luật phát hành trước hôm đó (SDK 0.5.1, node 0.1.30), hai bộ lọc bắt hết những bí mật có dạng chúng đã biết: mật khẩu trong URL, tham số api_key= và token=, JWT trong request dạng JSON, phần user/mật khẩu của postgres://, trường secretAccessKey, khoá Stripe viết cứng trong mã nguồn. Năm cái vẫn đọc được sau cả hai lớp:
- token GitHub sau
Authorization: token, vì chỉBearervàBasiclà scheme đã biết; - AWS access key ID (
AKIA…), vì tên trườngaccessKeyIdkhông nằm trong danh sách tên nhạy cảm; - khoá OpenAI dạng
sk-proj-; - URL webhook Slack, hai lần, trong message và trong mã nguồn.
Cả năm đều có tiền tố được nhà cung cấp công bố, dễ nhận ra. GitHub chẳng hạn công bố tiền tố token (ghp_, gho_, github_pat_ và các loại khác) chính là để công cụ tìm được chúng. Nên mình thêm luật cho khoá có tiền tố nhà cung cấp, cho đường dẫn bí mật của webhook Slack, Discord và URL bot Telegram. Luật mới cũng bắt dòng Authorization: <scheme> <giá trị> trong văn bản. Đi kèm là 16 ca kiểm thử mới và 4 ca trông giống mà phải giữ nguyên. Sau bản phát hành cùng ngày (SDK 0.5.2, node 0.1.31), cùng 15 bí mật đó không còn cái nào đọc được sau bất kỳ lớp nào.
10 ký tự đầu từ JSON.parse thì vẫn còn. Chúng cho biết đây là khoá Stripe live nhưng bản thân không phải khoá, và không luật nào gỡ được một mẩu như vậy mà không gỡ luôn chữ bình thường.
Nên làm gì trong mã của mình#
- Đừng đưa bí mật vào message. Ghi host và mã trạng thái, đừng ghi URL kèm query. Ném
new Error("orders API returned 401")rồi gắn các trường an toàn riêng. - Đừng stringify nguyên object vào lỗi. Object request và cấu hình là chỗ khoá hay nằm.
- Truyền token qua biến môi trường, đừng qua dòng lệnh, để
execSynclỗi không trích ra được. - Lọc trong
beforeSendtheo đúng các dạng ứng dụng của bạn dùng, rồi kiểm tra: ném một lỗi chứa khoá giả và xem event nhận được. - Kiểm cả breadcrumb và dòng mã nguồn, không chỉ message. Tắt breadcrumb từ console nếu log của bạn có token.
- Đổi khoá (rotate) nếu thấy nó trong event. Xoá khỏi công cụ theo dõi lỗi không xoá được bản đã chép.
Belvador nằm ở đâu#
Trang bảo vệ dữ liệu của Belvador liệt kê mọi luật cả hai lớp áp dụng, gồm các khoá có tiền tố nhà cung cấp mới thêm trong SDK 0.5.2 và node 0.1.31. Trên máy chủ Node.js, Belvador dùng SDK của chính Sentry kèm một hook lọc, như hướng dẫn Node mô tả. Vì vậy bộ lọc của node là lớp mọi event đều phải đi qua, trên một máy chủ nằm trong project Hetzner của chính bạn. Trang bảo mật nói về các lớp kiểm soát khác. Nếu sau đó event được đưa cho công cụ AI, bài gửi lỗi production cho LLM an toàn nói tiếp bước đó.
Đọc tiếp#
Thuộc bài hướng dẫn: Gửi lỗi production cho LLM thế nào cho an toàn
Bài liên quan
Mọi bài viết
Hướng dẫn
Gửi lỗi production cho LLM thế nào cho an toàn
Một event lỗi chứa secret, dữ liệu cá nhân và chữ người dùng gõ vào. Mình gửi một event có lệnh ẩn cho bốn model 72 lần; dán nguyên văn, một model làm theo cả sáu lần và có lần khuyên tắt xác thực.
Andrew Babo8 phút đọc

Hướng dẫn
Lưu trữ dữ liệu lỗi và nơi nhà cung cấp AI xử lý: hai lựa chọn riêng
Giữ dữ liệu lỗi ở EU không nói gì về nơi một model AI xử lý cái lỗi bạn dán vào. Mình so cài đặt của bốn nhà cung cấp AI và thấy nơi lưu và nơi xử lý là hai nút vặn khác nhau.
Andrew Babo6 phút đọc
Đọc tiếp trong tài liệu
Xem lỗi production của chính bạn như thế này
Belvador chạy trên máy chủ Hetzner của chính bạn: lỗi, cron, uptime và trace — dữ liệu vẫn là của bạn.