SQL Injection คือประเภทของการโจมตีฐานข้อมูลแอปพลิเคชันที่ผู้โจมตีฉีดโค้ด SQL ที่เป็นอันตรายลงในพารามิเตอร์คำสั่ง ทำให้เข้าถึงข้อมูลโดยไม่ได้รับอนุญาตหรือความสามารถในการแก้ไขข้อมูล ตามข้อมูลของ OWASP (2025) SQL Injection ยังคงเป็นหนึ่งในช่องโหว่ที่สำคัญที่สามารถนำไปสู่การประนีประนอมฐานข้อมูลทั้งหมด การฉีดโค้ด SQL ช่วยให้ผู้โจมตีสามารถอ่าน แก้ไข และลบเรกคอร์ด และในบางกรณีสามารถเข้าถึงระบบปฏิบัติการของเซิร์ฟเวอร์
ประเด็นสำคัญ
SQL Injection คือช่องโหว่ที่เกิดขึ้นเมื่อแอปพลิเคชันสร้างคำสั่ง SQL โดย การต่อสตริง กับข้อมูลผู้ใช้ ผู้โจมตีส่งสตริงที่ถูกสร้างขึ้นเป็นพิเศษในพารามิเตอร์คำสั่ง ซึ่งเปลี่ยนโครงสร้างของคำสั่ง SQL แทนที่จะเป็นข้อมูล สตริงที่ถูกฉีดกลายเป็นส่วนหนึ่งของโค้ด SQL ทำให้ผู้โจมตีสามารถดำเนินการคำสั่งใดๆ ต่อฐานข้อมูลได้ ตาม รายงานการละเมิดข้อมูลของ Verizon (2025) SQL Injection ปรากฏใน 8% ของการละเมิดข้อมูลทั้งหมดที่ถูกสอบสวน
แม้จะเป็นช่องโหว่ที่รู้จักกันดี (กล่าวถึงครั้งแรกในช่วงปลายทศวรรษ 1990) SQL Injection ยังคงพบในแอปพลิเคชันสมัยใหม่ สาเหตุคือ ข้อผิดพลาดของมนุษย์: นักพัฒนาเขียนโค้ดด้วยการต่อสตริง โค้ดเก่าไม่ได้รับการปรับโครงสร้าง และเฟรมเวิร์ก ORM ถูกใช้อย่างไม่ถูกต้อง (เช่น คำสั่ง raw ที่มีการแทรกสตริง) ตามข้อมูลของ Veracode (2026) ประมาณ 14% ของแอปพลิเคชันที่ถูกสแกนทั้งหมดมีช่องโหว่ SQLi อย่างน้อยหนึ่งรายการ
SQL Injection ที่สำเร็จทำให้ผู้โจมตีมีความสามารถมากมาย: อ่านตารางฐานข้อมูลใดๆ รวมถึงแฮชรหัสผ่านและข้อมูลส่วนตัวของผู้ใช้; แก้ไขและลบเรกคอร์ด; ดำเนินการด้านการดูแลระบบ (DROP TABLE, TRUNCATE); และในบางการกำหนดค่า การดำเนินการคำสั่งระยะไกลผ่าน xp_cmdshell (MSSQL) หรือ INTO OUTFILE (MySQL) ผลที่ตามมาตั้งแต่การรั่วไหลของข้อมูลผู้ใช้จนถึงการสูญเสียการควบคุมระบบทั้งหมด
SQL Injection ถูกจำแนกตามวิธีการดึงข้อมูลจากฐานข้อมูล การเลือกวิธีการขึ้นอยู่กับวิธีที่แอปพลิเคชันจัดการกับผลลัพธ์คำสั่งและข้อความแสดงข้อผิดพลาด การจำแนกของ OWASP ระบุสามประเภทหลัก: In-band (การดึงข้อมูลผ่านช่องทางเดียวกัน), Inferential/Blind (การอนุมานเชิงตรรกะ) และ Out-of-band (การส่งข้อมูลผ่านช่องทางอื่น)
| ประเภท | วิธีการดึงข้อมูล | ความซับซ้อน | ความถี่ |
|---|---|---|---|
| In-band (คลาสสิก) | โดยตรงผ่านผลลัพธ์คำสั่ง | ต่ำ | สูง |
| Blind SQLi | การอนุมานเชิงตรรกะจากการตอบสนองของเซิร์ฟเวอร์ | สูง | ปานกลาง |
| Out-of-band | ผ่านช่องทางภายนอก (DNS, HTTP) | ปานกลาง | ต่ำ |
ประเภทที่พบบ่อยที่สุด ผู้โจมตีฉีดโค้ด SQL ลงในพารามิเตอร์คำสั่ง และผลลัพธ์ของการฉีดจะปรากฏโดยตรงในการตอบสนองของเซิร์ฟเวอร์ สองประเภทย่อย: Error-based (ผ่านข้อความแสดงข้อผิดพลาดของ DB) และ UNION-based (ผ่านตัวดำเนินการ UNION SELECT) Error-based ใช้ข้อมูลจากข้อความแสดงข้อผิดพลาด เช่น ข้อผิดพลาดไวยากรณ์ MySQL ที่อาจเปิดเผยชื่อตารางหรือโครงสร้างคำสั่ง UNION-based ช่วยให้รวมผลลัพธ์ของคำสั่งที่ถูกต้องกับข้อมูลจากตารางอื่นในฐานข้อมูล
ใช้เมื่อแอปพลิเคชันไม่แสดงผลลัพธ์คำสั่งหรือข้อความแสดงข้อผิดพลาด ผู้โจมตีถาม คำถามใช่/ไม่ใช่ โดยส่งคำสั่งที่มีเงื่อนไขเชิงตรรกะและวิเคราะห์ความแตกต่างในการตอบสนองของเซิร์ฟเวอร์ (เช่น เวลาตอบสนองหรือเนื้อหาเพจ) Time-based Blind SQLi ใช้ฟังก์ชันหน่วงเวลา (SLEEP, WAITFOR DELAY) เพื่อยืนยันเงื่อนไข — หากหน้าโหลดนานขึ้น เงื่อนไขเป็นจริง วิธีนี้ช้ามาก — การดึงข้อมูลเรกคอร์ดเดียวอาจใช้เวลาหลายชั่วโมง
# ตัวอย่าง Blind SQL Injection (ตามเวลา)
# หาก SQLi เสี่ยง SLEEP(2) จะดำเนินการหากเงื่อนไขเป็นจริง
import requests
import time
payload = "' OR IF(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a',SLEEP(2),0) -- "
url = "http://example.com/user?id=1" + payload
start = time.time()
response = requests.get(url)
elapsed = time.time() - start
# หากการตอบสนองมาหลังจาก >2 วินาที — อักษรตัวแรกของรหัสผ่านคือ 'a'
print(f"Time: {elapsed:.2f}s - First letter is 'a'" if elapsed > 2 else "First letter is not 'a'")
ข้อมูลถูกส่งไม่ผ่านการตอบสนอง HTTP แต่ผ่าน ช่องทางอื่น: คำสั่ง DNS, คำขอ HTTP ไปยังเซิร์ฟเวอร์ภายนอก, SMTP ใช้เมื่อแอปพลิเคชันไม่ส่งคืนผลลัพธ์คำสั่งหรือแสดงข้อผิดพลาด MySQL รองรับฟังก์ชัน LOAD_FILE() ซึ่งสามารถเริ่มต้นคำสั่ง DNS และ MSSQL มี xp_dirtree สำหรับส่งข้อมูลไปยังเซิร์ฟเวอร์ SMB ระยะไกล Out-of-band SQL Injection มีประสิทธิภาพแต่ต้องการการตั้งค่าเซิร์ฟเวอร์ผู้โจมตีเพิ่มเติมและฟังก์ชัน DB เฉพาะ
กลไกของ SQL Injection ขึ้นอยู่กับการที่ SQL ใช้ เครื่องหมายคำพูดสำหรับสตริงลิเทอรัล หากแอปพลิเคชันแทรกอินพุตผู้ใช้ลงในคำสั่ง SQL โดยตรงโดยไม่ escape ผู้โจมตีสามารถ “ปิด” สตริงและเพิ่มโค้ด SQL ใดๆ ตัวอย่างเช่น ในคำสั่ง SELECT * FROM users WHERE name = '$input' การแทรก ' OR '1'='1 จะเปลี่ยนเป็น SELECT * FROM users WHERE name = '' OR '1'='1' ซึ่งส่งคืนผู้ใช้ทั้งหมด
พิจารณาแบบฟอร์มเข้าสู่ระบบด้วยคำสั่ง SELECT * FROM users WHERE username = '$user' AND password = '$pass' หากผู้โจมตีป้อน admin' -- ในช่องชื่อผู้ใช้และปล่อยรหัสผ่านว่าง คำสั่งที่ได้จะกลายเป็น SELECT * FROM users WHERE username = 'admin' -- ' AND password = '' อักขระ -- จะคอมเมนต์ส่วนที่เหลือของคำสั่ง ปิดการตรวจสอบรหัสผ่าน เซิร์ฟเวอร์ส่งคืนเรกคอร์ดผู้ใช้ admin และผู้โจมตีเข้าสู่ระบบโดยไม่รู้รหัสผ่าน
# ตัวอย่าง SQL Injection — การหลบเลี่ยงการตรวจสอบสิทธิ์
# โค้ดที่เสี่ยง: การต่อสตริงโดยตรง
def login_vulnerable(username, password):
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
cursor.execute(query)
return cursor.fetchone() is not None
# username = "admin' --" ทำให้การตรวจสอบรหัสผ่านเป็นโมฆะ
# เวอร์ชันที่ปลอดภัย: คำสั่งที่กำหนดพารามิเตอร์
def login_secure(username, password):
query = "SELECT * FROM users WHERE username = %s AND password = %s"
cursor.execute(query, (username, password))
return cursor.fetchone() is not None
ตัวดำเนินการ UNION SELECT ช่วยให้รวมผลลัพธ์ของสองคำสั่ง SELECT หากผู้โจมตีพบพารามิเตอร์ที่เสี่ยง พวกเขาเพิ่ม UNION SELECT ด้วยคำสั่งจากตารางอื่น ตัวอย่าง: ' UNION SELECT username, password FROM admins -- เงื่อนไขความสำเร็จคือจำนวนคอลัมน์ในทั้งสองคำสั่งต้องเท่ากัน จำนวนคอลัมน์ถูกกำหนดผ่าน ORDER BY (การแทรก ' ORDER BY 1-- จากนั้น 2, 3... จนกว่าจะเกิดข้อผิดพลาด) เมื่อทราบจำนวนคอลัมน์ ผู้โจมตีแทรก UNION SELECT ด้วยจำนวนฟิลด์เท่ากัน
แอปพลิเคชันมือถือเผชิญกับ SQL Injection ทั้งฝั่งเซิร์ฟเวอร์ (API) และฝั่งไคลเอ็นต์ — ในฐานข้อมูลท้องถิ่น (SQLite, Realm) ในขณะที่ SQLi ฝั่งเซิร์ฟเวอร์ใน API มือถือคล้ายกับแอปพลิเคชันเว็บ ฐานข้อมูลท้องถิ่น สร้างเวกเตอร์เพิ่มเติม หากแอปพลิเคชันเก็บข้อมูลใน SQLite และดำเนินการคำสั่งด้วยการต่อสตริง ข้อมูลที่เป็นอันตรายที่เข้าสู่ฐานข้อมูลท้องถิ่นผ่าน API สามารถทำให้เกิด SQLi ในการประมวลผลภายหลัง
ฐานข้อมูล SQLite ท้องถิ่นบนอุปกรณ์ก็เสี่ยงต่อ SQL Injection เช่นกันหากแอปพลิเคชันสร้างคำสั่งโดยการต่อสตริง Content Providers บน Android และ Core Data บน iOS ใช้การกำหนดพารามิเตอร์เป็นค่าเริ่มต้น แต่คำสั่ง raw ต้องการความสนใจจากนักพัฒนา SQLite ไม่รองรับหลายคำสั่งที่คั่นด้วยอัฒภาค ซึ่งจำกัดตัวเลือกของผู้โจมตีแต่ไม่ได้ป้องกันการอ่านข้อมูลผ่านเงื่อนไข WHERE ใช้ selectionArgs บน Android และ NSPredicate พร้อมพารามิเตอร์บน iOS เสมอ
API ที่แอปพลิเคชันมือถือสื่อสารด้วยนั้นเสี่ยงเช่นเดียวกับเว็บเซิร์ฟเวอร์ใดๆ นักพัฒนาแอปมือถือมักคิดว่า SQLi เป็นปัญหาเฉพาะของแบ็กเอนด์ แต่ช่องโหว่เกิดขึ้นที่ปลายทาง API ที่ยอมรับพารามิเตอร์จากไคลเอ็นต์ การแบ่งแยกความรับผิดชอบ ไม่ได้ป้องกัน: หากนักพัฒนาแบ็กเอนด์ลืมกำหนดพารามิเตอร์คำสั่ง แอปมือถือของผู้ใช้จะกลายเป็นเวกเตอร์การโจมตี กำหนดให้แบ็กเอนด์ใช้ ORM หรือ prepared statements
// ตัวอย่าง SQL Injection ใน SQLite ท้องถิ่นบน Android
// โค้ดที่เสี่ยง: การต่อโดยตรง
fun getUserVulnerable(db: SQLiteDatabase, userId: String): Cursor {
return db.rawQuery(
"SELECT * FROM users WHERE id = $userId", null
)
}
// เวอร์ชันที่ปลอดภัย: การกำหนดพารามิเตอร์ผ่าน selectionArgs
fun getUserSecure(db: SQLiteDatabase, userId: String): Cursor {
return db.rawQuery(
"SELECT * FROM users WHERE id = ?",
arrayOf(userId)
)
}
การป้องกัน SQL Injection ขึ้นอยู่กับหลักการง่ายๆ: อย่าไว้วางใจอินพุตผู้ใช้ ในคำสั่ง SQL วิธีการที่เชื่อถือได้เพียงอย่างเดียวคือการกำหนดพารามิเตอร์คำสั่ง (prepared statements) ซึ่งโค้ด SQL และข้อมูลถูกส่งแยกกัน วิธีการอื่นๆ ทั้งหมด — การ escape, การตรวจสอบ, WAF — เป็นชั้นความปลอดภัยเพิ่มเติมแต่ไม่ได้แทนที่การกำหนดพารามิเตอร์ ตาม OWASP (2025) การกำหนดพารามิเตอร์ป้องกันการโจมตี SQL Injection ได้ 100%
เมื่อใช้ prepared statements คำสั่ง SQL จะถูก คอมไพล์ โดยเซิร์ฟเวอร์ DB ก่อนโดยไม่มีข้อมูล จากนั้นค่าพารามิเตอร์จะถูกส่งแยกกัน ฐานข้อมูลถือว่าพารามิเตอร์เป็นข้อมูล ไม่ใช่โค้ดที่สามารถดำเนินการได้ แม้ว่าผู้โจมตีจะส่ง ' OR '1'='1 ฐานข้อมูลจะตีความว่าเป็นค่าสตริง ไม่ใช่โค้ด SQL Prepared statements รองรับโดยภาษาและเฟรมเวิร์กสมัยใหม่ทั้งหมด: PDO ใน PHP, PreparedStatement ใน Java, cursor.execute ใน Python
ORM สมัยใหม่ (Hibernate, Entity Framework, SQLAlchemy, Room) ใช้การกำหนดพารามิเตอร์โดยอัตโนมัติเมื่อดำเนินการคำสั่ง เว้นแต่นักพัฒนาจะเปลี่ยนไปใช้ คำสั่ง raw อย่างไรก็ตาม ORM ไม่ได้ป้องกันอย่างสมบูรณ์: โครงสร้างเช่น @Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true) ใน JPA ต้องการการส่งพารามิเตอร์ผ่านพารามิเตอร์ที่มีชื่อ ไม่ใช่การต่อสตริง ตัวสร้างคำสั่ง (Knex, jOOQ) ก็กำหนดพารามิเตอร์คำสั่งเป็นค่าเริ่มต้นเช่นกันหากไม่ได้ใช้วิธีการ raw
การ escape อักขระพิเศษ (mysql_real_escape_string) เป็นวิธีการที่ล้าสมัยซึ่งไม่ป้องกัน SQL Injection ทุกประเภท ปัญหา: การ escape ขึ้นอยู่กับการเข้ารหัสและสามารถเลี่ยงได้เมื่อใช้ การเข้ารหัสแบบหลายไบต์ (เช่น GBK ในระบบเอเชีย) ใช้การ escape เฉพาะในโค้ดเก่าที่ไม่สามารถกำหนดพารามิเตอร์ได้ และใช้ร่วมกับการตรวจสอบชนิดอินพุตที่เข้มงวดเสมอ
| วิธีการ | ประสิทธิผล | คำแนะนำ |
|---|---|---|
| Prepared Statements | 100% | จำเป็นสำหรับทุกคำสั่ง |
| ORM (การใช้ที่ถูกต้อง) | 99% | แนะนำ |
| การ escape สตริง | 70% (ขึ้นอยู่กับการเข้ารหัส) | เฉพาะโค้ดเก่า |
| การตรวจสอบอินพุต (white-list) | 50% (เฉพาะตัวเลข) | เพิ่มเติม |
| WAF (Web Application Firewall) | 60% | เพิ่มเติม |
บัญชีแอปพลิเคชันควรมี สิทธิ์ที่จำเป็นน้อยที่สุด: SELECT, INSERT, UPDATE, DELETE — เฉพาะบนตารางที่แอปพลิเคชันต้องการจริงๆ ห้ามการใช้ DROP, TRUNCATE, CREATE สำหรับบัญชีแอปพลิเคชัน ซึ่งจำกัดความเสียหายแม้ในกรณีที่ SQL Injection สำเร็จ: ผู้โจมตีจะไม่สามารถลบตารางหรือดำเนินการด้านการดูแลระบบ
การทดสอบ SQL Injection เป็นประจำควรเป็นส่วนหนึ่งของ ไปป์ไลน์การพัฒนาที่ปลอดภัย การรวมกันของการวิเคราะห์แบบคงที่ การสแกนแบบไดนามิก และการทดสอบเจาะระบบด้วยตนเองให้ผลลัพธ์ที่ดีที่สุด ตาม รายงานความปลอดภัยทางไซเบอร์ของ Synopsys (2025) สแกนเนอร์อัตโนมัติพบช่องโหว่ SQLi ได้มากถึง 70% แต่การโจมตี Blind ที่ซับซ้อนต้องการการทดสอบด้วยตนเอง
สำหรับแอปพลิเคชันมือถือ การวิเคราะห์ SQLite ท้องถิ่น ก็สำคัญเช่นกัน: ตรวจสอบการเรียก rawQuery ทั้งหมด คำสั่ง ContentProvider และคำสั่ง Room ด้วย rawQuery เครื่องมือ: Android Studio Lint (ตรวจจับ SQLi ใน SQLite), MobSF (Mobile Security Framework) สำหรับการวิเคราะห์แบบคงที่และไดนามิกอัตโนมัติของ APK/IPA นอกจากนี้ยังแนะนำให้ทดสอบปลายทาง API ผ่าน sqlmap พร้อมการสกัดกั้นพร็อกซีของการรับส่งข้อมูลของแอปพลิเคชันมือถือ
คำถามที่พบบ่อย
SQL Injection โจมตีฐานข้อมูลเชิงสัมพันธ์ผ่านคำสั่ง SQL NoSQL Injection ส่งผลกระทบต่อฐานข้อมูลที่ไม่ใช่เชิงสัมพันธ์ (MongoDB, Couchbase) ผ่าน ตัวดำเนินการคำสั่ง ($gte, $ne, $where) ใน MongoDB การฉีดเป็นไปได้หากแอปพลิเคชันสร้างเอกสาร BSON จากสตริง JSON กลไกการป้องกันคล้ายกัน: การกำหนดพารามิเตอร์และการตรวจสอบชนิด
ค้นหาทุกตำแหน่งที่คำสั่ง SQL ถูกสร้างโดย การต่อสตริง กับข้อมูลผู้ใช้ มองหารูปแบบเช่น "SELECT ... WHERE id = " + userId หรือ f"UPDATE ... SET name = '{name}'" แต่ละบรรทัดดังกล่าวคือ SQL Injection ที่อาจเกิดขึ้น แทนที่ทั้งหมดด้วยคำสั่งที่กำหนดพารามิเตอร์หรือ prepared statements
เฟรมเวิร์ก ORM ป้องกันโดยอัตโนมัติเฉพาะเมื่อคุณใช้ วิธีการ Query Builder และ พารามิเตอร์ที่มีชื่อ หากคุณใช้คำสั่ง raw (nativeQuery ใน JPA, rawQuery ใน Room) การป้องกันจะไม่ทำงาน — คุณต้องส่งพารามิเตอร์ผ่านนิพจน์ที่เตรียมไว้ ไม่ใช่ผ่านการต่อสตริง
Second-Order SQL Injection คือการโจมตีที่ข้อมูลที่เป็นอันตรายถูก จัดเก็บในฐานข้อมูล อย่างปลอดภัย แต่ถูกใช้ในคำสั่งอื่นโดยไม่มีการ escape ตัวอย่างเช่น ผู้โจมตีลงทะเบียนด้วยชื่อผู้ใช้เช่น ' OR '1'='1 ข้อมูลถูกบันทึกเป็นสตริง — ไม่มีการโจมตีเมื่อลงทะเบียน แต่หากคำสั่งอื่นใช้ชื่อผู้ใช้ใน SQL โดยไม่มีการกำหนดพารามิเตอร์ การฉีดจะทำงาน
NoSQL Injection อาจเป็นอันตรายมากกว่าเนื่องจาก ความตระหนักของนักพัฒนาที่น้อยกว่า นักพัฒนารู้จัก SQL Injection และส่วนใหญ่ใช้ ORM แต่มีเพียงไม่กี่คนที่รู้จัก NoSQL Injection ใน MongoDB คำสั่งที่มีรูปแบบไม่ถูกต้องอาจส่งคืนเอกสารทั้งหมดในคอลเล็กชัน การป้องกันเหมือนกัน — prepared statements (การกำหนดพารามิเตอร์ BSON) และการตรวจสอบอินพุตที่เข้มงวด
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม