ทำไม API ต้องมีการรักษาความปลอดภัย
เวลาเราสร้างแอปพลิเคชันขึ้นมาสักตัว เรามักจะโฟกัสไปที่การเขียนฟีเจอร์ให้ใช้งานได้จริง แต่ในฐานะโปรแกรมเมอร์มือใหม่ เราต้องไม่ลืมเรื่อง Security Best Practices (แนวปฏิบัติที่ดีที่สุดด้านความปลอดภัย) เพราะ API (ช่องทางสื่อสารระหว่างซอฟต์แวร์) ของเราเปรียบเสมือนประตูบ้านที่เปิดทิ้งไว้ให้คนแปลกหน้าเข้ามาได้ตลอดเวลา
ลองนึกภาพว่าบ้านของคุณไม่มีกลอนประตู ไม่มีรั้ว และไม่มีใครคอยตรวจบัตรประชาชนที่หน้าบ้าน ใครจะเดินเข้ามาหยิบของหรือทำลายข้าวของในบ้านคุณก็ได้ การทำ API ให้ปลอดภัยก็เหมือนกับการติดตั้งกล้องวงจรปิด ติดตั้งสัญญาณกันขโมย และมีรปภ. คอยคัดกรองคนเข้าออก เพื่อให้ข้อมูลของผู้ใช้งานปลอดภัยจากการโจมตีของเหล่าแฮกเกอร์
ถ้าเราละเลยเรื่องนี้ ข้อมูลส่วนตัวของผู้ใช้งานอาจรั่วไหล หรือเซิร์ฟเวอร์ (เครื่องคอมพิวเตอร์ที่เก็บข้อมูลและทำงานให้เรา) อาจถูกถล่มจนล่มใช้งานไม่ได้ การเรียนรู้วิธีป้องกันตั้งแต่เนิ่นๆ จะช่วยให้เราเป็นโปรแกรมเมอร์ที่เขียนโค้ดอย่างมืออาชีพ และสร้างความมั่นใจให้กับคนที่มาใช้งานซอฟต์แวร์ของเราได้เป็นอย่างดี
ป้องกันการโจมตีด้วย Helmet (HTTP Headers)
Helmet (หมวกกันน็อค) คือเครื่องมือที่จะช่วยเซตค่า HTTP Headers (ข้อมูลส่วนหัวที่ส่งไปพร้อมกับข้อมูลหลัก เพื่อบอกรายละเอียดว่าข้อมูลนี้มาจากไหนหรือต้องจัดการอย่างไร) ให้มีความปลอดภัยมากขึ้นโดยอัตโนมัติ ซึ่งเป็นเรื่องที่มือใหม่มักจะมองข้ามไปเพราะมันทำงานอยู่เบื้องหลังตลอดเวลา
ปกติแล้วเวลาเบราว์เซอร์ (โปรแกรมที่ใช้เข้าเว็บ) ติดต่อกับ API ของเรา มันจะมีการส่งข้อมูลเล็กๆ น้อยๆ ติดไปด้วย ซึ่งแฮกเกอร์อาจใช้ช่องโหว่ตรงนี้เพื่อสวมรอยหรือฝังโค้ดอันตรายเข้ามา การใช้ helmet จะช่วยปิดช่องโหว่เหล่านี้ เช่น การป้องกันการถูกแอบอ้างชื่อเว็บไซต์หรือการโดนฝังเนื้อหาแปลกปลอมลงบนหน้าเว็บ
การติดตั้งทำได้ง่ายมากด้วยคำสั่ง npm install helmet ในโปรเจกต์ Node.js (ระบบที่ช่วยให้เราเขียน JavaScript ฝั่งเซิร์ฟเวอร์ได้) ของเรา หลังจากนั้นเราแค่เรียกใช้มันเป็น Middleware (ซอฟต์แวร์ตัวกลางที่คอยดักจับหรือจัดการคำขอ ก่อนจะส่งไปถึงโค้ดหลัก) เพียงไม่กี่บรรทัดในไฟล์เซิร์ฟเวอร์ของเรา
// นำเข้า express และ helmet
const express = require('express');
const helmet = require('helmet');
const app = express();
// เรียกใช้ helmet เป็น middleware เพื่อป้องกันช่องโหว่พื้นฐาน
app.use(helmet());
app.get('/', (req, res) => {
res.send('API ปลอดภัยขึ้นแล้ว');
});
ในโค้ดชุดนี้ เราทำการดึง helmet เข้ามาใช้งานและสั่ง app.use(helmet()) เพื่อให้มันเริ่มทำงานกับทุกคำขอที่เข้ามายัง API ของเรา บรรทัดที่สำคัญคือการเรียกใช้ app.use ซึ่งจะทำให้ทุกเส้นทางใน API ของเราได้รับความคุ้มครองทันที
เมื่อรันเซิร์ฟเวอร์แล้ว ถ้าคุณลองเช็กที่ Network Tab (แถบแสดงการรับส่งข้อมูลในเครื่องมือสำหรับนักพัฒนาของเบราว์เซอร์) คุณจะพบว่าใน Response Headers จะมีค่าความปลอดภัยเพิ่มขึ้นมา เช่น X-Content-Type-Options หรือ Content-Security-Policy ซึ่งช่วยยืนยันว่า API ของเรากำลังใส่ใจความปลอดภัยอยู่
จัดการสิทธิ์การเข้าถึงด้วย CORS
CORS (การแชร์ทรัพยากรข้ามแหล่งกำเนิด) เป็นกลไกที่เบราว์เซอร์ใช้ควบคุมว่า เว็บไซต์จากโดเมนอื่นสามารถมาดึงข้อมูลจาก API ของเราได้หรือไม่ หากเราไม่ตั้งค่านี้ให้ดี คนอื่นอาจจะดึงข้อมูลของเราไปใช้ในทางที่ผิด หรือในทางกลับกัน เราก็อาจจะเรียกใช้ API ของคนอื่นไม่ได้เลย
ลองจินตนาการว่าคุณมีสมุดบันทึกวางไว้บนโต๊ะ ถ้าคุณไม่กำหนดกฎเกณฑ์เลย ใครเดินผ่านไปมาก็อาจจะมาเปิดอ่านหรือขีดเขียนอะไรลงไปก็ได้ การใช้ cors จึงเป็นการกำหนดว่า "อนุญาตให้เฉพาะเว็บไซต์ที่เรารู้จักเท่านั้นนะ ถึงจะเข้ามาอ่านข้อมูลใน API นี้ได้"
สำหรับการพัฒนาในช่วงแรก เราอาจจะตั้งค่าให้เข้าถึงได้จากทุกที่ แต่เมื่อต้องเอาขึ้น Production (สภาพแวดล้อมที่ใช้งานจริงสำหรับผู้ใช้ทั่วไป) เราต้องเปลี่ยนมาอนุญาตเฉพาะโดเมนของเว็บเราเท่านั้น เพื่อป้องกันไม่ให้คนอื่นแอบอ้างนำ API ของเราไปใช้บนเว็บไซต์ของเขาเอง
const cors = require('cors');
// อนุญาตเฉพาะโดเมนที่ระบุเท่านั้น
const corsOptions = {
origin: 'https://my-awesome-app.com'
};
app.use(cors(corsOptions));
ในตัวอย่างนี้ เรากำหนดค่า corsOptions โดยระบุ origin (แหล่งกำเนิด) ให้ชัดเจนว่าอนุญาตให้แค่เว็บไซต์ของเราเท่านั้นที่เรียกใช้ได้ หากมีเว็บอื่นพยายามเรียกใช้ เบราว์เซอร์จะบล็อกการเชื่อมต่อให้ทันทีตามกฎที่เราตั้งไว้
ผลลัพธ์ที่ได้คือ เมื่อเราพยายามยิงคำขอ API จากเว็บไซต์อื่นที่ไม่ใช่ my-awesome-app.com เบราว์เซอร์จะแสดงข้อผิดพลาดเกี่ยวกับ CORS ในคอนโซล ทำให้มั่นใจได้ว่าข้อมูลของเราถูกเข้าถึงโดยผู้ที่ได้รับอนุญาตจริงๆ เท่านั้น
ป้องกัน Brute Force ด้วย express-rate-limit
Brute Force (การสุ่มเดารหัสผ่านหรือข้อมูลจำนวนมากในเวลาอันสั้น) คือวิธีที่แฮกเกอร์ใช้เพื่อพยายามเจาะระบบโดยการสุ่มรหัสผ่านไปเรื่อยๆ จนกว่าจะถูก การมี express-rate-limit (เครื่องมือจำกัดจำนวนคำขอ) จะช่วยหยุดการโจมตีแบบนี้ได้โดยการจำกัดว่าในหนึ่งนาที ห้ามยิงคำขอเกินกี่ครั้ง
เปรียบเหมือนมีคนพยายามจะหมุนลูกบิดประตูบ้านคุณรัวๆ เป็นร้อยครั้งต่อวินาที ถ้าคุณเป็นเจ้าของบ้าน คุณคงจะล็อกประตูและห้ามคนนั้นเข้าใกล้บ้านอีกสักพัก express-rate-limit ทำหน้าที่แบบนั้นเลย คือถ้าใครทำตัวน่าสงสัย ก็จะโดนจำกัดสิทธิ์ชั่วคราว
การใช้งานเครื่องมือนี้เป็นเรื่องที่สำคัญมากสำหรับหน้า Login (เข้าสู่ระบบ) หรือหน้าสมัครสมาชิก เพราะเป็นจุดที่แฮกเกอร์มักจะเล็งเป้าหมายเพื่อสุ่มข้อมูล หากเราไม่ป้องกันไว้ ระบบเราอาจจะค้างหรือข้อมูลผู้ใช้หลุดได้ง่ายๆ
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // กำหนดช่วงเวลา 15 นาที
max: 100 // จำกัดให้ทำได้แค่ 100 ครั้งต่อ 15 นาที
});
app.use('/api/', limiter);
โค้ดส่วนนี้กำหนดว่าในทุกเส้นทางที่ขึ้นต้นด้วย /api/ จะถูกจำกัดไว้ที่ 100 ครั้งต่อ 15 นาที หากใครพยายามเกินกว่านี้ เซิร์ฟเวอร์จะตอบกลับด้วยข้อผิดพลาด 429 Too Many Requests (คำขอมากเกินไป) ทันที
เมื่อคุณลองทดสอบด้วยการยิงคำขอรัวๆ เกินจำนวนที่กำหนด คุณจะเห็นว่าเซิร์ฟเวอร์ปฏิเสธการทำงานและส่งสถานะ 429 กลับมา ซึ่งนี่คือสัญญาณว่าระบบป้องกันของเราทำงานได้อย่างถูกต้องและกำลังปกป้อง API จากการโดนยิงถล่มอยู่
ทำความสะอาดข้อมูลด้วย Sanitization
Sanitization (การทำให้ข้อมูลสะอาด) คือขั้นตอนการกรองข้อมูลที่ผู้ใช้ส่งเข้ามา เพื่อป้องกันไม่ให้มีโค้ดอันตรายแฝงตัวเข้ามาทำลายระบบ โดยเฉพาะการโจมตีที่เรียกว่า Injection (การใส่คำสั่งแปลกปลอมเข้าไปในฐานข้อมูล) ซึ่งอาจทำให้ข้อมูลในระบบของเราเสียหายทั้งหมด
ลองนึกภาพว่าคุณเปิดรับพัสดุจากคนแปลกหน้า แต่คุณมีเครื่องสแกนที่หน้าบ้านเพื่อตรวจดูว่าข้างในมีระเบิดหรือสิ่งของอันตรายหรือไม่ ถ้าเจอสิ่งของอันตราย คุณก็แค่ทิ้งมันไปก่อนจะนำเข้าบ้าน การทำ Sanitization ก็คือการสแกนข้อมูลที่รับมาจาก req.body (ข้อมูลที่ส่งมาพร้อมกับคำขอ) นั่นเอง
ใน Node.js เรามักใช้ไลบรารีอย่าง express-validator เพื่อช่วยตรวจสอบและทำความสะอาดข้อมูล ไม่ว่าจะเป็นการตัดแท็ก HTML หรือการตรวจสอบรูปแบบข้อมูลให้ถูกต้องก่อนจะนำไปเก็บลงในฐานข้อมูล เพื่อป้องกันไม่ให้แฮกเกอร์ฝังคำสั่งรันโค้ดลงบนเซิร์ฟเวอร์ของเรา
const { body, validationResult } = require('express-validator');
app.post('/register',
body('username').trim().escape(), // ตัดช่องว่างและแปลงอักขระพิเศษ
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
// ทำงานต่อได้ถ้าข้อมูลสะอาดแล้ว
}
);
ในตัวอย่างนี้ เราใช้ trim() เพื่อเอาช่องว่างหน้าหลังออก และ escape() เพื่อแปลงตัวอักษรพิเศษอย่าง < หรือ > ให้กลายเป็นข้อความธรรมดา เพื่อไม่ให้มันถูกมองว่าเป็นคำสั่งของโปรแกรม
ผลลัพธ์คือ แม้จะมีคนพยายามส่งข้อมูลที่เป็นอันตรายเข้ามา ข้อมูลนั้นจะถูกแปลงให้อยู่ในรูปแบบที่ปลอดภัยก่อนจะถูกนำไปใช้งานจริง ทำให้ระบบของเราไม่ต้องกังวลเรื่องการโดนฝังโค้ดแปลกปลอมอีกต่อไป
สรุป: เริ่มต้นสร้างความปลอดภัยตั้งแต่วันนี้
การทำให้ API ปลอดภัยไม่ได้มีอะไรซับซ้อนเกินกว่าความพยายามของเรา แค่เริ่มต้นจากสิ่งพื้นฐานที่เราคุยกันไป ไม่ว่าจะเป็นการใช้ helmet ปิดช่องโหว่ทั่วไป การตั้งค่า cors ให้ถูกที่ การจำกัดคำขอด้วย express-rate-limit และการตรวจความสะอาดของข้อมูลด้วย Sanitization ทุกอย่างล้วนเป็นกำแพงป้องกันที่สำคัญ
สำหรับคนที่กำลังฝึกฝนเพื่อเป็นโปรแกรมเมอร์ การหัดนำเครื่องมือเหล่านี้ไปใช้ในโปรเจกต์เล็กๆ ของตัวเองเป็นจุดเริ่มต้นที่ดีที่สุด คุณไม่จำเป็นต้องรอให้เก่งเทพถึงจะเริ่มทำความปลอดภัย แต่ควรเริ่มทำตั้งแต่วันที่เริ่มเขียนโค้ดบรรทัดแรก เพื่อสร้างนิสัยการเขียนโค้ดที่คำนึงถึงผู้ใช้งานและความปลอดภัยอยู่เสมอ
จงจำไว้ว่าความปลอดภัยคือส่วนหนึ่งของฟีเจอร์ ไม่ใช่สิ่งที่ทำทีหลังได้ การฝึกทำตามแนวทางเหล่านี้จะทำให้พอร์ตโฟลิโอของคุณดูน่าเชื่อถือและพร้อมสำหรับการทำงานจริงในอนาคต ขอให้สนุกกับการเขียนโค้ดและสร้างสรรค์ซอฟต์แวร์ที่ปลอดภัยให้กับโลกใบนี้ครับ