การแทรก SQL

หมวดหมู่ 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() เพื่อตรวจสอบว่าคอลัมน์อยู่ในรายการที่อนุญาตใน setProjectionMap
  • setStrictGrammar() เพื่อจํากัดคําค้นหาย่อย

ใช้คลัง Room

แพ็กเกจ android.database.sqlite มี API ที่จําเป็นสําหรับการใช้ฐานข้อมูลใน Android อย่างไรก็ตาม วิธีการนี้ต้องใช้การเขียนโค้ดระดับล่างและไม่มีการยืนยันการค้นหา SQL ดิบในคอมไพล์ เมื่อกราฟข้อมูลมีการเปลี่ยนแปลง คุณจะต้องอัปเดตการค้นหา SQL ที่ได้รับผลกระทบด้วยตนเอง ซึ่งเป็นกระบวนการที่ใช้เวลานานและเกิดข้อผิดพลาดได้ง่าย

โซลูชันระดับสูงคือการใช้ไลบรารีการคงข้อมูลของ Room เป็นเลเยอร์การแยกแยะสำหรับฐานข้อมูล SQLite ฟีเจอร์ของห้องพักประกอบด้วย

  • คลาสฐานข้อมูลที่ใช้เป็นจุดเข้าใช้งานหลักสำหรับเชื่อมต่อกับข้อมูลที่เก็บถาวรของแอป
  • เอนทิตีข้อมูลที่แสดงถึงตารางของฐานข้อมูล
  • ออบเจ็กต์การเข้าถึงข้อมูล (DAO) ซึ่งให้เมธอดที่แอปสามารถใช้เพื่อค้นหา อัปเดต แทรก และลบข้อมูล

สิทธิประโยชน์ของ Room มีดังนี้

  • การยืนยันการค้นหา SQL ขณะคอมไพล์
  • การลดโค้ดเทมเพลตที่ทำให้เกิดข้อผิดพลาด
  • ย้ายข้อมูลฐานข้อมูลที่มีประสิทธิภาพมากขึ้น

แนวทางปฏิบัติแนะนำ

การแทรก SQL เป็นรูปแบบการโจมตีที่มีประสิทธิภาพซึ่งอาจทำให้ระบบไม่สามารถรับมือได้ทั้งหมด โดยเฉพาะกับแอปพลิเคชันขนาดใหญ่และซับซ้อน คุณควรพิจารณาด้านความปลอดภัยเพิ่มเติมเพื่อจำกัดความรุนแรงของข้อบกพร่องที่อาจเกิดขึ้นในอินเทอร์เฟซข้อมูล ซึ่งรวมถึงสิ่งต่อไปนี้

  • แฮชแบบเข้ารหัสทางเดียวและแบบเพิ่ม Salt ที่มีประสิทธิภาพเพื่อเข้ารหัสรหัสผ่าน
    • AES 256 บิตสําหรับแอปพลิเคชันเชิงพาณิชย์
    • ขนาดคีย์สาธารณะ 224 หรือ 256 บิตสําหรับวิทยาการเข้ารหัสลับโดยใช้เส้นโค้ง
  • การจํากัดสิทธิ์
  • การจัดโครงสร้างรูปแบบข้อมูลอย่างแม่นยำและยืนยันว่าข้อมูลเป็นไปตามรูปแบบที่คาดไว้
  • หลีกเลี่ยงการจัดเก็บข้อมูลส่วนบุคคลหรือข้อมูลที่ละเอียดอ่อนของผู้ใช้ หากเป็นไปได้ (เช่น การใช้ตรรกะแอปพลิเคชันโดยการแฮชแทนการส่งหรือจัดเก็บข้อมูล)
  • ลดจำนวน API และแอปพลิเคชันของบุคคลที่สามที่เข้าถึงข้อมูลที่ละเอียดอ่อน

แหล่งข้อมูล