หมวดหมู่ OWASP: MASVS-CODE: คุณภาพของโค้ด
ภาพรวม
การแทรก SQL จะใช้ประโยชน์จากแอปพลิเคชันที่มีช่องโหว่โดยการแทรกโค้ดลงในคำสั่ง SQL เพื่อเข้าถึงฐานข้อมูลพื้นฐานที่อยู่นอกเหนืออินเทอร์เฟซที่ตั้งใจเปิดเผย การโจมตีอาจเปิดเผยข้อมูลส่วนตัว สร้างความเสียหายให้กับเนื้อหาในฐานข้อมูล และแม้แต่ทำให้โครงสร้างพื้นฐานแบ็กเอนด์ถูกบุกรุก
SQL อาจเสี่ยงต่อการแทรกผ่านการค้นหาที่สร้างขึ้นแบบไดนามิกโดยการต่อข้อมูลจากผู้ใช้ก่อนการดำเนินการ การแทรก SQL มักจะอยู่ในOWASP Top Ten ของช่องโหว่บนเว็บ โดยกำหนดเป้าหมายไปที่เว็บ อุปกรณ์เคลื่อนที่ และแอปพลิเคชันฐานข้อมูล SQL ผู้โจมตีใช้เทคนิคนี้ในการละเมิดที่มีชื่อเสียงหลายครั้ง
ในตัวอย่างพื้นฐานนี้ ระบบจะแทรกอินพุตที่ผู้ใช้ป้อนลงในช่องหมายเลขคำสั่งซื้อลงในสตริง SQL และตีความเป็นคำค้นหาต่อไปนี้
SELECT * FROM users WHERE email = 'example@example.com' AND order_number = '251542'' LIMIT 1
โค้ดดังกล่าวจะสร้างข้อผิดพลาดทางไวยากรณ์ของฐานข้อมูลในเว็บคอนโซล ซึ่งแสดงว่าแอปพลิเคชันอาจเสี่ยงต่อการโจมตีด้วย SQL Injection การแทนที่หมายเลขคำสั่งซื้อด้วย 'OR 1=1– หมายความว่าสามารถทำการตรวจสอบสิทธิ์ได้เนื่องจากฐานข้อมูลประเมินคำสั่งเป็น True เนื่องจาก 1 เท่ากับ 1 เสมอ
ในทำนองเดียวกัน การค้นหานี้จะแสดงแถวทั้งหมดจากตาราง
SELECT * FROM purchases WHERE email='admin@app.com' OR 1=1;
ผู้ให้บริการเนื้อหา
ผู้ให้บริการเนื้อหามีกลไกการจัดเก็บที่มีโครงสร้างซึ่งจำกัดไว้สำหรับแอปพลิเคชันหรือส่งออกเพื่อแชร์กับแอปอื่นๆ ได้ ควรตั้งค่าสิทธิ์ตามหลักการให้สิทธิ์ขั้นต่ำที่สุด โดย ContentProvider ที่ส่งออกจะมีสิทธิ์ที่ระบุเดียวสำหรับการอ่านและการเขียนได้
โปรดทราบว่าการแทรก SQL บางรายการไม่ได้นำไปสู่การแสวงหาประโยชน์ ผู้ให้บริการเนื้อหาบางรายอนุญาตให้ผู้อ่านเข้าถึงฐานข้อมูล SQLite ได้อย่างสมบูรณ์อยู่แล้ว การเรียกใช้การค้นหาที่กำหนดเองจึงไม่ค่อยมีประโยชน์ รูปแบบที่อาจแสดงถึงปัญหาด้านความปลอดภัย ได้แก่
- ผู้ให้บริการเนื้อหาหลายรายแชร์ไฟล์ฐานข้อมูล SQLite เดียวกัน
- ในกรณีนี้ ตารางแต่ละตารางอาจมีไว้สำหรับผู้ให้บริการเนื้อหาที่ไม่ซ้ำกัน การแทรก SQL ที่สำเร็จใน ContentProvider รายการหนึ่งจะให้สิทธิ์เข้าถึงตารางอื่นๆ
- ผู้ให้บริการเนื้อหามีสิทธิ์หลายอย่างสำหรับเนื้อหาภายในฐานข้อมูลเดียวกัน
- การแทรก SQL ในผู้ให้บริการเนื้อหารายเดียวที่ให้สิทธิ์เข้าถึงด้วยสิทธิ์ระดับต่างๆ อาจทําให้เกิดการข้ามการตั้งค่าความปลอดภัยหรือความเป็นส่วนตัวในเครื่อง
ผลกระทบ
การแทรก SQL อาจเปิดเผยข้อมูลที่ละเอียดอ่อนของผู้ใช้หรือแอปพลิเคชัน ข้ามข้อจำกัดในการตรวจสอบสิทธิ์และการให้สิทธิ์ และทำให้ฐานข้อมูลเสี่ยงต่อการเสียหายหรือถูกลบ ผลกระทบอาจรวมถึงผลกระทบที่อันตรายและยาวนานต่อผู้ใช้ที่ข้อมูลส่วนบุคคลถูกเปิดเผย ผู้ให้บริการแอปและบริการมีความเสี่ยงที่จะสูญเสียทรัพย์สินทางปัญญาหรือความไว้วางใจของผู้ใช้
การลดปัญหา
พารามิเตอร์ที่แทนที่ได้
การใช้ ? เป็นพารามิเตอร์แบบแทนที่ได้ในวลีการเลือกและอาร์เรย์ที่แยกต่างหากของอาร์กิวเมนต์การเลือกจะเชื่อมโยงข้อมูลจากผู้ใช้กับการค้นหาโดยตรง แทนที่จะตีความว่าเป็นส่วนหนึ่งของคำสั่ง 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 ฟีเจอร์ของ Spaces ประกอบด้วย
- คลาสฐานข้อมูลซึ่งทำหน้าที่เป็นจุดเข้าใช้งานหลักสำหรับการเชื่อมต่อกับข้อมูลที่คงอยู่ของแอป
- เอนทิตีข้อมูลที่แสดงตารางของฐานข้อมูล
- ออบเจ็กต์การเข้าถึงข้อมูล (DAO) ซึ่งมีเมธอดที่แอปใช้เพื่อค้นหา อัปเดต แทรก และลบข้อมูลได้
สิทธิประโยชน์ของห้องมีดังนี้
- การยืนยันการค้นหา SQL ในเวลาคอมไพล์
- ลดโค้ด Boilerplate ที่มีแนวโน้มที่จะเกิดข้อผิดพลาด
- การย้ายฐานข้อมูลที่มีประสิทธิภาพ
แนวทางปฏิบัติแนะนำ
การแทรก SQL เป็นการโจมตีที่ร้ายแรงซึ่งอาจป้องกันได้ยาก โดยเฉพาะกับแอปพลิเคชันขนาดใหญ่และซับซ้อน ควรพิจารณาด้านความปลอดภัยเพิ่มเติมเพื่อจำกัดความรุนแรงของข้อบกพร่องที่อาจเกิดขึ้นในอินเทอร์เฟซข้อมูล ซึ่งรวมถึง
- แฮชแบบทางเดียวและแบบเพิ่ม Salt ที่มีประสิทธิภาพเพื่อเข้ารหัสรหัสผ่าน
- AES 256 บิตสำหรับแอปพลิเคชันเชิงพาณิชย์
- ขนาดคีย์สาธารณะ 224 หรือ 256 บิตสำหรับการเข้ารหัสลับโดยใช้เส้นโค้ง
- การจำกัดสิทธิ์
- การจัดรูปแบบข้อมูลอย่างแม่นยำและการยืนยันว่าข้อมูลเป็นไปตามรูปแบบที่คาดไว้
- หลีกเลี่ยงการจัดเก็บข้อมูลผู้ใช้ส่วนบุคคลหรือข้อมูลที่ละเอียดอ่อนหากเป็นไปได้ (เช่น การใช้ตรรกะของแอปพลิเคชันโดยการแฮชแทนการส่งหรือจัดเก็บข้อมูล)
- ลดการใช้ API และแอปพลิเคชันของบุคคลที่สามที่เข้าถึงข้อมูลที่ละเอียดอ่อน