ทำไมการ "เชื่อมั่นในโค้ดตัวเอง" ถึงเป็นกับดักที่อันตรายที่สุด
เวลาที่เราเขียนโปรแกรม ไม่ว่าจะเป็นฟีเจอร์เล็กๆ หรือระบบจัดการข้อมูลขนาดใหญ่ เรามักจะมีความมั่นใจในตรรกะที่ตัวเองเพิ่งเขียนเสร็จใหม่ๆ เพราะเราเข้าใจที่มาที่ไปของมันดีที่สุด แต่ในโลกของ การทดสอบความปลอดภัย (Penetration Testing) หรือการหาช่องโหว่ในระบบ ความมั่นใจนี้แหละครับที่เป็นจุดบอดที่น่ากลัวที่สุด เพราะการที่เรา "คิดว่า" โค้ดทำงานได้ตามที่ต้องการ ไม่ได้แปลว่ามันทำงานได้จริงในทุกกรณีเสมอไป
ผมเพิ่งทำการทดสอบระบบ AI Hub (ศูนย์กลางการประมวลผลข้อมูลสำหรับ AI) ของตัวเองเมื่อสัปดาห์ก่อน สิ่งที่ผมพบไม่ใช่บั๊กที่เกิดจากช่องโหว่ระดับโลก (CVE) หรือการถูกแฮกจากภายนอก แต่มันคือช่องโหว่ที่เกิดจาก "ความเชื่อมั่นในสิ่งที่ยังไม่ได้พิสูจน์" ผมเขียนคำสั่งป้องกันไว้ แต่ไม่เคยนั่งเฝ้าดูจริงๆ ว่ามันทำงานอย่างไรในสถานการณ์จริง เหมือนกับการที่เราล็อกประตูบ้านไว้ แต่ไม่เคยเดินไปเขย่าดูว่ากลอนมันแน่นพอจริงๆ หรือเปล่า
Penetration Testing คือกระบวนการจำลองการโจมตีเพื่อหาจุดอ่อนของระบบ ซึ่งมือใหม่มักเข้าใจผิดว่าต้องใช้เครื่องมือซับซ้อน แต่จริงๆ แล้วมันคือการตรวจสอบว่า "เงื่อนไขที่เราตั้งไว้" นั้นรัดกุมจริงไหม สำหรับโปรแกรมเมอร์มือใหม่ นี่คือทักษะสำคัญในการเปลี่ยนจากคนเขียนโค้ดให้ "ทำงานได้" (Working code) ไปสู่การเขียนโค้ดที่ "ปลอดภัยและเชื่อถือได้" (Secure code)
กฎเหล็ก: อย่าเชื่อจนกว่าจะเห็นด้วยตาตัวเอง
ปัญหาที่ผมเจอในระบบตัวเองคือ การที่ผมมั่นใจว่าข้อมูลส่วนตัว (Identity attributes) จะไม่หลุดออกไปนอกระบบ เพราะผมได้ตั้งค่าตัวกรองไว้แล้วใน OpenTelemetry Collector (เครื่องมือรวบรวมข้อมูลการทำงานของระบบ) แต่พอผมลองทดสอบด้วยการจำลองสถานการณ์จริง ผลลัพธ์กลับกลายเป็นว่าข้อมูลส่วนตัวยังคงหลุดไปแสดงผลอยู่ในระบบจัดการข้อมูล (Loki) ได้อย่างง่ายดาย
เหตุผลคือ "ความเข้าใจผิดเรื่องลำดับชั้นของข้อมูล" ผมคิดว่าเมื่อกรองข้อมูลที่ชั้นแรกแล้ว มันจะปลอดภัยตลอดไป แต่ความจริงคือมันยังมีช่องโหว่ที่ข้อมูลข้ามผ่านตัวกรองไปได้ นี่คือบทเรียนสำคัญ: การเขียนโค้ดเพื่อป้องกัน (Defense) ต้องได้รับการตรวจสอบด้วยการสังเกตการณ์จริง (Observation) ไม่ใช่แค่การเขียนแล้วปล่อยผ่านไป
ลองดูตัวอย่างความผิดพลาดที่เกิดขึ้นจริงในการตั้งค่าตัวกรองข้อมูลที่ผมเคยเขียนไว้:
# แบบที่ผิด: คิดว่าการกรองแค่ระดับหนึ่งจะเพียงพอ
- context: resource statements:
- keep_keys(resource.attributes, ["service.name"])
# ผลลัพธ์: ตัวกรองนี้กรองได้แค่บางชั้น ทำให้ข้อมูลสำคัญยังหลุดรอดไปได้
ในโค้ดด้านบน เราพยายามใช้ keep_keys เพื่อเก็บไว้แค่ชื่อบริการ แต่หากโครงสร้างข้อมูลมีความซับซ้อนกว่านั้น ข้อมูลในส่วน scope ก็จะถูกละเลยและหลุดรอดไปได้ การทดสอบที่แท้จริงไม่ใช่การรันสคริปต์อัตโนมัติ แต่คือการ ตรวจสอบผลลัพธ์ (Verify) ว่าข้อมูลที่หลุดออกไปมีอะไรบ้าง
วิธีทดสอบระบบแบบมืออาชีพ (โดยไม่ต้องพึ่งเครื่องมือแฮก)
คุณไม่จำเป็นต้องใช้เครื่องมือแฮกเกอร์ระดับโลกเพื่อทดสอบระบบตัวเอง สิ่งที่คุณต้องทำคือการสร้าง Dynamic Run (การรันระบบในสภาพแวดล้อมจำลองเหมือนจริง) ขึ้นมาใน Docker เพื่อดูว่าข้อมูลวิ่งไปที่ไหนบ้าง การทำแบบนี้จะทำให้คุณเห็นเส้นทางของข้อมูล (Data flow) อย่างชัดเจนและแม่นยำที่สุด
ขั้นตอนการทดสอบด้วยตัวเองมีดังนี้:
- สร้างสภาพแวดล้อมจำลอง: ใช้ Docker Compose รันระบบทั้งหมดที่คุณสร้างขึ้นมาในเครื่องตัวเอง
- ตรวจสอบบันทึกการทำงาน (Logs): เข้าไปดูในไฟล์ Log หรือแดชบอร์ดที่คุณสร้างไว้ ว่ามีข้อมูลอะไรที่ไม่ควรอยู่ปรากฏออกมาหรือไม่
- ทำลายสมมติฐาน: ลองตั้งค่าตัวกรองให้เข้มงวดที่สุดเท่าที่จะเป็นไปได้ แล้วดูว่าระบบพังหรือไม่ ถ้าไม่พัง แสดงว่าคุณอาจจะกรองไม่หมด
จุดที่มือใหม่มักพลาดคือการคิดว่า "ถ้าไม่มี Error ขึ้น ก็แปลว่าปลอดภัย" แต่จริงๆ แล้ว ความเงียบของระบบ (ไม่มีการแจ้งเตือน) อาจหมายถึงความล้มเหลวในการตรวจจับช่องโหว่ก็ได้ครับ ดังนั้น การตรวจสอบด้วยการอ่าน Log อย่างละเอียดจึงสำคัญกว่าการรันเครื่องมือสแกนอัตโนมัติ
การแก้ไขที่ยั่งยืน: แก้ไขที่ "วิธี" ไม่ใช่แค่ "รอยรั่ว"
หลังจากพบช่องโหว่ ผมไม่ได้แค่ไล่ปิดรูรั่วทีละจุด แต่ผมเปลี่ยน วิธีคิด ในการจัดการข้อมูลใหม่ การแก้ไขที่ดีที่สุดมักจะเรียบง่ายและครอบคลุมทุกกรณี (No-op fix) เหมือนกับการเปลี่ยนจากการพยายาม "ไล่จับขโมย" เป็นการ "ปิดประตูทุกบานที่ขโมยจะเข้าได้" อย่างถาวร
นี่คือตัวอย่างการแก้ไขที่ถูกต้องและรัดกุมกว่าเดิม:
# แบบที่ถูก: ปิดช่องโหว่ที่ต้นเหตุด้วยการกรองทุกชั้น
- context: scope statements:
- keep_keys(scope.attributes, [])
# อธิบาย: การกำหนดให้ scope.attributes เป็นรายการว่าง
# จะเป็นการบังคับให้ระบบไม่ส่งข้อมูลส่วนเกินออกไปอย่างเด็ดขาด
การแก้ไขนี้ดูเหมือนไม่มีอะไร แต่ในทางปฏิบัติมันคือการตัดวงจรข้อมูลส่วนตัวไม่ให้เข้าสู่ระบบจัดเก็บได้เลย การแก้ไขที่ทรงพลังที่สุดมักจะดูเรียบง่ายที่สุดเสมอ เพราะมันลดความซับซ้อนและโอกาสที่จะเกิดความผิดพลาดซ้ำซ้อนในอนาคต
สรุป: เปลี่ยนจากคนเขียนโค้ด เป็นโปรแกรมเมอร์ที่รอบคอบ
บทเรียนจากเหตุการณ์นี้คือ "การพิสูจน์ที่รันได้จริง ไม่ใช่การพิสูจน์ที่แค่คิดว่ารันได้" สำหรับคนที่กำลังฝึกเขียนโปรแกรม ผมอยากให้คุณจำไว้ว่าการเขียนโค้ดให้ทำงานได้เป็นเพียงก้าวแรก แต่ก้าวถัดไปที่สำคัญคือการตั้งคำถามกับโค้ดของตัวเองเสมอว่า "มันปลอดภัยจริงหรือเปล่า?" และ "เราเห็นมันทำงานด้วยตาตัวเองแล้วหรือยัง?"
ตัวอย่างการนำไปใช้จริง: ครั้งต่อไปที่คุณเขียนฟังก์ชันจัดการข้อมูลผู้ใช้ (เช่น การดึงข้อมูลอีเมล) อย่าเพิ่งเชื่อว่ามันปลอดภัยดีแล้ว ให้ลอง Print ค่าที่ฟังก์ชันนั้นส่งออกมาดูจริงๆ ในขณะที่คุณรันโปรแกรมในเครื่อง (Local environment) หากคุณเห็นข้อมูลที่เป็นความลับโผล่ออกมาในที่ที่ไม่ควรอยู่ นั่นแหละคือโอกาสที่คุณจะได้เรียนรู้และแก้ไขก่อนที่โปรเจกต์จะถูกนำไปใช้งานจริง
การเป็นโปรแกรมเมอร์ที่เก่ง ไม่ใช่คนที่เขียนโค้ดได้เร็วที่สุด แต่คือคนที่ ตรวจสอบและรับผิดชอบ ต่อสิ่งที่ตัวเองเขียนได้ดีที่สุด เริ่มฝึกนิสัยนี้ตั้งแต่วันนี้ แล้วคุณจะพบว่าโค้ดที่คุณเขียนจะมีความมั่นคงและน่าเชื่อถือมากขึ้นอย่างก้าวกระโดดครับ
ที่มา: I pentested my own AI hub and shipped the method, not the map — DEV Community