ขนมปังต้นฉบับส่งการ์ดข้อความผ่านตะแกรงอำพันก่อนวางลงในกรอบหน้าเว็บบนโต๊ะมิ้นต์

Bun.escapeHTML: หนีอักขระตอนส่งออก ไม่ใช่ใบอนุญาตให้เชื่อ HTML

พี่โชว์Engineering

การเอาชื่อผู้ใช้หรือข้อความจากฐานข้อมูลไปต่อใน HTML ตรง ๆ เปิดทางให้เครื่องหมายอย่าง <, > และ & เปลี่ยนจากข้อมูลเป็น markup Bun.escapeHTML แปลงอักขระพิเศษให้เป็น entity ที่เบราว์เซอร์แสดงเป็นข้อความ จึงเหมาะกับ output boundary ที่เรากำลังประกอบ HTML เองครับ

หลักคิดคือเก็บข้อมูลดิบไว้ตามจริง แล้ว escape ตามบริบทตอนส่งออก ไม่ควร escape ก่อนบันทึก เพราะข้อมูลเดียวกันอาจถูกนำไปใช้ใน JSON, CLI หรือ SQL ซึ่งมีกติกาคนละแบบ

const displayName = `<img src=x onerror="alert('x')">`;
const safeName = Bun.escapeHTML(displayName);
const page = `<p>Hello, ${safeName}</p>`;

if (page.includes("<img")) {
  throw new Error("raw markup crossed the output boundary");
}
if (!page.includes("&lt;img")) {
  throw new Error("expected escaped text");
}

console.log(page);

escaping ไม่เท่ากับ sanitization ถ้าผลิตภัณฑ์อนุญาตให้ผู้ใช้ส่ง rich text เช่น <strong> หรือ link แล้วต้องเก็บบาง tag ไว้ เราต้องใช้ sanitizer ที่ parse HTML และมี allowlist ชัดเจน การ escape จะทำให้ทุก tag กลายเป็นข้อความ ส่วนการกรองด้วย regex มักพลาดรูปแบบ encoding และโครงสร้างซ้อนกัน

บริบทก็สำคัญ ตัวอย่างนี้วางข้อมูลใน text node ของ HTML ถ้าจะใส่ใน JavaScript, CSS, URL หรือ attribute ต้องใช้ API และ encoder ที่เหมาะกับบริบทนั้น ทางที่ปลอดภัยกว่าใน browser คือสร้าง element แล้วกำหนด textContent; ส่วน template engine ส่วนใหญ่ escape ให้โดยปริยาย จึงไม่ควร escape ซ้ำจนผู้ใช้เห็น &amp;lt;

Recap

  • escape ข้อมูลตอนออกสู่ HTML ไม่ใช่ตอนรับเข้าฐานข้อมูล
  • แยก plain text escaping ออกจาก rich HTML sanitization
  • ใช้ encoder ตามบริบท ไม่เอาของ HTML ไปใช้กับ JavaScript หรือ URL
  • ตรวจว่า framework escape ให้อยู่แล้วก่อนเรียกซ้ำ