הזרקת SQL

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

סקירה כללית

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

שאילתות 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 נעשה שימוש ב-? כplaceholder לפרמטרים, ולכן ניסיון ההזרקה הבא שקומפל לא יהיה יעיל:

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. הארגומנטים מסופקים בנפרד, ולכן הם עוברים escape לפני השילוב שלהם, וכך נמנעת הזרקת 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() כדי להגביל את השאילתות המשנה.

שימוש בספריית החדרים

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

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

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

היתרונות של שימוש במרחבים כוללים:

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

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

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

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

משאבים