Skip to main content

ยิง API แล้วติด "คอ?" (CORS) คืออะไร ทำไมยิงไม่ได้?

ทำไมทั้งที่เขียนเองแท้ ๆ ทั้งหน้าบ้านหลังบ้าน แต่พอยิง API จากหน้าบ้านไปแล้วถึงติด”คอ” ได้?

“CORS” ตัวหนังสือสั้น ๆ 4 ตัวที่เป็นฝันร้ายของ Frontend Developer ที่คุยกับหลังบ้านไม่ได้ หรือที่ยิ่งเศร้ากว่าก็คือ Full-Stack Developer ที่แม้แต่ API ที่เขียนเองก็ยังยิงไม่ได้

พวกนี้มันเกิดจากอะไร ทำไมบางทีรันบนเครื่องไม่เป็น แต่เอาขึ้นไปแล้วเป็น? จะแก้ไขได้ต้องทำความเข้าใจก่อนว่า CORS คืออะไร แล้วมีไว้ทำไม?

CORS มีไว้ทำอะไร?

ก่อนจะรู้จัก CORS ต้องรู้จัก Same-Origin Policy ที่จะเป็นกลไกการรักษาความปลอดภัยในเบราว์เซอร์ของเราที่จะไม่ให้ส่ง HTTP Request ที่อยู่ต่าง Origin กัน

ต่าง Origin คือการที่โดเมน, พอร์ท หรือ Scheme* ไม่ตรงกัน ดังนั้น http://myweb.com และ http://api.myweb.com ก็ไม่ถือว่าเป็น Origin เดียวกัน

แต่ถ้า http://myweb.com กับ http://api.myweb.com คุยกันไม่ได้เว็บจะทำงานได้อย่างไร? ตรงนี้คือจุดที่ CORS หรือ Cross-Origin Resource Sharing เข้ามาช่วยให้ต่าง Origin คุยกันได้ โดยจะจำกัดว่ายอมให้ Origin ไหนเรียกใช้งานได้บ้าง

* Scheme คือการระบุโปรโตคอลที่จะใช้งานเช่น http:// ก็เป็นการระบุว่าจะใช้งาน HTTP


ถ้าไม่จำกัดโดเมนมันอันตรายขนาดไหน?

จากที่ได้กล่าวไปว่าถ้าต่าง Origin กันมาเรียกกันได้อาจไม่ปลอดภัยเพราะอะไร ส่วนนี้จะยกตัวอย่างด้วยการโจมตีแบบ Cross-Site Request Forgery หรือ CSRF

สมมุติว่าเว็บธนาคารที่เราเข้าใช้งานยอมให้ทุก Origin มาเรียกใช้งานได้ จะทำให้โดนโจมตีได้ดังนี้:

  1. เข้าสู่ระบบเพื่อเข้าไปใช้งานธนาคารผ่านเว็บตามปกติ
  2. ได้โทเคนมาเก็บไว้ในเบราว์เซอร์ เพื่อคงสถานะการเข้าสู่ระบบ
  3. ใช้งานเว็บธนาคารเสร็จก็ท่องเว็บไปเรื่อย แต่ดันเผลอไปเข้าเว็บที่ประสงค์ร้าย
  4. ในขณะที่เข้าชมเว็บนั้นอยู่ เบื้องหลังก็ส่ง Request ไปยังเว็บธนาคาร ซึ่งการที่เบราว์เซอร์เรามีโทเคนของธนาคารค้างไว้อยู่ ทำให้ Request นั้นเหมือนเราเป็นคนส่งออกไป ทำธุรกรรมแทนเราได้อย่างไม่รู้ตัว

เว็บนั้นอาจเป็นเว็บรวมรูปแมว, เว็บสูตรอาหาร หรืออื่น ๆ ที่ดูไม่มีพิษภัยอะไร แต่รู้ตัวอีกทีเงินก็อาจหมดบัญชีแล้วก็ได้ ดังนั้นนี่จึงเป็นเหตุผลว่าทำไมถึงต้องใช้ CORS มาจำกัดในส่วนนี้

หมายเหตุ - การตั้ง CORS ที่เหมาะสมไม่ได้ป้องกัน CSRF Attack ได้อย่าง 100% เพราะ CORS เป็นกลไกฝั่งเบราว์เซอร์เท่านั้น


CORS ทำงานอย่างไร?

มาถึงการทำงานของ CORS ว่ามันคุยกับเบราว์เซอร์อย่างไรถึงรู้ว่า Request ที่จะส่งไปให้คุยกันได้

สมมุติว่าหน้าบ้านอยากจะยิง API หลังบ้าน จะมีกิจกรรมเกิดในเบราว์เซอร์ตามลำดับนี้:

  1. ก่อน Request ตัวจริงจะส่งออกไป เบราว์เซอร์จะส่ง Preflight Request ออกไปตรวจสอบ CORS เพื่อขอรายการ Origin ที่อนุญาตจาก API หลังบ้านก่อน
  2. API หลังบ้านส่งรายการ Origin ที่อนุญาตกลับมาให้เบราว์เซอร์
  3. เบราว์เซอร์ตรวจสอบว่า Origin ของหน้าเว็บปัจจุบันที่เราเข้าอยู่ อยู่ในรายการที่ได้รับมาไหม ถ้าไม่อยู่ก็จะไม่ยอมส่ง Request จริงให้
  4. (ถ้า Origin ได้รับอนุญาต) ส่ง Request ตัวจริงออกไป
  5. ได้รับ Response ตัวจริงกลับมา เป็นอันเสร็จสิ้นการทำงาน

แต่การที่เรามักจะติด CORS กันนั้น มักจะเป็นที่ฝั่ง API ตั้งค่าไม่ถูก ไม่ยอมให้หน้าบ้านเรียก ไม่อยู่ในรายการที่ได้รับในขั้นตอนที่ 2 ทำให้ไม่ผ่านการตรวจสอบแล้วก็ Error กันไป


แก้อย่างไร: ถ้าเป็นตอนรันบนเครื่องเรา?

เจอปัญหานี้ตองลองรันในเครื่องแก้อย่างไรได้บ้าง? เพราะถึงจะรันบน localhost เหมือนกันแต่อย่าลืมว่าพอร์ทต่างกันก็ไม่ถือว่าเป็น Origin เดียวกันแล้ว

วิธีที่ 1 - ตั้งให้รับ Request จากทุก Origin ไปเลย

ถ้าเบราว์เซอร์เห็น Access-Control-Allow-Origin: * ส่งมาจาก API ก็จะยอมให้เราผ่านทันที เพราะว่าแบบนี้เป็นการบอกว่า ไม่ว่าใครหน้าไหนก็มาเรียกได้หมด

แต่ต้องระวังว่าอย่าลืมเอาออก หรือเปิดใช้งานเฉพาะตอนลองรันบนเครื่องเท่านั้น เพราะถ้าหลุดไป Production คงไม่ต้องคิดว่าจะเกิดอะไรขึ้น ถ้าใคร ๆ ก็มาเรียก API เราได้

วิธีที่ 2 - ตั้งค่า Proxy ให้กับโปรเจคหน้าบ้าน

บางไลบราลีและเฟรมเวิร์คจะมีการตั้งค่า Proxy อยู่ในตัวเลย ซึ่งจะทำให้อย่างเช่น Frontend เปิดอยู่ที่ localhost:3000และ API ที่ localhost:4000 สามารถตั้งให้ API ถูกเรียกจาก localhost:3000/api ได้ เท่านี้ตัวเบราว์เซอร์ก็จะมองว่าเป็น Origin เดียวกันแล้ว

รู้หรือไม่?

ทำไมเราลองยิงบน Postman แล้วมันผ่านตลอดเลยละ?

คำตอบคือเพราะ Postman ไม่ใช่ Browser แต่เป็นแค่เครื่องมือเอาไว้ลอง API เท่านั้น เลยไม่ได้ทำตาม CORS Policy ที่จะต้องมาตรวจ Origin ก่อน


แก้อย่างไร: ถ้าเป็นขึ้นไปแล้วเป็น?

มาที่ฝั่งเวลาเอาขึ้นไปแล้วบ้าง ขึ้นไปแล้วใช้ไม่ได้ปวดหัวหนักกว่าตอนลองบนเครื่องอึก เพราะไม่รู้จะไปตามดูตรงไหน!

วิธีที่ 1 - ตั้งให้ CORS รับ Request จากโดเมน Frontend

เหมือนกับตอนแก้ตอนรันบนเครื่อง แต่จำกัดมาหน่อยว่าเฉพาะโดเมนเว็บของเรานะที่ให้เรียกได้

วิธีที่ 2 - ตรวจสอบการตั้งค่าในบริการคลาวด์

เอาขึ้นไปบนคลาวด์บางครั้งมีบริการเกี่ยวข้องหลายบริการ และบางบริการก็อาจมีการตั้ง CORS ของตัวมันเองอีกด้วย

จากประสบการณ์ส่วนตัวในการใช้ Microsoft Azure ถ้าเปิด API ผ่าน App Service แล้วครอบด้วย API Management อีกที จะต้องไปตั้งค่า CORS ในทั้งสองตัวนั้นเลยให้ตรงกันโดเมนของเรา โดยเฉพาะตัว API Management ที่อย่างฉลาด ถ้า CORS ไม่ผ่านก็ตัดตอนไม่ยอมไปหา App Service ให้เราเลย

วิธีที่ 3 - ตั้งค่า Proxy ให้ API และ Frontend อยู่ใต้โดเมนเดียวกัน

ตรงนี้จะไม่ใช่การตั้งค่าผ่านไลบราลีและเฟรมเวิร์คเหมือนตอนลองรันในเครื่องแล้ว แต่ว่าจะเป็นการหาเซิร์ฟเวอร์หรือบริการอื่น ๆ เช่น Load Balancer, Reverse Proxy, Application Gateway, ฯลฯ มาเป็นทางเข้าก่อนพาไปยัง API และเว็บเราอีกที

โดยวิธีนี้ผลก็คล้ายเดิมก็คือเบราว์เซอร์เห็นว่า Frontend กับ API อยู่ใต้ Origin เดียวกัน คุยกันได้อย่างไม่มีปัญหา


เนื้อหานี้ถูกเผยแพร่ครั้งแรกในรูปแบบของโพสท์ Facebook เพื่อให้ง่ายต่อการสืบค้นจึงนำมาจัดเก็บในรูปแบบบทความด้วย