Unicode Normalization Bypass ใน ASP.NET Core

Unicode Normalization Bypass ใน ASP.NET Core
b
benzdeus
Mar 29, 2026·2 min read

เคยสงสัยไหมครับว่า... ทำไมเราติดตั้ง WAF (Web Application Firewall) ตัวละเป็นล้าน ตั้งกฎดัก Single Quote (') ไว้อย่างดี แต่สุดท้าย Hacker ก็ยังยิง SQL Injection เข้ามาที่ Database เราได้อยู่ดี?

วันนี้ผมจะพาไปดูช่องโหว่ที่เรียกว่า "Unicode Normalization Bypass" ซึ่งเป็นเคสที่ WAF กับ Application Logic ดันคุยกันคนละเรื่อง จนเกิดช่องว่างให้ผู้ร้ายแอบย่องเข้าบ้านเราครับ

รู้จักกับ "ตัวร้าย" ในคราบนักบุญ: Full-width Character

ในโลกของ Unicode มีอักขระที่หน้าตาเหมือนตัวอักษรภาษาอังกฤษเป๊ะ แต่จริงๆ แล้วมันคือคนละตัวกันครับ เราเรียกพวกมันว่า Full-width Forms

ประเด็นคือ: WAF ส่วนใหญ่ถูกสอนมาให้ดัก 0x27 (ASCII) แต่พอเจอ EF-BC-87 (Full-width) มันกลับมองว่าเป็น "ตัวอักษรภาษาแปลกๆ" ที่ไม่มีอันตราย แล้วก็ปล่อยให้ผ่านประตูไป...

เมื่อ Backend "ใจดี" เกินเหตุ

ความพังมันเริ่มตรงนี้ครับ... Programmer หลายคน (รวมถึงใน ASP.NET Core) มักจะใช้ฟังก์ชัน Normalization เพื่อล้างข้อมูลให้เป็นมาตรฐานเดียวกันก่อนบันทึกหรือประมวลผล เช่นการใช้:

Plain text
// ความหวังดี: "ฉันอยากเปลี่ยนตัวแปลกๆ ให้เป็นตัวปกติ จะได้ค้นหาข้อมูลง่ายๆ"
string cleanInput = userInput.Normalize(NormalizationForm.FormKC);

ฟังก์ชัน FormKC จะทำหน้าที่ "ยุบ" ตัว Unicode ที่หน้าตาคล้ายกันให้กลับมาเป็นตัวมาตรฐาน ผลที่ได้คือ:

(Full-width) —> ถูกแปลงเป็น —> ' (ASCII) ทันที!

Lab Time: ทดลองด้วย Docker (PoC)

Plain text
app.MapGet("/search", (string payload) => {
    // 1. รับค่ามา (ที่ WAF ปล่อยผ่านเพราะไม่มี 0x27)
    // 2. Normalize มันซะ! (จุดตาย)
    string normalized = payload.Normalize(NormalizationForm.FormKC); 

    // 3. ประกอบร่าง SQL (Sinks)
    string sqlQuery = $"SELECT * FROM Users WHERE ID = '{normalized}'";

    return Results.Json(new {
        input_from_waf = payload,
        after_normalize = normalized,
        final_sql = sqlQuery
    });
});

วิธีการ Bypass: ลองส่ง Payload นี้ไปที่แอปของเรา: admin' OR 1=1--

ผลลัพธ์ที่ได้: WAF จะปล่อยผ่าน เพราะมองไม่เห็นตัว ' แต่พอเข้าถึง Code ของเรา...

  1. payload = admin' OR 1=1--

  2. normalized = admin' OR 1=1-- (โดนแปลงร่างแล้ว!)

  3. sqlQuery = SELECT * FROM Users WHERE ID = 'admin' OR 1=1--'

ตูม! SQL Injection สำเร็จเรียบร้อย

สรุปบทเรียน: เราควรแก้ตรงไหน?

ความผิดนี้ไม่ใช่ของ WAF และก็ไม่ใช่ของฟังก์ชัน Normalize ครับ แต่มันคือเรื่องของ Context Mismatch (การประมวลผลข้อมูลไม่ตรงกันระหว่างชั้นความปลอดภัย)

วิธีแก้ที่ถูกต้อง:

  1. Parameterized Queries (Prepared Statements): ต่อให้คุณจะ Normalize จนได้ ' ออกมาหมื่นตัว ถ้าคุณใช้ Parameterized Query ตัว Database จะมองว่ามันคือ "ข้อมูล (Data)" ไม่ใช่ "คำสั่ง (Command)" เสมอ

    • Don't: ... WHERE ID = ' + input + '

    • Do: ... WHERE ID = @id (ส่ง input เข้าไปที่ @id)

  2. Input Validation: ตรวจสอบข้อมูลตั้งแต่ต้นทางว่ามีอักขระแปลกปลอมนอกเหนือจากที่อนุญาตหรือไม่

ทิ้งท้าย

โลกของ Cybersecurity สนุกตรงนี้แหละครับ บางครั้งช่องโหว่ไม่ได้เกิดจากความผิดพลาดของโค้ดที่เขียน "ผิด" แต่เกิดจากโค้ดที่เขียน "ดีเกินไป" จนลืมนึกถึงมุมที่ Hacker จะใช้ประโยชน์จากความใจดีนั้น

หวังว่า Use Case นี้จะเป็นประโยชน์กับเพื่อนๆ Dev และ Security ทุกคนนะครับ!

cybersecurityaspnetcoresqlinjectionunicodebypasswebdev