SQL Injection คืออะไร และจะป้องกันได้อย่างไร
การวิเคราะห์เชิงปฏิบัติของการโจมตี SQL injection พร้อมตัวอย่างจริงและวิธีแก้ไขที่ได้ผลจริงในโค้ดโปรดัคชัน
SQL injection มีมาตั้งแต่ปลายยุค 1990s และยังคงเป็นวิธีหนึ่งที่พบได้บ่อยที่สุดสำหรับการบุกรุกเว็บแอปพลิเคชัน ข้อบกพร่องนี้ง่ายเกินไป: ผู้โจมตีส่งข้อมูลซึ่งถูกตีความว่าเป็นโค้ด SQL แทนที่จะเป็นข้อมูลธรรมดา และฐานข้อมูลจะทำสิ่งที่ไม่ควรจะทำ
วิธีการโจมตีที่แท้จริง
ลองนึกภาพฟอร์มล็อกอินที่สร้างคำขอแบบนี้ใน PHP:
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
หากแอปพลิเคชันไม่ตัดสินใจกลั่นกรองข้อมูลเข้า ผู้โจมตีจะพิมพ์ admin' -- ลงในช่องชื่อผู้ใช้ คำขอจะกลายเป็น:
SELECT * FROM users WHERE username = 'admin' --' AND password = ''
-- ทำให้ส่วนที่เหลือของบรรทัดกลายเป็นหมายเหตุ จึงไม่มีการตรวจสอบรหัสผ่าน นี่คือการบายพาสการยืนยันตัวตนแบบคลาสสิก ผู้โจมตียังใช้ UNION SELECT เพื่อดึงข้อมูลจากตารางอื่น หรือเรียงคำขอหลายคำด้วยเซมิโคลอนเพื่อรันสิ่งที่ทำลายล้างเช่น DROP TABLE users; ถ้าไดรเวอร์อนุญาตให้ใช้คำสั่งหลายคำ
มีอีกวิธีหนึ่งคือ blind SQL injection ซึ่งแอปพลิเคชันไม่แสดงผลลัพธ์คำขอโดยตรง ผู้โจมตีสรุปข้อมูลผ่านการวัดเวลา (SLEEP(5) ใน MySQL) หรือการตอบสนองแบบบูลีน (หน้าเว็บทำงานต่างกันเมื่อเงื่อนไขเป็นจริงหรือเท็จหรือไม่) เครื่องมือเช่น sqlmap ทำให้กระบวนการสกัดข้อมูลอัตโนมัติเมื่อพบพารามิเตอร์ที่มีช่องโหว่
เหตุใดการเชื่อมต่อสตริงจึงเป็นปัญหาหลัก
ทุกรูปแบบของการโจมตีนี้กลับไปสู่สิ่งเดียว: การผสมโค้ดและข้อมูลในสตริงเดียวกัน ฐานข้อมูลไม่สามารถแยกแยะความแตกต่างระหว่างค่าที่ถูกต้องและไวยากรณ์ที่ถูกฉีดเข้ามา เพราะมันเข้ามาผ่านช่องทางเดียวกัน การหลุดออกจากการอ้างอิงช่วยได้ในบางกรณี แต่มันอ่อนแอ — การเข้ารหัสที่แตกต่างกัน การฉีดลำดับที่สอง (ข้อมูลเก็บไว้ครั้งหนึ่ง จากนั้นนำมาใช้อีกครั้งอย่างไม่ปลอดภัย) และความแปลกประหลาดเฉพาะไดรเวอร์ทั้งหมดสร้างเส้นทางบายพาส การหลุดออกจากการอ้างอิงเป็นแพตช์ไม่ใช่การแก้ไข
คำขอแบบพารามิเตอร์เป็นการแก้ไขที่แท้จริง
การแก้ไขคือการแยกโค้ด SQL ออกจากข้อมูลผู้ใช้ที่ระดับไดรเวอร์ โดยใช้คำขอแบบพารามิเตอร์ (เรียกอีกอย่างว่า prepared statements) ฐานข้อมูลจะได้รับโครงสร้างคำขอก่อน จากนั้นจึงผูกค่าต่อมา ดังนั้นข้อมูลเข้าจากผู้ใช้จึงไม่สามารถเปลี่ยนความหมายของคำขอได้
ใน Python ด้วย psycopg2:
cur.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))
ใน Node.js ด้วย mysql2:
connection.execute('SELECT * FROM users WHERE username = ? AND password = ?', [username, password]);
ใน Java ด้วย JDBC:
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?");
stmt.setString(1, username);
stmt.setString(2, password);
สังเกตรูปแบบ: ตัวยึดตำแหน่ง (%s, ?) สำรองตำแหน่งสำหรับข้อมูล และค่าจริงจะถูกส่งแยกต่างหาก ไม่มีการเชื่อมต่อสตริง ไม่จำเป็นต้องหลุดออกจากการอ้างอิงด้วยตัวเอง วิธีนี้ใช้ได้กับส่วนใหญ่ของคำขอที่คุณจะเขียนในแอปพลิเคชันปกติ
จะเป็นอย่างไรกับชื่อตารางหรือชื่อคอลัมน์ที่มีการเปลี่ยนแปลง?
การกำหนดพารามิเตอร์จัดการกับค่า แต่ไม่จัดการกับตัวระบุ — คุณไม่สามารถผูกชื่อตารางเป็นพารามิเตอร์ได้ ถ้าแอปพลิเคชันของคุณต้องเลือกตารางแบบไดนามิก (드물고มักจะเป็นเครื่องหมายการออกแบบที่ไม่ดี) ให้ใช้รายชื่อที่อนุญาตเพียงค่าเสีย ที่กำหนดแทนการไว้วางใจข้อมูลเข้าจากผู้ใช้โดยตรง:
allowed_tables = {'orders', 'invoices', 'customers'}
if table_name not in allowed_tables:
raise ValueError("Invalid table")
ไม่ต้องสร้างชื่อตัวระบุผ่านการจัดรูปแบบสตริงจากข้อมูลเข้าจากผู้ใช้ แม้จะหลุดออกจากการอ้างอิง
ปกป้องชั้นนอกเหนือจากคำขอนั้นเอง
คำขอแบบพารามิเตอร์เป็นการควบคุมหลัก แต่มีสิ่งอื่นๆ อีกสองสามอย่างที่สำคัญ:
- สิทธิ์ที่จำกัดที่สุดบนบัญชีฐานข้อมูล บัญชีผู้ใช้ DB ของแอปพลิเคชันไม่ควรมี
DROP,ALTERหรือการเข้าถึงสกีมาที่ไม่เกี่ยวข้อง หากการฉีดเข้ามาหลุดออกไป สิทธิ์ที่จำกัดจะจำกัดความเสียหาย - ORM ช่วยให้ปลอดภัยตามค่าเริ่มต้น Django ORM, SQLAlchemy และ Hibernate ทั้งหมดกำหนดพารามิเตอร์คำขออัตโนมัติเมื่อคุณใช้วิธีการสร้างคำขอมาตรฐาน ความเสี่ยงกลับมาเมื่อผู้พัฒนาโยนตัวเองลงในโค้ด SQL ดิบหรือใช้การเรียก
.extra()/text()ด้วยการแทรกสตริง — ดังนั้นจึงควรตรวจสอบจุดเหล่านั้นโดยเฉพาะ - การตรวจสอบข้อมูลเข้าเป็นเลเยอร์ทุติยภูมิ ไม่ใช่การแทนที่ การตรวจสอบว่าช่องอีเมลดูเหมือนอีเมลหรือไม่เป็นวิธีปฏิบัติที่ดี แต่มันจะไม่หยุดการฉีดเข้ามาด้วยตัวเอง — ผู้โจมตีค้นหาเพย์โลดที่สร้างสรรค์ที่ยังคงผ่านการตรวจสอบที่ไม่เข้มงวด
- WAF สามารถจับรูปแบบการโจมตีที่ทราบได้ แต่มันเป็นเลเยอร์การตรวจจับ ไม่ใช่การแก้ไขสำหรับโค้ดพื้นฐาน
ทดสอบโค้ดของคุณเองสำหรับข้อบกพร่องนี้
เรียกคำขอของคุณผ่านเครื่องมือวิเคราะห์แบบสแตติก (Bandit สำหรับ Python, Semgrep พร้อมชุดกฎ SQL injection) เป็นส่วนหนึ่งของ CI สำหรับการทดสอบด้วยตนเอง ลองฉีดเข้าไปอัญประกาศเดียว (') ลงในทุกช่องข้อมูลเข้าและดูข้อความข้อผิดพลาด SQL ที่รั่วไหลในการตอบกลับ — นั่นมักจะเป็นสัญญาณแรกว่าคำขอไม่ได้รับการกำหนดพารามิเตอร์
ถ้าคุณต้องการเจาะลึกเรื่องนี้ Korra Studio's Web Security track ครอบคลุมการฉีดเข้ามาควบคู่ไป กับ XSS และการบายพาส auth และส่วน Databases อธิบายรูปแบบการออกแบบคำขอที่หลีกเลี่ยงคลาสข้อบกพร่องนี้อย่างสิ้นเชิง
เขียนด้วยความช่วยเหลือของ AI ตรวจสอบและเผยแพร่โดย Michal Pilch (CISSP), Korra Studio
นี่คือบันทึกหนึ่งจากฐานความรู้ของ Korra Studio — แพลตฟอร์มจับคู่หัวข้อแต่ละหัวข้อกับการฝึกสอนแบบ 1-to-1
เริ่มใช้งานฟรีarrow_forward