SQL 삽입

OWASP 카테고리: MASVS-CODE: 코드 품질

개요

SQL 삽입은 SQL 문에 코드를 삽입하여 의도적으로 노출된 인터페이스 이외의 기반 데이터베이스에 액세스하는 방식으로 취약한 애플리케이션을 악용합니다. 이 공격은 비공개 데이터를 노출하고, 데이터베이스 콘텐츠를 손상시키고, 나아가 백엔드 인프라를 손상시킬 수 있습니다.

SQL은 실행 전에 사용자 입력을 연결하여 동적으로 생성되는 쿼리를 통한 삽입에 취약할 수 있습니다. 웹, 모바일, 그리고 모든 SQL 데이터베이스 애플리케이션을 타겟팅하는 SQL 삽입은 보통 OWASP 상위 10개 웹 취약점에 포함됩니다. 공격자들은 잘 알려진 여러 보안 사고에서 이 기법을 사용한 바 있습니다.

아래 기본 예에서는 사용자가 주문 번호 상자에 입력한 이스케이프 처리되지 않은 값이 SQL 문자열에 삽입되어 다음 쿼리로 해석될 수 있습니다.

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

이러한 코드는 웹 콘솔에서 데이터베이스 문법 오류를 발생시키며, 이는 애플리케이션이 SQL 삽입에 취약함을 보여줍니다. 주문 번호를 'OR 1=1–로 바꾸면 1은 항상 1과 같으므로 데이터베이스가 문을 True로 평가하여 인증에 성공할 수 있습니다.

마찬가지로, 다음 쿼리는 표의 모든 행을 반환합니다.

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

콘텐츠 제공자

콘텐츠 제공자는 특정 애플리케이션으로 제한되거나 다른 앱과의 공유를 위해 내보낼 수 있는 구조화된 저장소 메커니즘을 제공합니다. 권한은 최소 권한의 원칙에 따라 설정해야 합니다. 즉, 내보낸 ContentProvider는 읽기 및 쓰기에 대해 지정된 단일 권한을 가질 수 있습니다.

모든 SQL 삽입이 악용으로 이어지는 것은 아닙니다. 일부 콘텐츠 제공자는 읽기 권한이 있는 사용자에게 SQLite 데이터베이스에 대한 전체 액세스 권한을 부여하므로 임의의 쿼리를 실행할 수 있다고 해도 그에 따른 이점이 거의 없습니다. 보안 문제를 나타낼 수 있는 패턴은 다음과 같습니다.

  • 여러 콘텐츠 제공자가 단일 SQLite 데이터베이스 파일 공유
    • 이 경우 각 표가 각각의 콘텐츠 제공자를 위한 것일 수 있습니다. 하나의 콘텐츠 제공자에서 SQL 삽입이 성공적으로 이루어지면 다른 표에 대한 액세스 권한이 부여됩니다.
  • 하나의 콘텐츠 제공자가 동일한 데이터베이스 내의 콘텐츠에 대해 복수의 권한을 가짐
    • 하나의 콘텐츠 제공자에서 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 문은 ID의 값으로 읽히기 때문에 아무런 결과도 발생하지 않습니다. 반면에 원시 쿼리는 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)

쿼리 메서드 사용

이 긴 예에서는 query() 메서드의 selectionselectionArgs가 결합되어 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 사용

개발자는 SQLiteDatabase 객체로 전송될 쿼리를 빌드하는 데 도움이 되는 클래스인 SQLiteQueryBuilder를 사용하여 애플리케이션을 추가로 보호할 수 있습니다. 권장되는 구성은 다음과 같습니다.

Room 라이브러리 사용

android.database.sqlite 패키지는 Android에서 데이터베이스를 사용하는 데 필요한 API를 제공합니다. 그러나 이 접근 방식을 사용하려면 하위 수준의 코드를 작성해야 하며 원시 SQL 쿼리의 컴파일 시간 확인이 이루어지지 않습니다. 데이터 그래프가 변경됨에 따라 영향을 받는 SQL 쿼리를 수동으로 업데이트해야 하는데 이는 시간이 오래 걸리고 오류가 발생하기 쉬운 프로세스입니다.

이에 대한 개략적인 솔루션은 Room 지속성 라이브러리를 SQLite 데이터베이스의 추상화 레이어로 사용하는 것입니다. Room의 기능은 다음과 같습니다.

  • 앱의 지속되는 데이터에 연결하기 위한 기본 액세스 포인트로 기능하는 데이터베이스 클래스
  • 데이터베이스의 표를 나타내는 데이터 항목
  • 앱이 데이터를 쿼리, 업데이트, 삽입, 삭제하는 데 사용할 수 있는 메서드를 제공하는 데이터 액세스 객체(DAO)

Room의 이점은 다음과 같습니다.

  • SQL 쿼리의 컴파일 시간 확인
  • 오류가 발생하기 쉬운 상용구 코드의 감소
  • 간소화된 데이터베이스 이전

권장사항

SQL 삽입은 완전히 복원하기 어려울 수 있는 강력한 공격이며, 크고 복잡한 애플리케이션에서는 더욱 그렇습니다. 데이터 인터페이스의 잠재적인 결함으로 인한 문제의 심각성을 완화하기 위해 다음과 같은 추가적인 보안 사항을 고려해야 합니다.

  • 강력한 단방향 솔트 해시를 사용하여 비밀번호 암호화
    • 상업용 애플리케이션: 256비트 AES
    • 타원 곡선 암호: 224비트 또는 256비트 공개 키 크기
  • 권한 제한
  • 데이터 형식을 정밀하게 구조화하고 데이터가 예상되는 형식을 준수하는지 확인
  • 가능한 경우 개인 정보 또는 민감한 사용자 데이터를 저장하지 않음(예: 데이터를 전송하거나 저장하는 대신 해싱하여 애플리케이션 로직 구현)
  • 민감한 정보에 액세스하는 API 및 서드 파티 애플리케이션 최소화

리소스