הזרקת SQL

קטגוריה ב-OWASP: MASVS-CODE: איכות הקוד

סקירה כללית

הזרקת SQL מנצלת אפליקציות חשופות על ידי החדרת קוד להצהרות SQL כדי לגשת למסדי הנתונים הבסיסיים מעבר לממשקים שנחשפו בכוונה. ההתקפה עלולה לחשוף מידע פרטי, לפגוע בתוכן של מסד נתונים ואפילו לפגוע בתשתית הקצה העורפי.

שאילתות SQL יכולות להיות חשופות להזרקה דרך שאילתות שנוצרות באופן דינמי על ידי שרשור קלט של משתמשים לפני ביצוע. החדרת SQL היא התקפה שמטרגטת את האינטרנט, את האפליקציות לנייד ואת כל אפליקציה שמשתמשת במסד נתוני SQL. בדרך כלל היא נכללת בעשרת הנקודות המובילות של OWASP לנקודות חולשה באינטרנט. תוקפים השתמשו בשיטה הזו בכמה פרצות מפורסמות.

בדוגמה הבסיסית הזו, קלט של משתמש ללא תווי בריחה בתיבת מספר הזמנה יכול להתווסף למחרוזת ה-SQL ולהיחשב כשאילתה הבאה:

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

קוד כזה יגרום לשגיאת תחביר של מסד נתונים במסוף אינטרנט, שמראה שהאפליקציה עשויה להיות חשופה להזרקת SQL. החלפת מספר ההזמנה ב-'OR 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)

שימוש בשיטות של שאילתות

בדוגמה הארוכה הזו, הערכים selection ו-selectionArgs של method‏ 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. ההגדרות המומלצות כוללות:

שימוש בספרייה של החדר

החבילה android.database.sqlite מספקת ממשקי API שנדרשים לשימוש במסדי נתונים ב-Android. עם זאת, הגישה הזו דורשת כתיבת קוד ברמה נמוכה, ואין בה אימות של שאילתות SQL גולמיות בזמן הידור. כשתרשים הנתונים משתנה, צריך לעדכן באופן ידני את שאילתות ה-SQL המושפעות – תהליך שאורך זמן וטומן בחובו שגיאות.

פתרון ברמה גבוהה הוא להשתמש ב-Room Persistence Library כשכבת הפשטה למסדי נתונים של SQLite. מאפייני החדר כוללים:

  • סוג של מסד נתונים שמשמש כנקודת הגישה הראשית להתחברות לנתונים הקבועים של האפליקציה.
  • ישויות נתונים שמייצגות את הטבלאות של מסד הנתונים.
  • אובייקטים של גישה לנתונים (DAO), שמספקים שיטות שבהן האפליקציה יכולה להשתמש כדי לשלוח שאילתות, לעדכן, להוסיף ולמחוק נתונים.

היתרונות של Room כוללים:

  • אימות של שאילתות SQL בזמן הידור.
  • הפחתת כמות הקוד הסטנדרטי שעלול להכיל שגיאות.
  • העברה יעילה של מסדי נתונים.

שיטות מומלצות

הזרקת SQL היא התקפה חזקה שקשה מאוד להתגונן מפניה, במיוחד באפליקציות גדולות ומורכבות. כדי להגביל את חומרת הפגמים האפשריים בממשקי הנתונים, צריך להביא בחשבון שיקולים נוספים של אבטחה, כולל:

  • גיבובים חזקים, חד-כיווניים ומתובלנים (salted) להצפנת סיסמאות:
    • AES של 256 ביט לאפליקציות מסחריות.
    • גדלים של מפתחות ציבוריים ב-224 או ב-256 ביט להצפנה המבוססת על עקומים אליפטיים.
  • הגבלת ההרשאות.
  • מבנה מדויק של פורמטים של נתונים ואימות שהנתונים תואמים לפורמט הצפוי.
  • הימנעות משמירת נתוני משתמשים אישיים או רגישים במידת האפשר (לדוגמה, הטמעת לוגיקה של אפליקציה באמצעות גיבוב במקום העברת נתונים או אחסון שלהם).
  • צמצום מספר ממשקי ה-API והאפליקציות של צד שלישי שיש להם גישה לנתונים רגישים.

משאבים