Chèn SQL

Danh mục OWASP: MASVS-CODE: Chất lượng mã

Tổng quan

Kỹ thuật chèn SQL khai thác các ứng dụng dễ bị tấn công bằng cách chèn mã vào câu lệnh SQL để truy cập vào cơ sở dữ liệu cơ bản, ngoài giao diện hiển thị có chủ đích của chúng. Hình thức tấn công này có thể làm lộ dữ liệu riêng tư, phá hoại nội dung trong cơ sở dữ liệu và thậm chí xâm phạm cơ sở hạ tầng phụ trợ.

SQL có thể dễ chèn thông qua các truy vấn được tạo động bằng cách nối hoạt động đầu vào của người dùng trước khi thực thi. Nhắm mục tiêu đến web, thiết bị di động và bất kỳ ứng dụng cơ sở dữ liệu SQL nào, kỹ thuật chèn SQL thường được liệt vào OWASP Top 10 lỗ hổng bảo mật web. Những kẻ tấn công đã sử dụng kỹ thuật này trong một số vụ vi phạm tầm cỡ.

Trong ví dụ cơ bản này, thông tin không thoát mà người dùng nhập vào ô mã đơn đặt hàng có thể được chèn vào chuỗi SQL và được hiểu là truy vấn sau:

SELECT * FROM users WHERE email = 'example@example.com' AND order_number = '251542'' LIMIT 1

Mã như vậy sẽ tạo ra lỗi cú pháp cơ sở dữ liệu trong bảng điều khiển web. Lỗi này cho thấy ứng dụng có thể dễ bị tấn công bằng cách chèn SQL. Việc thay thế mã đơn đặt hàng bằng 'OR 1=1– có nghĩa là bạn có thể đạt được quy trình xác thực vì cơ sở dữ liệu đánh giá câu lệnh thành True, vì một luôn bằng một.

Tương tự, truy vấn này trả về tất cả các hàng trong bảng:

SELECT * FROM purchases WHERE email='admin@app.com' OR 1=1;

Trình cung cấp nội dung

Trình cung cấp nội dung đặt ra một cơ chế lưu trữ có cấu trúc có thể bị giới hạn ở một ứng dụng hoặc được xuất để chia sẻ với các ứng dụng khác. Quyền phải được đặt dựa trên nguyên tắc về đặc quyền tối thiểu; ContentProvider đã xuất có thể có một quyền đọc và ghi được chỉ định.

Lưu ý: Không phải thao tác chèn SQL nào cũng dẫn đến việc bị lợi dụng. Một số trình cung cấp nội dung đã cấp cho người đọc quyền truy cập đầy đủ vào cơ sở dữ liệu SQLite; việc có thể thực thi các truy vấn tuỳ ý không mang lại quá nhiều lợi thế. Các kiểu hành vi có thể cho thấy vấn đề bảo mật đang xảy ra bao gồm:

  • Nhiều trình cung cấp nội dung dùng chung một tệp cơ sở dữ liệu SQLite.
    • Trong trường hợp này, mỗi bảng có thể chỉ dành cho một trình cung cấp nội dung duy nhất. Việc chèn SQL thành công trong một trình cung cấp nội dung sẽ cấp quyền truy cập vào mọi bảng khác.
  • Trình cung cấp nội dung có nhiều quyền đối với nội dung trong cùng một cơ sở dữ liệu.
    • Việc chèn SQL trong một nhà cung cấp nội dung cấp quyền truy cập có nhiều cấp độ quyền có thể dẫn đến tình trạng bỏ qua cục bộ các chế độ cài đặt bảo mật hoặc quyền riêng tư.

Tác động

Kỹ thuật chèn SQL có thể làm lộ dữ liệu nhạy cảm của người dùng hoặc ứng dụng, vượt qua các quy tắc hạn chế về xác thực và uỷ quyền, đồng thời khiến các cơ sở dữ liệu dễ bị phá hoại hoặc bị xoá. Mức độ tác động có thể bao gồm các hệ quả nguy hiểm và lâu dài đối với những người dùng bị lộ dữ liệu cá nhân. Các nhà cung cấp ứng dụng và dịch vụ có nguy cơ mất tài sản trí tuệ hoặc sự tin tưởng của người dùng.

Giải pháp giảm thiểu

Tham số có thể thay thế

Việc dùng ? làm tham số có thể thay thế trong các mệnh đề lựa chọn và một mảng các đối số lựa chọn riêng biệt sẽ liên kết trực tiếp hoạt động đầu vào của người dùng với truy vấn thay vì diễn giải đó là một phần câu lệnh 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;

Hoạt động đầu vào của người dùng được liên kết trực tiếp với truy vấn thay vì được hiểu là SQL nên sẽ ngăn chặn việc chèn mã.

Dưới đây là ví dụ chi tiết hơn về truy vấn của một ứng dụng mua sắm để truy xuất chi tiết giao dịch mua bằng các tham số có thể thay thế:

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;
}

Sử dụng các đối tượng PreparedStatement

Giao diện PreparedStatement biên dịch trước các câu lệnh SQL dưới dạng một đối tượng và sau đó có thể được thực thi hiệu quả nhiều lần. PreparedStatement dùng ? làm phần giữ chỗ cho các tham số, từ đó vô hiệu hoá nỗ lực chèn đoạn mã đã biên dịch sau đây:

WHERE id=295094 OR 1=1;

Trong trường hợp này, câu lệnh 295094 OR 1=1 được đọc dưới dạng giá trị cho mã nhận dạng mà khả năng cao là không mang lại kết quả nào, còn truy vấn thô sẽ hiểu câu lệnh OR 1=1 là một phần khác của mệnh đề WHERE. Ví dụ bên dưới cho thấy một truy vấn chứa tham số:

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)

Sử dụng phương thức truy vấn

Trong ví dụ dài hơn này, selectionselectionArgs của phương thức query() được kết hợp để tạo thành mệnh đề WHERE. Vì được cung cấp riêng biệt, nên các đối số sẽ được thoát trước khi kết hợp, từ đó ngăn chặn việc chèn 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
    );

Sử dụng SQLiteQueryBuilder được định cấu hình đúng cách

Các nhà phát triển có thể bảo vệ ứng dụng hơn nữa bằng cách dùng SQLiteQueryBuilder – một lớp giúp tạo các truy vấn gửi đến các đối tượng SQLiteDatabase. Các cấu hình được đề xuất bao gồm:

Sử dụng thư viện Room

Gói android.database.sqlite cung cấp API cần thiết để sử dụng cơ sở dữ liệu trên Android. Tuy nhiên, phương pháp này thiếu khả năng xác minh thời gian biên dịch đối với các truy vấn SQL thô và yêu cầu bạn phải viết mã cấp thấp. Khi biểu đồ dữ liệu thay đổi, các truy vấn SQL bị ảnh hưởng cần được cập nhật theo cách thủ công – một quy trình tốn thời gian và dễ xảy ra lỗi.

Một giải pháp cấp cao là sử dụng Room Persistence Library làm lớp tóm tắt cho cơ sở dữ liệu SQLite. Các tính năng của thư viện Room bao gồm:

  • Một lớp cơ sở dữ liệu đóng vai trò là điểm truy cập chính để kết nối với dữ liệu cố định của ứng dụng.
  • Thực thể dữ liệu đại diện cho bảng của cơ sở dữ liệu.
  • Đối tượng truy cập dữ liệu (DAO), cung cấp các phương thức mà ứng dụng có thể dùng để truy vấn, cập nhật, chèn và xoá dữ liệu.

Các lợi ích của thư viện Room bao gồm:

  • Xác minh thời gian biên dịch của truy vấn SQL.
  • Giảm mã nguyên mẫu dễ gặp lỗi.
  • Đơn giản hoá quy trình di chuyển cơ sở dữ liệu.

Các phương pháp hay nhất

Chèn SQL là một kỹ thuật tấn công nguy hiểm mà khó có thể bảo vệ hoàn toàn, đặc biệt với các ứng dụng lớn và phức tạp. Bạn nên cân nhắc thêm các biện pháp bảo mật để hạn chế mức độ nghiêm trọng của các lỗi có thể xảy ra trong giao diện dữ liệu, bao gồm:

  • Hàm băm mạnh mẽ, một chiều và ngẫu nhiên để mã hoá mật khẩu:
    • AES 256 bit cho các ứng dụng thương mại.
    • Khoá công khai có kích thước 224 hoặc 256 bit để mã hoá đường cong elip.
  • Hạn chế quyền.
  • Xây dựng cấu trúc định dạng dữ liệu chính xác và xác minh rằng dữ liệu tuân thủ định dạng dự kiến.
  • Tránh lưu trữ dữ liệu cá nhân hoặc nhạy cảm của người dùng nếu có thể (ví dụ: triển khai logic ứng dụng bằng cách băm thay vì truyền hoặc lưu trữ dữ liệu).
  • Giảm thiểu các API và ứng dụng bên thứ ba truy cập vào dữ liệu nhạy cảm.

Tài nguyên