วิธีเปิดใช้งาน CORS ด้วย HTTPOnly Cookie เพื่อรักษาความปลอดภัยโทเค็น

เผยแพร่แล้ว: 2021-12-29

ในบทความนี้ เราจะเห็นวิธีเปิดใช้งาน CORS (การแชร์ทรัพยากรแบบข้ามแหล่งที่มา) ด้วยคุกกี้ HTTPOnly เพื่อรักษาความปลอดภัยโทเค็นการเข้าถึงของเรา

ปัจจุบัน เซิร์ฟเวอร์แบ็กเอนด์และไคลเอ็นต์ฟรอนต์เอนด์ถูกปรับใช้บนโดเมนต่างๆ ดังนั้นเซิร์ฟเวอร์จึงต้องเปิดใช้งาน CORS เพื่อให้ไคลเอนต์สามารถสื่อสารกับเซิร์ฟเวอร์บนเบราว์เซอร์ได้

นอกจากนี้ เซิร์ฟเวอร์กำลังใช้การพิสูจน์ตัวตนแบบไร้สัญชาติเพื่อความสามารถในการปรับขนาดที่ดีขึ้น โทเค็นจะถูกจัดเก็บและบำรุงรักษาที่ฝั่งไคลเอ็นต์ แต่ไม่ใช่ในฝั่งเซิร์ฟเวอร์เหมือนเซสชัน เพื่อความปลอดภัย การจัดเก็บโทเค็นในคุกกี้ HTTPOnly จะดีกว่า

เหตุใดคำขอข้ามแหล่งที่มาจึงถูกบล็อก

สมมติว่าแอปพลิเคชันส่วนหน้าของเราใช้งานได้ที่ https://app.geekflare.com สคริปต์ที่โหลดใน https://app.geekflare.com สามารถขอได้เฉพาะทรัพยากรที่มีต้นกำเนิดเดียวกันเท่านั้น

เมื่อใดก็ตามที่เราพยายามส่งคำขอข้ามต้นทางไปยังโดเมนอื่น https://api.geekflare.com หรือพอร์ตอื่น https://app.geekflare.com:3000 หรือรูปแบบอื่น http://app.geekflare.com คำขอข้ามที่มาจะถูกบล็อกโดยเบราว์เซอร์

แต่ทำไมคำขอเดียวกันที่ถูกบล็อกโดยเบราว์เซอร์ถูกส่งจากเซิร์ฟเวอร์แบ็กเอนด์โดยใช้คำขอ curl หรือส่งโดยใช้เครื่องมือเช่นบุรุษไปรษณีย์โดยไม่มีปัญหา CORS จริงๆ แล้วมีไว้เพื่อความปลอดภัยในการปกป้องผู้ใช้จากการโจมตี เช่น CSRF(Cross-Site Request Forgery)

มาดูตัวอย่างกัน สมมติว่าผู้ใช้รายใดลงชื่อเข้าใช้บัญชี PayPal ของตนเองในเบราว์เซอร์ หากเราสามารถส่งคำขอข้ามที่มาไปยัง paypal.com จากสคริปต์ที่โหลดบนโดเมนอื่นที่ malicious.com com โดยไม่มีข้อผิดพลาด/การบล็อก CORS เหมือนกับที่เราส่งคำขอต้นทางเดียวกัน

ผู้โจมตีสามารถส่งหน้า https://malicious.com/transfer-money-to-attacker-account-from-user-paypal-account อันตรายได้อย่างง่ายดาย โดยแปลงเป็น URL แบบสั้นเพื่อซ่อน URL จริง เมื่อผู้ใช้คลิกลิงก์ที่เป็นอันตราย สคริปต์ที่โหลดในโดเมน malicious.com จะส่งคำขอข้ามที่มาไปยัง PayPal เพื่อโอนจำนวนผู้ใช้ไปยังบัญชี PayPal ผู้โจมตีจะถูกดำเนินการ ผู้ใช้ทุกคนที่ลงชื่อเข้าใช้บัญชี PayPal และคลิกลิงก์ที่เป็นอันตรายนี้จะเสียเงิน ใครๆ ก็ขโมยเงินได้ง่ายๆ โดยที่ผู้ใช้บัญชี PayPal ไม่รู้

ด้วยเหตุผลข้างต้น เบราว์เซอร์จึงบล็อกคำขอข้ามต้นทางทั้งหมด

CORS (การแชร์ทรัพยากรแบบข้ามที่มา) คืออะไร

CORS เป็นกลไกการรักษาความปลอดภัยตามส่วนหัวที่ใช้โดยเซิร์ฟเวอร์เพื่อบอกให้เบราว์เซอร์ส่งคำขอข้ามต้นทางจากโดเมนที่เชื่อถือได้
เซิร์ฟเวอร์ที่เปิดใช้งานด้วยส่วนหัว CORS ที่ใช้เพื่อหลีกเลี่ยงคำขอข้ามต้นทางที่ถูกบล็อกโดยเบราว์เซอร์

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

เนื่องจากเซิร์ฟเวอร์ได้กำหนดโดเมนที่เชื่อถือได้ในการกำหนดค่า CORS แล้ว เมื่อเราส่งคำขอไปยังเซิร์ฟเวอร์ การตอบกลับจะบอกเบราว์เซอร์ว่าโดเมนที่ร้องขอนั้นเชื่อถือได้หรือไม่อยู่ในส่วนหัว

คำขอ CORS สองประเภทมีอยู่:

  • ของ่ายๆ
  • คำขอเที่ยวบินล่วงหน้า

คำของ่ายๆ:

ขั้นตอนการร้องของ่าย ๆ ของ CORS บอกว่าจะส่งคำขอข้ามต้นทาง แต่เมื่อได้รับการตอบสนอง มันตรวจสอบส่วนหัว

  • เบราว์เซอร์ส่งคำขอไปยังโดเมนข้ามต้นทางที่มี ต้นทาง (https://app.geekflare.com)
  • เซิร์ฟเวอร์ส่งการตอบกลับที่เกี่ยวข้องกลับด้วย วิธีการที่ อนุญาต และ ต้นทางที่อนุญาต
  • หลังจากได้รับคำขอแล้วเบราว์เซอร์จะตรวจสอบค่าส่วนหัวต้นทางที่ส่ง ( https://app.geekflare.com ) และรับค่า access-control-allow-origin ( https://app.geekflare.com ) ว่าเหมือนกันหรือ สัญลักษณ์แทน(*). มิฉะนั้น มันจะส่งข้อผิดพลาด CORS

คำขอเที่ยวบินล่วงหน้า:

CORS-Preflight Request Image which show the flow of cross-origin request with OPTIONS preflight request before sending actual request for verifying headers.

  • ขึ้นอยู่กับพารามิเตอร์คำขอที่กำหนดเองจากคำขอข้ามต้นทางเช่นเมธอด (PUT, DELETE) หรือส่วนหัวที่กำหนดเองหรือประเภทเนื้อหาอื่น ฯลฯ เบราว์เซอร์จะตัดสินใจส่งคำขอตัวเลือก preflight เพื่อตรวจสอบว่าคำขอจริงปลอดภัยหรือไม่ หรือไม่.
  • หลังจากได้รับการตอบสนอง (รหัสสถานะ: 204 ซึ่งหมายถึงไม่มีเนื้อหา) เบราว์เซอร์จะตรวจสอบพารามิเตอร์การ อนุญาตการควบคุมการเข้าถึง สำหรับคำขอจริง หากเซิร์ฟเวอร์อนุญาตพารามิเตอร์คำขอ คำขอข้ามที่มาที่แท้จริงส่งและรับ

หาก access-control-allow-origin: * อนุญาตการตอบกลับสำหรับ access-control-allow-origin: * ทั้งหมด แต่มันไม่ปลอดภัยเว้นแต่คุณต้องการ

วิธีเปิดใช้งาน CORS

ในการเปิดใช้งาน CORS สำหรับโดเมนใดๆ ให้เปิดใช้งานส่วนหัว CORS เพื่ออนุญาตที่มา วิธีการ ส่วนหัวที่กำหนดเอง ข้อมูลประจำตัว ฯลฯ

เบราว์เซอร์อ่านส่วนหัว CORS จากเซิร์ฟเวอร์และอนุญาตคำขอจริงจากไคลเอนต์หลังจากตรวจสอบพารามิเตอร์คำขอแล้วเท่านั้น

  • Access-Control-Allow-Origin: เพื่อระบุโดเมนที่แน่นอน (https://app.geekflate.com, https://lab.geekflare.com) หรือ wildcard(*)
  • Access-Control-Allow-Methods: เพื่ออนุญาตวิธี HTTP (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) ที่เราต้องการเท่านั้น
  • Access-Control-Allow-Headers: เพื่ออนุญาตเฉพาะส่วนหัว (Authorization, csrf-token)
  • Access-Control-Allow-Credentials: ค่าบูลีนที่ใช้เพื่ออนุญาต cross-origin-credentials (คุกกี้, ส่วนหัวการให้สิทธิ์)
  • Access-Control-Max-Age: บอกให้เบราว์เซอร์แคชการตอบสนอง preflight ในบางครั้ง
  • Access-Control-Expose-Headers: ระบุส่วนหัวที่สามารถเข้าถึงได้โดยสคริปต์ฝั่งไคลเอ็นต์

สำหรับการเปิดใช้งาน CORS ใน apache และเว็บเซิร์ฟเวอร์ Nginx ให้ทำตามบทช่วยสอนนี้

เปิดใช้งาน CORS ใน ExpressJS

มาดูตัวอย่างแอป ExpressJS ที่ไม่มี CORS:

 const express = require('express'); const app = express() app.get('/users', function (req, res, next) { res.json({msg: 'user get'}) }); app.post('/users', function (req, res, next) { res.json({msg: 'user create'}) }); app.put('/users', function (req, res, next) { res.json({msg: 'User update'}) }); app.listen(80, function () { console.log('CORS-enabled web server listening on port 80') })

ในตัวอย่างข้างต้น เราได้เปิดใช้งานปลายทาง API ของผู้ใช้สำหรับเมธอด POST, PUT, GET แต่ไม่ใช่เมธอด DELETE

เพื่อให้ง่ายต่อการเปิดใช้งาน CORS ในแอป ExpressJS คุณสามารถติดตั้ง cors

 npm install cors

การเข้าถึง-การควบคุม-อนุญาต-แหล่งกำเนิด

เปิดใช้งาน CORS สำหรับโดเมนทั้งหมด

 app.use(cors({ origin: '*' }));

เปิดใช้งาน CORS สำหรับโดเมนเดียว

 app.use(cors({ origin: 'https://app.geekflare.com' }));

หากคุณต้องการอนุญาต CORS สำหรับแหล่งกำเนิด https://app.geekflare.com และ https://lab.geekflare.com

 app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ] }));

การเข้าถึง-ควบคุม-อนุญาต-วิธีการ

สำหรับการเปิดใช้งาน CORS สำหรับวิธีการทั้งหมด ให้ละเว้นตัวเลือกนี้ในโมดูล CORS ใน ExpressJS แต่สำหรับการเปิดใช้งานวิธีการเฉพาะ (GET, POST, PUT)

 app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'] }));

Access-Control-Allow-Headers

ใช้เพื่ออนุญาตให้ส่วนหัวอื่นที่ไม่ใช่ค่าเริ่มต้นส่งพร้อมกับคำขอจริง

 app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'] }));

Access-Control-Allow-Credentials

ละเว้นนี้ถ้าคุณไม่ต้องการที่จะบอกเบราว์เซอร์เพื่อให้ข้อมูลประจำตัวตามคำขอแม้ใน withCredentials กำหนดเป็นจริง

 app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true }));

Access-Control-Max-Age

เพื่อใกล้ชิดกับเบราว์เซอร์เพื่อแคชข้อมูลการตอบสนองของ preflight ในแคชสำหรับวินาทีที่ระบุ ข้ามสิ่งนี้หากคุณไม่ต้องการแคชการตอบกลับ

 app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600 }));

การตอบสนอง preflight ที่แคชไว้จะใช้งานได้เป็นเวลา 10 นาทีในเบราว์เซอร์

การเข้าถึง-การควบคุม-เปิดเผย-ส่วนหัว

 app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600, exposedHeaders: ['Content-Range', 'X-Content-Range'] }));

หากเราใส่ wildcard(*) ไว้ใน ExposedHeaders จะไม่เปิดเผยส่วนหัว Authorization เราเลยต้องเปิดเผยให้ชัดเหมือนข้างล่างนี้

 app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600, exposedHeaders: ['*', 'Authorization', ] }));

ด้านบนจะเปิดเผยส่วนหัวและส่วนหัวการอนุญาตทั้งหมดด้วย

คุกกี้ HTTP คืออะไร

คุกกี้คือข้อมูลชิ้นเล็กๆ ที่เซิร์ฟเวอร์จะส่งไปยังเบราว์เซอร์ไคลเอ็นต์ ในคำขอในภายหลัง เบราว์เซอร์จะส่งคุกกี้ทั้งหมดที่เกี่ยวข้องกับโดเมนเดียวกันในทุกคำขอ

คุกกี้มีแอตทริบิวต์ซึ่งสามารถกำหนดเพื่อให้คุกกี้ทำงานแตกต่างออกไปตามที่เราต้องการ

  • ชื่อ ชื่อคุกกี้.
  • ค่า: ข้อมูลของคุกกี้ที่เกี่ยวข้องกับชื่อคุกกี้
  • โดเมน: คุกกี้จะถูกส่งไปยังโดเมนที่กำหนดเท่านั้น
  • เส้นทาง: คุกกี้ที่ส่งหลังจากเส้นทางคำนำหน้า URL ที่กำหนดไว้เท่านั้น สมมติว่าเราได้กำหนดเส้นทางคุกกี้ของเราเช่น path='admin/' ไม่ได้ส่งคุกกี้สำหรับ URL https://geekflare.com/expire/ แต่ส่งด้วยคำนำหน้า URL https://geekflare.com/admin/
  • อายุสูงสุด/หมดอายุ (ตัวเลขเป็นวินาที): เมื่อใดที่คุกกี้จะหมดอายุ อายุการใช้งานของคุกกี้ทำให้คุกกี้ใช้งานไม่ได้หลังจากเวลาที่กำหนด
  • HTTPOnly(Boolean): เซิร์ฟเวอร์แบ็กเอนด์สามารถเข้าถึงคุกกี้ HTTPOnly นั้นได้ แต่ไม่สามารถเข้าถึงสคริปต์ฝั่งไคลเอ็นต์เมื่อเป็นจริง
  • ปลอดภัย (บูลีน): คุกกี้จะส่งผ่านโดเมน SSL/TLS เมื่อเป็นจริงเท่านั้น
  • sameSite(string [Strict, Lax, None]): ใช้เพื่อเปิดใช้งาน/จำกัดคุกกี้ที่ส่งผ่านคำขอข้ามไซต์ หากต้องการทราบรายละเอียดเพิ่มเติมเกี่ยวกับคุกกี้ sameSite โปรดดู MDN ยอมรับสามตัวเลือก เข้มงวด, หละหลวม, ไม่มี ตั้งค่าความปลอดภัยของคุกกี้เป็น true สำหรับการกำหนดค่าคุกกี้ sameSite=None

เหตุใดจึงใช้คุกกี้ HTTPOnly สำหรับโทเค็น

การจัดเก็บโทเค็นการเข้าถึงที่ส่งจากเซิร์ฟเวอร์ในที่เก็บข้อมูลฝั่งไคลเอ็นต์ เช่น ที่ จัดเก็บในตัว เครื่อง DB ที่จัดทำดัชนี และ คุกกี้ (ไม่ได้ตั้งค่า HTTPOnly เป็นจริง) จะเสี่ยงต่อการโจมตี XSS มากกว่า สมมติว่าหน้าใดหน้าหนึ่งของคุณอ่อนแอต่อการโจมตี XSS ผู้โจมตีอาจใช้โทเค็นของผู้ใช้ที่จัดเก็บไว้ในเบราว์เซอร์ในทางที่ผิด

คุกกี้ HTTPOnly ถูกตั้งค่า/รับโดยเซิร์ฟเวอร์/แบ็กเอนด์เท่านั้น แต่ไม่อยู่ในฝั่งไคลเอ็นต์

สคริปต์ฝั่งไคลเอ็นต์จำกัดการเข้าถึงคุกกี้ HTTPonly นั้น ดังนั้นคุกกี้ HTTPOnly จึงไม่เสี่ยงต่อการโจมตี XSS และมีความปลอดภัยมากขึ้น เพราะเข้าถึงได้โดยเซิร์ฟเวอร์เท่านั้น

เปิดใช้งานคุกกี้ HTTPOnly ในแบ็กเอนด์ที่เปิดใช้งาน CORS

การเปิดใช้งานคุกกี้ใน CORS จำเป็นต้องมีการกำหนดค่าด้านล่างในแอปพลิเคชัน/เซิร์ฟเวอร์

  • ตั้งค่าส่วนหัว Access-Control-Allow-Credentials เป็น true
  • Access-Control-Allow-Origin และ Access-Control-Allow-Headers ไม่ควรเป็น wildcard(*)
  • แอตทริบิวต์ SameSite ของคุกกี้ควรเป็น None
  • สำหรับการเปิดใช้งานค่า sameSite เป็น none ให้ตั้งค่าการรักษาความปลอดภัยเป็น true: เปิดใช้งานแบ็กเอนด์ด้วยใบรับรอง SSL/TLS เพื่อทำงานในชื่อโดเมน

มาดูตัวอย่างโค้ดที่กำหนดโทเค็นการเข้าถึงในคุกกี้ HTTPOnly หลังจากตรวจสอบข้อมูลรับรองการเข้าสู่ระบบ

 const express = require('express'); const app = express(); const cors = require('cors'); app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600, exposedHeaders: ['*', 'Authorization' ] })); app.post('/login', function (req, res, next) { res.cookie('access_token', access_token, { expires: new Date(Date.now() + (3600 * 1000 * 24 * 180 * 1)), //second min hour days year secure: true, // set to true if your using https or samesite is none httpOnly: true, // backend only sameSite: 'none' // set to none for cross-request }); res.json({ msg: 'Login Successfully', access_token }); }); app.listen(80, function () { console.log('CORS-enabled web server listening on port 80') });

คุณสามารถกำหนดค่าคุกกี้ CORS และ HTTPOnly ได้โดยใช้สี่ขั้นตอนข้างต้นในภาษาแบ็กเอนด์และเว็บเซิร์ฟเวอร์ของคุณ

คุณสามารถทำตามบทช่วยสอนนี้สำหรับ apache และ Nginx เพื่อเปิดใช้งาน CORS โดยทำตามขั้นตอนข้างต้น

withCredentials สำหรับคำขอข้ามต้นทาง

ข้อมูลประจำตัว (คุกกี้ การอนุญาต) ที่ส่งไปพร้อมกับคำขอต้นทางเดียวกันโดยค่าเริ่มต้น สำหรับ cross-origin เราต้องระบุ withCredentials ให้เป็นจริง

XMLHttpRequest API:

 var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://api.geekflare.com/user', true); xhr.withCredentials = true; xhr.send(null);

ดึงข้อมูล API:

 fetch('http://api.geekflare.com/user', { credentials: 'include' });

JQuery อาแจ็กซ์:

 $.ajax({ url: 'http://api.geekflare.com/user', xhrFields: { withCredentials: true } });

แอกซิส :

 axios.defaults.withCredentials = true

บทสรุป

ฉันหวังว่าบทความข้างต้นจะช่วยให้คุณเข้าใจวิธีการทำงานของ CORS และเปิดใช้งาน CORS สำหรับคำขอข้ามต้นทางในเซิร์ฟเวอร์ เหตุใดการจัดเก็บคุกกี้ใน HTTPOnly จึงปลอดภัยและการใช้ withCredentials ในไคลเอนต์สำหรับคำขอข้ามต้นทาง