หมวดหมู่ OWASP: MASVS-CODE: คุณภาพโค้ด
ภาพรวม
การแทรก SQL จะใช้ประโยชน์จากแอปพลิเคชันที่เปราะบางโดยการแทรกโค้ดลงในคำสั่ง SQL เพื่อเข้าถึงฐานข้อมูลที่สําคัญนอกเหนือจากอินเทอร์เฟซที่เปิดเผยโดยเจตนา การโจมตีอาจเปิดเผยข้อมูลส่วนตัว ทำให้เนื้อหาในฐานข้อมูลเสียหาย และอาจทำให้โครงสร้างพื้นฐานแบ็กเอนด์ถูกบุกรุก
SQL อาจเสี่ยงต่อการแทรกผ่านคำค้นหาที่สร้างแบบไดนามิกโดยการต่อข้อมูลป้อนเข้าของผู้ใช้ก่อนดำเนินการ การแทรก SQL มักปรากฏในรายการความเสี่ยงด้านความปลอดภัย 10 อันดับสูงสุดของ OWASP สำหรับช่องโหว่ของเว็บ โดยมุ่งเป้าไปที่เว็บ อุปกรณ์เคลื่อนที่ และแอปพลิเคชันฐานข้อมูล SQL ผู้โจมตีใช้เทคนิคนี้ในการละเมิดข้อมูลระดับสูงหลายครั้ง
ในตัวอย่างนี้ อินพุตที่ผู้ใช้ป้อนลงในช่องหมายเลขคำสั่งซื้อซึ่งไม่มีการหลีกค่าจะถูกแทรกลงในสตริง SQL และตีความเป็นการค้นหาต่อไปนี้
SELECT * FROM users WHERE email = 'example@example.com' AND order_number = '251542'' LIMIT 1
โค้ดดังกล่าวจะสร้างข้อผิดพลาดด้านไวยากรณ์ฐานข้อมูลในเว็บคอนโซล ซึ่งแสดงให้เห็นว่าแอปพลิเคชันอาจเสี่ยงต่อการถูกแทรก SQL การเปลี่ยนหมายเลขคำสั่งซื้อเป็น 'OR 1=1–
หมายความว่าการตรวจสอบสิทธิ์สำเร็จเนื่องจากฐานข้อมูลประเมินคำสั่งเป็น True
เนื่องจาก 1 เท่ากับ 1 เสมอ
ในทํานองเดียวกัน การค้นหานี้จะแสดงแถวทั้งหมดจากตาราง
SELECT * FROM purchases WHERE email='admin@app.com' OR 1=1;
ผู้ให้บริการเนื้อหา
ผู้ให้บริการเนื้อหามีกลไกพื้นที่เก็บข้อมูลที่มีโครงสร้างซึ่งสามารถจํากัดไว้สําหรับแอปพลิเคชันเดียว หรือส่งออกเพื่อแชร์กับแอปอื่นๆ คุณควรตั้งค่าสิทธิ์ตามหลักการให้สิทธิ์ขั้นต่ำที่สุด โดย ContentProvider
ที่ส่งออกจะมีสิทธิ์ที่ระบุไว้เพียงสิทธิ์เดียวสําหรับการอ่านและการเขียน
โปรดทราบว่าการแทรก SQL บางรายการไม่ได้นำไปสู่การแสวงหาประโยชน์ ผู้ให้บริการเนื้อหาบางรายให้สิทธิ์ผู้อ่านเข้าถึงฐานข้อมูล SQLite อย่างเต็มรูปแบบอยู่แล้ว ความสามารถในการดำเนินการค้นหาแบบกำหนดเองจึงให้ประโยชน์เพียงเล็กน้อย รูปแบบที่อาจแสดงถึงปัญหาด้านความปลอดภัย ได้แก่
- ผู้ให้บริการเนื้อหาหลายรายที่ใช้ไฟล์ฐานข้อมูล SQLite ไฟล์เดียวร่วมกัน
- ในกรณีนี้ แต่ละตารางอาจกำหนดไว้สำหรับผู้ให้บริการเนื้อหาที่ไม่ซ้ำกัน การแทรก SQL ที่สำเร็จใน Content Provider รายการหนึ่งจะมอบสิทธิ์เข้าถึงตารางอื่นๆ
- ผู้ให้บริการเนื้อหามีสิทธิ์หลายรายการสำหรับเนื้อหาภายในฐานข้อมูลเดียวกัน
- การใช้ SQL Injection ในผู้ให้บริการเนื้อหารายเดียวที่ให้สิทธิ์เข้าถึงในระดับสิทธิ์ที่แตกต่างกันอาจนำไปสู่การข้ามการตั้งค่าความปลอดภัยหรือความเป็นส่วนตัวในเครื่อง
ผลกระทบ
การใช้ SQL Injection อาจเปิดเผยข้อมูลที่ละเอียดอ่อนของผู้ใช้หรือแอปพลิเคชัน ลบล้างข้อจำกัดการตรวจสอบสิทธิ์และการให้สิทธิ์ และทำให้ฐานข้อมูลเสี่ยงต่อการถูกลบหรือเสียหาย ผลกระทบอาจรวมถึงผลกระทบที่อันตรายและส่งผลถาวรต่อผู้ใช้ที่มีข้อมูลส่วนบุคคลถูกเปิดเผย ผู้ให้บริการแอปและบริการอาจสูญเสียทรัพย์สินทางปัญญาหรือความไว้วางใจของผู้ใช้
การลดปัญหา
พารามิเตอร์ที่เปลี่ยนได้
การใช้ ?
เป็นพารามิเตอร์แบบแทนที่ได้ในวลีการเลือกและอาร์เรย์ที่แยกต่างหากของอาร์กิวเมนต์การเลือกจะเชื่อมโยงอินพุตของผู้ใช้กับการค้นหาโดยตรงแทนที่จะตีความว่าเป็นส่วนหนึ่งของคำสั่ง SQL
Kotlin
// Constructs a selection clause with a replaceable parameter.
val selectionClause = "var = ?"
// Sets up an array of arguments.
val selectionArgs: Array<String> = arrayOf("")
// Adds values to the selection arguments array.
selectionArgs[0] = userInput
Java
// Constructs a selection clause with a replaceable parameter.
String selectionClause = "var = ?";
// Sets up an array of arguments.
String[] selectionArgs = {""};
// Adds values to the selection arguments array.
selectionArgs[0] = userInput;
ระบบจะเชื่อมโยงข้อมูลที่ผู้ใช้ป้อนกับข้อความค้นหาโดยตรงแทนที่จะถือว่าเป็น SQL ซึ่งจะช่วยป้องกันการแทรกโค้ด
ต่อไปนี้คือตัวอย่างที่ละเอียดยิ่งขึ้นซึ่งแสดงการค้นหาของแอปช็อปปิ้งเพื่อดึงข้อมูลรายละเอียดการซื้อด้วยพารามิเตอร์ที่เปลี่ยนได้
Kotlin
fun validateOrderDetails(email: String, orderNumber: String): Boolean {
val cursor = db.rawQuery(
"select * from purchases where EMAIL = ? and ORDER_NUMBER = ?",
arrayOf(email, orderNumber)
)
val bool = cursor?.moveToFirst() ?: false
cursor?.close()
return bool
}
Java
public boolean validateOrderDetails(String email, String orderNumber) {
boolean bool = false;
Cursor cursor = db.rawQuery(
"select * from purchases where EMAIL = ? and ORDER_NUMBER = ?",
new String[]{email, orderNumber});
if (cursor != null) {
if (cursor.moveToFirst()) {
bool = true;
}
cursor.close();
}
return bool;
}
ใช้ออบเจ็กต์ PreparedStatement
อินเทอร์เฟซ PreparedStatement
จะคอมไพล์คำสั่ง SQL เป็นออบเจ็กต์ล่วงหน้า จากนั้นจึงสามารถเรียกใช้ได้อย่างมีประสิทธิภาพหลายครั้ง PreparedStatement ใช้ ?
เป็นตัวยึดตําแหน่งสําหรับพารามิเตอร์ ซึ่งจะทำให้การพยายามแทรกข้อมูลที่คอมไพล์แล้วต่อไปนี้ไม่มีประสิทธิภาพ
WHERE id=295094 OR 1=1;
ในกรณีนี้ ระบบจะอ่านคำสั่ง 295094 OR 1=1
เป็นค่าสำหรับรหัส ซึ่งอาจไม่แสดงผลลัพธ์ใดๆ ขณะที่การค้นหาแบบดิบจะตีความคำสั่ง OR 1=1
เป็นส่วนอื่นของประโยค WHERE
ตัวอย่างด้านล่างแสดงคำค้นหาที่มีพารามิเตอร์
Kotlin
val pstmt: PreparedStatement = con.prepareStatement(
"UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?").apply {
setString(1, "Barista")
setInt(2, 295094)
}
Java
PreparedStatement pstmt = con.prepareStatement(
"UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?");
pstmt.setString(1, "Barista")
pstmt.setInt(2, 295094)
ใช้เมธอดการค้นหา
ในตัวอย่างที่ยาวกว่านี้ ระบบจะรวม selection
และ selectionArgs
ของเมธอด query()
เข้าด้วยกันเพื่อสร้างประโยค WHERE
เนื่องจากมีการระบุอาร์กิวเมนต์แยกกัน ระบบจึงจะหลีกหนีอาร์กิวเมนต์ก่อนที่จะรวมกันเพื่อป้องกันการแทรก SQL
Kotlin
val db: SQLiteDatabase = dbHelper.getReadableDatabase()
// Defines a projection that specifies which columns from the database
// should be selected.
val projection = arrayOf(
BaseColumns._ID,
FeedEntry.COLUMN_NAME_TITLE,
FeedEntry.COLUMN_NAME_SUBTITLE
)
// Filters results WHERE "title" = 'My Title'.
val selection: String = FeedEntry.COLUMN_NAME_TITLE.toString() + " = ?"
val selectionArgs = arrayOf("My Title")
// Specifies how to sort the results in the returned Cursor object.
val sortOrder: String = FeedEntry.COLUMN_NAME_SUBTITLE.toString() + " DESC"
val cursor = db.query(
FeedEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return
// (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // Don't group the rows
null, // Don't filter by row groups
sortOrder // The sort order
).use {
// Perform operations on the query result here.
it.moveToFirst()
}
Java
SQLiteDatabase db = dbHelper.getReadableDatabase();
// Defines a projection that specifies which columns from the database
// should be selected.
String[] projection = {
BaseColumns._ID,
FeedEntry.COLUMN_NAME_TITLE,
FeedEntry.COLUMN_NAME_SUBTITLE
};
// Filters results WHERE "title" = 'My Title'.
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };
// Specifies how to sort the results in the returned Cursor object.
String sortOrder =
FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";
Cursor cursor = db.query(
FeedEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
sortOrder // The sort order
);
ใช้ SQLiteQueryBuilder ที่กําหนดค่าอย่างถูกต้อง
นักพัฒนาแอปพลิเคชันสามารถปกป้องแอปพลิเคชันเพิ่มเติมได้โดยใช้ SQLiteQueryBuilder
ซึ่งเป็นคลาสที่ช่วยสร้างการค้นหาที่จะส่งไปยังออบเจ็กต์ SQLiteDatabase
การกำหนดค่าที่แนะนำ ได้แก่
- โหมด
setStrict()
สําหรับการตรวจสอบการค้นหา setStrictColumns()
เพื่อตรวจสอบว่าคอลัมน์อยู่ในรายการที่อนุญาตใน setProjectionMapsetStrictGrammar()
เพื่อจํากัดคําค้นหาย่อย
ใช้คลัง Room
แพ็กเกจ android.database.sqlite
มี API ที่จําเป็นสําหรับการใช้ฐานข้อมูลใน Android อย่างไรก็ตาม วิธีการนี้ต้องใช้การเขียนโค้ดระดับล่างและไม่มีการยืนยันการค้นหา SQL ดิบในคอมไพล์ เมื่อกราฟข้อมูลมีการเปลี่ยนแปลง คุณจะต้องอัปเดตการค้นหา SQL ที่ได้รับผลกระทบด้วยตนเอง ซึ่งเป็นกระบวนการที่ใช้เวลานานและเกิดข้อผิดพลาดได้ง่าย
โซลูชันระดับสูงคือการใช้ไลบรารีการคงข้อมูลของ Room เป็นเลเยอร์การแยกแยะสำหรับฐานข้อมูล SQLite ฟีเจอร์ของห้องพักประกอบด้วย
- คลาสฐานข้อมูลที่ใช้เป็นจุดเข้าใช้งานหลักสำหรับเชื่อมต่อกับข้อมูลที่เก็บถาวรของแอป
- เอนทิตีข้อมูลที่แสดงถึงตารางของฐานข้อมูล
- ออบเจ็กต์การเข้าถึงข้อมูล (DAO) ซึ่งให้เมธอดที่แอปสามารถใช้เพื่อค้นหา อัปเดต แทรก และลบข้อมูล
สิทธิประโยชน์ของ Room มีดังนี้
- การยืนยันการค้นหา SQL ขณะคอมไพล์
- การลดโค้ดเทมเพลตที่ทำให้เกิดข้อผิดพลาด
- ย้ายข้อมูลฐานข้อมูลที่มีประสิทธิภาพมากขึ้น
แนวทางปฏิบัติแนะนำ
การแทรก SQL เป็นรูปแบบการโจมตีที่มีประสิทธิภาพซึ่งอาจทำให้ระบบไม่สามารถรับมือได้ทั้งหมด โดยเฉพาะกับแอปพลิเคชันขนาดใหญ่และซับซ้อน คุณควรพิจารณาด้านความปลอดภัยเพิ่มเติมเพื่อจำกัดความรุนแรงของข้อบกพร่องที่อาจเกิดขึ้นในอินเทอร์เฟซข้อมูล ซึ่งรวมถึงสิ่งต่อไปนี้
- แฮชแบบเข้ารหัสทางเดียวและแบบเพิ่ม Salt ที่มีประสิทธิภาพเพื่อเข้ารหัสรหัสผ่าน
- AES 256 บิตสําหรับแอปพลิเคชันเชิงพาณิชย์
- ขนาดคีย์สาธารณะ 224 หรือ 256 บิตสําหรับวิทยาการเข้ารหัสลับโดยใช้เส้นโค้ง
- การจํากัดสิทธิ์
- การจัดโครงสร้างรูปแบบข้อมูลอย่างแม่นยำและยืนยันว่าข้อมูลเป็นไปตามรูปแบบที่คาดไว้
- หลีกเลี่ยงการจัดเก็บข้อมูลส่วนบุคคลหรือข้อมูลที่ละเอียดอ่อนของผู้ใช้ หากเป็นไปได้ (เช่น การใช้ตรรกะแอปพลิเคชันโดยการแฮชแทนการส่งหรือจัดเก็บข้อมูล)
- ลดจำนวน API และแอปพลิเคชันของบุคคลที่สามที่เข้าถึงข้อมูลที่ละเอียดอ่อน