Menu

Monday, September 21, 2026

Express.js trust proxy: ตั้งค่าผิดนิดเดียว เสี่ยงโดน IP Spoofing ไม่รู้ตัว

ทำไม Express.js ถึงอ่าน IP ฝั่ง Client ผิดพลาดเมื่ออยู่หลัง Proxy?

เมื่อเรานำ Express.js แอปพลิเคชันไปรันอยู่หลัง Reverse Proxy หรือ Load Balancer (เช่น Nginx, HAProxy, AWS ALB หรือ Cloudflare) Connection ที่วิ่งเข้าหา Node.js โดยตรงจะมาจาก IP ของ Proxy เอง ไม่ใช่ IP ของผู้ใช้งานจริง (Client) ส่งผลให้ค่า req.ip คืนค่าเป็น IP ของ Proxy เสมอ

เพื่อแก้ปัญหานี้ Reverse Proxy จะแนบ HTTP Header เพิ่มเติมเพื่อบอก IP ต้นทาง ได้แก่:

  • X-Forwarded-For: รายการ IP Address ที่ส่งต่อกันมาเป็นทอดๆ (รูปแบบ: client, proxy1, proxy2)
  • X-Forwarded-Proto: โปรโตคอลที่ Client ใช้เชื่อมต่อเข้ามาจริง (http หรือ https)
  • X-Forwarded-Host: Host Header ดั้งเดิมที่ Client เรียกใช้

การตั้งค่า app.set('trust proxy', value) คือการบอกให้ Express ทราบว่าจะยอมรับและนำค่าจาก Header เหล่านี้มาใช้อัปเดต Property เช่น req.ip, req.ips, req.protocol และ req.secure หรือไม่


ความหมายและพฤติกรรมของค่าแต่ละประเภท

Express.js รองรับการกำหนดค่า trust proxy ได้หลายรูปแบบตามความต้องการด้านความปลอดภัยและสถาปัตยกรรมของระบบ:

  • false (ค่าเริ่มต้น): ปิดการใช้งาน ไม่เชื่อถือ Header ตระกูล X-Forwarded-* ใดๆ ทั้งสิ้น ค่า req.ip จะดึงมาจากความสัมพันธ์ของ socket โดยตรง
  • true: เชื่อถือ Proxy ทุกตัวที่ส่งผ่านเข้ามา ค่า req.ip จะเลือกหยิบ IP ตัวแรกสุด (ซ้ายสุด) ใน X-Forwarded-For มาใช้ทันที (เสี่ยงต่อการถูกปลอมแปลง IP สูงมาก)
  • Number (เช่น 1, 2): ระบุจำนวน Hop ที่ไว้วางใจ โดย Express จะนับย้อนหลังจากขวาไปซ้ายเพื่อตัด IP ออกตามจำนวน Hop ที่กำหนด
  • String หรือ Array: ระบุ IP Address, Subnet หรือใช้ Pre-defined Keywords เช่น:
    • 'loopback' (เช่น 127.0.0.1/8, ::1/128)
    • 'linklocal' (เช่น 169.254.0.0/16)
    • 'unroutable' (เช่น Private IP Ranges เช่น 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
    • ระบุ IP / Subnet โดยตรง เช่น '10.0.0.1' หรือใช้แบบหลายค่า 'loopback, 10.0.0.0/8'
  • Function: Custom function สำหรับรับค่า IP เข้ามาตรวจสอบความถูกต้องเอง เช่น (addr, i) => i < 2 || addr === '127.0.0.1'

ทำไมการตั้งค่าตัวเลข Hop (Number) ถึงไม่ปลอดภัยเท่าที่คิด?

หลายคนเข้าใจผิดว่าการกำหนดตัวเลข เช่น app.set('trust proxy', 1) เป็นการยืนยันตัวตนของ IP แต่ในสถาปัตยกรรมของ Express (ซึ่งใช้แพ็กเกจ proxy-addr) กระบวนการทำงานเป็นเพียงการตัดตำแหน่งใน Array ทื่อๆ เท่านั้น

สมมติว่ามี Header ส่งมาเป็น: X-Forwarded-For: [IP-A], [IP-B], [IP-C] (โดย IP-C คือ Proxy ตัวสุดท้ายก่อนถึง Express)

  1. นับตำแหน่งตามสถิติ: ถ้ากำหนด trust proxy = 1 Express จะนับย้อนหลังจากขวา ตัด IP-C ออก 1 ตำแหน่ง แล้วหยิบ IP-B ไปใช้งานทันที โดยไม่ได้ตรวจสอบว่า IP-B มีอยู่จริงหรือเป็นของใคร
  2. เปราะบางเมื่อ Infrastructure เปลี่ยน: หากระบบถูกปรับโครงสร้าง (เช่น เพิ่ม Cloudflare, WAF หรือ Load Balancer เข้าไปหน้า Nginx) จำนวน Hop จริงจะเปลี่ยนจาก 1 เป็น 2 ทันที หากไม่ได้แก้อัปเดตโค้ด Express จะเลือกหยิบ IP ของ Proxy เราเอง หรือ IP ปลอมที่ถูกสปูฟเข้ามา
  3. ป้องกันการยิงอ้อม (Bypass Proxy) ไม่ได้: หากผู้ไม่หวังดีสแกนเจอ IP เครื่อง Server ที่รัน Express อยู่ตรงๆ แล้วส่ง Request ข้าม Reverse Proxy พร้อมแปะ Header ปลอม X-Forwarded-For: 1.2.3.4, 5.6.7.8 Express ก็ยังคงตัด 1 Hop ฝั่งขวาออกแล้วหลงเชื่อ IP ปลอมทางซ้ายอยู่ดี

เปรียบเทียบระดับความปลอดภัยตามรูปแบบการคอนฟิก

การกำหนดค่า (Value) กลไกการเลือก IP ระดับความปลอดภัย ข้อควรระวัง
false ใช้ IP จาก TCP Connection โดยตรง สูง (หากรันตรง ไม่ผ่าน Proxy) อ่าน IP จริงของผู้ใช้ไม่ได้หากอยู่หลัง Proxy
true เลือก IP ตัวซ้ายสุดเสมอ ไม่ปลอดภัย (Unsafe) ผู้ใช้ฉีด IP ปลอมเข้ามาหลอกระบบได้ง่าย
Number (เช่น 1) นับตัดตำแหน่งขวาไปซ้ายตามจำนวนตัวเลข ปานกลาง - ต่ำ
IP / Subnet / Pre-defined String ตรวจสอบย้อนกลับจนกว่าจะเจอ IP นอกเขตที่ระบุ ปลอดภัยที่สุด (Recommended) ต้องระบุ IP Range ของ Proxy ให้ถูกต้องครบถ้วน

แนวทางการตั้งค่าให้ปลอดภัยตามหลัก Best Practice

  1. จัดการ Sanitization ที่ Reverse Proxy หน้าสุด: ต้องตั้งค่า Nginx หรือ Edge Proxy ให้ทำการ Overwrite Header X-Forwarded-For ด้วย IP ที่ได้จาก TCP Connection เสมอ เพื่อล้าง IP ปลอมที่ผู้ใช้แนบมาจากภายนอก
    proxy_set_header X-Forwarded-For $remote_addr;
  2. ระบุ IP/Subnet ใน Express อย่างเจาะจง: ใช้การระบุ IP หรือ Subnet ของ Proxy แทนการระบุตัวเลข Hop
    // ระบุเฉพาะวง IP ของ Internal Proxy
    app.set('trust proxy', '10.0.0.0/8');
    
    // หรือใช้ Keywords ในการระบุ
    app.set('trust proxy', 'loopback, unroutable');

Security Takeaway: การระบุ IP หรือ Subnet แบบชัดเจนจะช่วยให้ Express ไล่ตรวจสอบ IP ย้อนหลังจากขวาไปซ้าย และจะหยุดยอมรับทันทีที่เจอ IP แรกที่ไม่ใช่วง Proxy ของเรา ทำให้ได้ IP ที่แท้จริงของ Client และป้องกันการสปูฟ IP เพื่อ Bypass ระบบ Rate Limiting หรือ Authentication ได้อย่างรวดเร็ว

No comments:

Post a Comment