การแทรก SQL

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

ใช้ไลบรารี Room

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

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

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

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

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

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

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

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

แหล่งข้อมูล