ทำไม Session Cookie ถึงเปรียบเสมือนกุญแจชั่วคราว
ในการเขียนโปรแกรมเว็บ เรื่องของ Session Cookie (คุกกี้ที่เก็บสถานะการใช้งานชั่วคราว) เป็นสิ่งที่มือใหม่ต้องเจอแน่นอน ลองจินตนาการว่าคุณไปโรงแรมแล้วได้รับคีย์การ์ดมาใบหนึ่ง คีย์การ์ดนี้ไม่ได้บอกว่าคุณคือใคร แต่บอกว่าคุณมีสิทธิ์เข้าห้องพักได้ในช่วงเวลาที่กำหนด Session (ช่วงเวลาที่ผู้ใช้ล็อกอินอยู่บนเว็บ) ก็ทำงานแบบเดียวกันเป๊ะ คือเป็นตัวยืนยันว่าคุณล็อกอินผ่านแล้วนะ ไม่ต้องกรอกรหัสผ่านทุกครั้งที่เปลี่ยนหน้าเว็บ
แต่ความอันตรายคือถ้าใครก็ตามได้คีย์การ์ดของคุณไป เขาก็เดินเข้าห้องคุณได้ทันทีโดยไม่ต้องใช้รหัสผ่านจริง ดังนั้นการตั้งค่าคุกกี้ให้ปลอดภัยในตอนแรกนั้นเป็นแค่จุดเริ่มต้นเท่านั้น เพราะตลอดอายุการใช้งานของมัน มีความเสี่ยงซ่อนอยู่มากมายที่โปรแกรมเมอร์มือใหม่มักจะมองข้ามไป
เราต้องมองว่า Session Cookie คือของมีค่าที่ต้องดูแลตลอดวงจรชีวิต ตั้งแต่ตอนที่มันถูกสร้างขึ้น ถูกนำไปใช้งาน ไปจนถึงตอนที่มันหมดอายุและต้องถูกทำลายทิ้ง ถ้าเราปล่อยให้มันอยู่นานเกินไปหรือจัดการไม่ดีพอ ข้อมูลของผู้ใช้ในระบบที่คุณสร้างขึ้นก็ตกอยู่ในความเสี่ยงทันที
CSRF: เมื่อเบราว์เซอร์กลายเป็นเครื่องมือของผู้ไม่หวังดี
ปัญหาหนึ่งที่น่าปวดหัวคือ CSRF (การปลอมแปลงคำสั่งจากเว็บไซต์อื่น) ซึ่งเกิดขึ้นเพราะนิสัยของเบราว์เซอร์ที่ชอบส่งคุกกี้ไปให้เซิร์ฟเวอร์โดยอัตโนมัติ สมมติว่าคุณล็อกอินเข้าเว็บธนาคารค้างไว้ แล้วเผลอไปกดลิงก์เว็บอันตราย เว็บนั้นอาจแอบส่งคำสั่งไปที่ธนาคารโดยที่คุณไม่รู้ตัว เบราว์เซอร์ก็จะแนบคุกกี้ของคุณไปให้ธนาคารด้วย ทำให้ธนาคารเข้าใจว่าเป็นคุณที่สั่งทำรายการนั้นจริงๆ
วิธีป้องกันที่ดีที่สุดคือการใช้ CSRF Token (รหัสลับที่ใช้ตรวจสอบความถูกต้องของคำขอ) โดยเซิร์ฟเวอร์จะสร้างรหัสสุ่มขึ้นมาหนึ่งชุดให้กับผู้ใช้ ทุกครั้งที่มีการส่งข้อมูลสำคัญ เช่น การกดเปลี่ยนอีเมล หรือการโอนเงิน เซิร์ฟเวอร์จะเช็คว่านอกจากคุกกี้แล้ว ต้องมีรหัสลับตัวนี้แนบมาด้วยเสมอ ถ้าไม่มีหรือรหัสไม่ตรงกัน เซิร์ฟเวอร์ก็จะปฏิเสธคำสั่งนั้นทันที
การเข้าใจเรื่องนี้สำคัญมากสำหรับคนที่อยากเป็นโปรแกรมเมอร์ เพราะมันทำให้เราเห็นว่าเราไม่ควรไว้ใจเบราว์เซอร์มากเกินไป การมีเพียงคุกกี้ที่ปลอดภัยอย่างเดียวไม่เพียงพอเสมอไป คุณต้องสร้างกลไกตรวจสอบเพิ่มเติมเพื่อให้มั่นใจว่าคำสั่งนั้นมาจากตัวผู้ใช้จริงๆ ไม่ได้มาจากเว็บอื่นที่แอบอ้าง
// ตัวอย่างการตรวจสอบ CSRF Token ในเซิร์ฟเวอร์
app.post("/update-email", (req, res) => {
const userToken = req.body.csrf_token; // รับรหัสจากฟอร์ม
const sessionToken = req.session.csrfToken; // รหัสที่เก็บใน Session
if (userToken !== sessionToken) {
return res.status(403).send("คำขอไม่ปลอดภัย");
}
// ดำเนินการเปลี่ยนอีเมลต่อ...
});
คำอธิบายโค้ด: บรรทัดที่ 3 คือการรับค่ารหัสที่ส่งมาจากหน้าเว็บ ส่วนบรรทัดที่ 4 คือการหยิบรหัสที่เซิร์ฟเวอร์เคยสร้างไว้ขึ้นมาเทียบ ถ้าค่าไม่ตรงกัน (บรรทัดที่ 6) ระบบจะตีกลับทันที ผลลัพธ์ที่ได้คือหากมีเว็บอันตรายพยายามส่งคำสั่งแทนเรา มันจะไม่มีรหัสลับตัวนี้ ทำให้คำสั่งถูกปฏิเสธด้วยสถานะ 403 Forbidden (ห้ามเข้าถึง)
Session Fixation: การดักจับเซสชันก่อนการล็อกอิน
อีกหนึ่งภัยเงียบที่น่ากลัวคือ Session Fixation (การจงใจกำหนดไอดีเซสชันให้เหยื่อ) แทนที่แฮกเกอร์จะพยายามขโมยคุกกี้ของคุณหลังจากล็อกอิน เขาจะใช้วิธีหลอกล่อให้คุณใช้ไอดีเซสชันที่เขารู้จักตั้งแต่แรก ถ้าเว็บของคุณไม่เปลี่ยนไอดีเซสชันใหม่หลังจากที่ผู้ใช้ล็อกอินสำเร็จ แฮกเกอร์คนนั้นก็จะกลายเป็นคุณทันทีโดยไม่ต้องรู้รหัสผ่านสักตัว
ตัวอย่างสถานการณ์คือ แฮกเกอร์ส่งลิงก์ที่มีไอดีเซสชันแฝงอยู่ให้คุณ เมื่อคุณเข้าเว็บผ่านลิงก์นั้น ระบบก็จะเริ่มเก็บสถานะของคุณด้วยไอดีนั้น เมื่อคุณกรอกชื่อผู้ใช้และรหัสผ่านเพื่อเข้าสู่ระบบ เซิร์ฟเวอร์ก็นำสถานะ "ล็อกอินแล้ว" ไปผูกกับไอดีเดิมที่แฮกเกอร์ถืออยู่ ทำให้เขาสามารถเข้าถึงบัญชีของคุณได้ทันทีโดยไม่ต้องเดารหัสผ่าน
โปรแกรมเมอร์มืออาชีพจึงต้องมีกฎเหล็กข้อหนึ่งเสมอคือ เมื่อมีการเปลี่ยนสถานะการล็อกอิน ต้องสร้างไอดีเซสชันใหม่ทิ้งของเก่าทันที ขั้นตอนนี้เรียกว่า Session Regeneration (การสร้างไอดีเซสชันใหม่) ซึ่งเป็นเกราะป้องกันชั้นดีที่ช่วยให้ไอดีเดิมที่แฮกเกอร์ถืออยู่กลายเป็นเศษกระดาษที่ใช้งานไม่ได้อีกต่อไป
// การสร้าง Session ใหม่หลังล็อกอิน
app.post("/login", async (req, res) => {
const user = await authenticate(req.body.username, req.body.password);
if (!user) return res.status(401).send("ล็อกอินไม่สำเร็จ");
req.session.regenerate((err) => { // สร้างไอดีใหม่ที่นี่
req.session.userId = user.id;
res.redirect("/dashboard");
});
});
คำอธิบายโค้ด: ฟังก์ชัน regenerate ในบรรทัดที่ 5 คือหัวใจสำคัญ มันจะล้างไอดีเดิมทิ้งและออกไอดีใหม่ให้ผู้ใช้ทันทีหลังจากตรวจสอบรหัสผ่านผ่านแล้ว ผลลัพธ์คือแม้แฮกเกอร์จะพยายามกำหนดไอดีเซสชันให้เหยื่อก่อนล็อกอิน แต่เมื่อเหยื่อล็อกอินสำเร็จ ไอดีนั้นจะถูกเปลี่ยนไปเป็นไอดีใหม่ ทำให้แฮกเกอร์เข้าถึงข้อมูลไม่ได้
การกำหนดเวลาหมดอายุเพื่อลดความเสี่ยง
เราไม่ควรปล่อยให้เซสชันมีอายุยืนยาวตลอดกาล เพราะถ้าวันหนึ่งมีใครเผลอทำคุกกี้หลุดไป แฮกเกอร์ก็จะมีเวลาแอบอ้างเป็นเราได้นานขึ้น การตั้งเวลาหมดอายุจึงเป็นเรื่องที่ต้องทำ ทั้งแบบ Idle Timeout (หมดอายุเมื่อไม่มีการใช้งาน) และ Absolute Timeout (หมดอายุตามเวลาที่กำหนดตายตัว)
ลองนึกภาพว่าคุณล็อกอินคอมพิวเตอร์สาธารณะแล้วลืมกดออกจากระบบ ถ้าเว็บไม่มี Idle Timeout หลังจากผ่านไป 30 นาที ระบบก็จะยังเปิดค้างไว้ให้คนอื่นมาใช้งานต่อได้ง่ายๆ การตั้งค่าให้ระบบตัดการเชื่อมต่ออัตโนมัติเมื่อผู้ใช้ไม่ได้ขยับเมาส์หรือกดปุ่มใดๆ เป็นเวลาหนึ่ง จึงช่วยลดโอกาสที่คนอื่นจะมาสวมรอยใช้เครื่องเราได้
สำหรับ Absolute Timeout คือการตั้งค่าว่า "ไม่ว่าคุณจะใช้งานต่อเนื่องแค่ไหน แต่พอครบ 8 ชั่วโมง คุณต้องล็อกอินใหม่นะ" วิธีนี้ช่วยจำกัดเวลาที่แฮกเกอร์จะแอบใช้เซสชันของเราได้หากเขาขโมยไปได้สำเร็จ การผสมผสานทั้งสองแบบนี้จะทำให้เว็บของคุณมีความปลอดภัยสูงขึ้นมากสำหรับผู้ใช้งานจริง
เมื่อฟีเจอร์ "จดจำฉันไว้" กลายเป็นดาบสองคม
ฟีเจอร์ Remember Me (จดจำการล็อกอิน) มักจะทำให้คุกกี้มีอายุยืนยาวขึ้นมาก เช่น จาก 30 นาที เป็น 30 วัน ซึ่งนั่นหมายความว่าความเสี่ยงก็เพิ่มขึ้นเป็นทวีคูณ หากมีใครได้ไฟล์คุกกี้ในเครื่องเราไป เขาก็จะสวมรอยเป็นเราได้นานถึงหนึ่งเดือนเต็มๆ ซึ่งเป็นช่องโหว่ที่ใหญ่มากสำหรับระบบที่มีความสำคัญ
แนวทางปฏิบัติที่ดีที่สุดคือการแยกส่วนการล็อกอินปกติออกจากฟีเจอร์จดจำการใช้งาน ถ้าผู้ใช้ต้องการทำรายการที่มีความเสี่ยงสูง เช่น การโอนเงินจำนวนมาก หรือการเปลี่ยนรหัสผ่าน ระบบควรบังคับให้ผู้ใช้ต้องกรอกรหัสผ่านใหม่อีกครั้งเสมอ แม้ว่าเขาจะเลือกฟีเจอร์จดจำการใช้งานไว้แล้วก็ตาม
ในฐานะโปรแกรมเมอร์ คุณต้องคิดเสมอว่าความสะดวกสบายของผู้ใช้ต้องมาคู่กับความปลอดภัย การยอมให้ผู้ใช้ไม่ต้องล็อกอินใหม่ทุกวันเป็นเรื่องดี แต่ต้องมีกลไกตรวจสอบที่เข้มงวดเมื่อมีการทำรายการที่สำคัญเสมอ อย่าทำแค่ทำให้มันใช้งานได้ แต่ต้องทำให้มันใช้งานได้อย่างปลอดภัยในระยะยาวด้วย
อย่าลืมทำลายเซสชันเมื่อผู้ใช้กดออกจากระบบ
หลายคนเข้าใจผิดว่าการสั่ง res.clearCookie ที่ฝั่งไคลเอนต์ (เบราว์เซอร์) ก็เพียงพอแล้วสำหรับการล็อกเอาต์ แต่นั่นคือการลบแค่กุญแจในมือผู้ใช้ ข้อมูลเซสชันที่อยู่บนเซิร์ฟเวอร์ยังคงทำงานได้ปกติ ถ้าแฮกเกอร์มีกุญแจสำรองที่ขโมยมาได้ เขาก็ยังเข้าถึงข้อมูลได้อยู่ดี
การ Logout (การออกจากระบบ) ที่ถูกต้องคือการสั่งให้เซิร์ฟเวอร์ลบทิ้งข้อมูลเซสชันนั้นออกจากฐานข้อมูลหรือหน่วยความจำทันที เมื่อเซสชันที่เซิร์ฟเวอร์ถูกทำลายแล้ว ต่อให้ใครมีคุกกี้เก่าอยู่ในมือ กุญแจนั้นก็กลายเป็นของไร้ค่าที่เปิดประตูเข้าไม่ได้อีกต่อไป
นี่คือขั้นตอนที่มือใหม่มักพลาดบ่อยที่สุด โปรดจำไว้ว่าการเขียนโปรแกรมความปลอดภัยไม่ใช่แค่เรื่องของหน้าตาเว็บ แต่เป็นเรื่องของการจัดการข้อมูลหลังบ้านให้สะอาดและปลอดภัยอยู่เสมอ การปิดประตูให้สนิททุกครั้งที่ผู้ใช้ออกจากระบบ คือหน้าที่สำคัญของโปรแกรมเมอร์ที่เขียนระบบล็อกอินทุกคน
// การทำลายเซสชันบนเซิร์ฟเวอร์ให้หมดจด
app.post("/logout", (req, res) => {
req.session.destroy((err) => {
if (err) return res.sendStatus(500);
res.clearCookie("session_id"); // ลบคุกกี้ในเบราว์เซอร์ออกด้วย
res.redirect("/");
});
});
คำอธิบายโค้ด: ฟังก์ชัน destroy ในบรรทัดที่ 3 จะลบข้อมูลเซสชันทั้งหมดที่เก็บไว้บนเซิร์ฟเวอร์ ส่วนบรรทัดที่ 5 คือการสั่งให้เบราว์เซอร์ลบคุกกี้ทิ้ง ผลลัพธ์ที่ได้คือเซสชันนั้นจะถูกยกเลิกการใช้งานอย่างถาวรทั้งสองฝั่ง ทำให้กุญแจเก่าไม่สามารถถูกนำกลับมาใช้ซ้ำได้อีก
สรุป
การเข้าใจวงจรชีวิตของเซสชันตั้งแต่เริ่มสร้างจนถึงการทำลาย คือทักษะสำคัญที่แยกโปรแกรมเมอร์ทั่วไปออกจากโปรแกรมเมอร์ที่ใส่ใจความปลอดภัย สิ่งที่คุณควรทำเมื่อต้องสร้างระบบล็อกอินคือ 1) ใช้ CSRF Token ทุกครั้งที่มีการส่งข้อมูล 2) สั่ง regenerate ไอดีเซสชันใหม่เสมอหลังล็อกอิน 3) ตั้งเวลาหมดอายุที่เหมาะสม และ 4) ทำลายเซสชันบนเซิร์ฟเวอร์ให้จบเมื่อผู้ใช้กดออก
ตัวอย่างการนำไปใช้จริง: หากคุณกำลังทำโปรเจกต์เว็บแอปพลิเคชันส่วนตัว ให้ลองเพิ่มฟังก์ชันการล็อกเอาต์ที่ทำลายเซสชันบนเซิร์ฟเวอร์จริงๆ ดูครับ ลองสังเกตว่าหลังจากกดล็อกเอาต์แล้ว ถ้าคุณพยายามนำค่า session_id เดิมกลับมาใส่ในเบราว์เซอร์อีกครั้ง ระบบควรจะปฏิเสธการเข้าถึงทันที หากทำได้สำเร็จ นั่นแปลว่าคุณเริ่มเข้าใจหัวใจของการรักษาความปลอดภัยบนเว็บอย่างถูกต้องแล้วครับ
ที่มา: Your Session Cookie Is Basically a Temporary Password - Part 2 — DEV Community