Skip to content

MASTG-DEMO-0102: SQL Injection via URI Path and Selection in Android Content Providers

Download MASTG-DEMO-0102 APK Open MASTG-DEMO-0102 Folder Build MASTG-DEMO-0102 APK

Sample

The following code implements a vulnerable ContentProvider that demonstrates two SQL injection cases.

The first case passes caller-controlled selection input into a SQL query.

The second case appends user-controlled input from the URI path directly into a SQL query.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package org.owasp.mastestapp

import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.content.UriMatcher
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import android.util.Log

class MastgTest(private val context: Context) {

    fun mastgTest(): String {
        return """
            This app's content provider is vulnerable to SQL injection.

            Selection based SQL injection:
            # adb shell 'content query --uri content://org.owasp.mastestapp.provider/students --where "name='\''Bob'\'' OR '\''1'\''='\''1'\''"'

            Path based SQL injection:
            # adb shell 'content query --uri "content://org.owasp.mastestapp.provider/students/filter/id%3D2%20OR%201%3D1"'
        """.trimIndent()
    }

    class StudentProvider : ContentProvider() {

        companion object {
            const val AUTHORITY = "org.owasp.mastestapp.provider"
            const val STUDENTS = 1
            const val STUDENT_ID = 2
            const val STUDENT_FILTER = 3

            val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
                addURI(AUTHORITY, "students", STUDENTS)
                addURI(AUTHORITY, "students/#", STUDENT_ID)
                addURI(AUTHORITY, "students/filter/*", STUDENT_FILTER)
            }
        }

        private lateinit var dbHelper: DatabaseHelper

        override fun onCreate(): Boolean {
            dbHelper = DatabaseHelper(context!!)
            return true
        }

        override fun query(
            uri: Uri,
            projection: Array<String>?,
            selection: String?,
            selectionArgs: Array<String>?,
            sortOrder: String?
        ): Cursor? {
            val db = dbHelper.readableDatabase
            val qb = SQLiteQueryBuilder()
            qb.tables = "students"

            when (uriMatcher.match(uri)) {
                STUDENTS -> {
                    /*
                     * Vulnerable case 1:
                     * The caller controlled selection argument is passed directly
                     * into qb.query(...) below.
                     */
                }

                STUDENT_ID -> {
                    /*
                     * Numeric ID route.
                     * The UriMatcher students/# pattern only accepts numeric path segments.
                     */
                    val id = uri.pathSegments[1]
                    qb.appendWhere("id=" + id)
                }

                STUDENT_FILTER -> {
                    /*
                     * Vulnerable case 2:
                     * Arbitrary caller controlled URI path input is appended directly
                     * into the SQL WHERE clause.
                     */
                    val filter = uri.pathSegments[2]
                    qb.appendWhere("" + filter)
                    Log.e("SQLI", "Injected filter segment: $filter")
                }

                else -> throw IllegalArgumentException("Unknown URI: $uri")
            }

            val cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder)
            cursor.setNotificationUri(context!!.contentResolver, uri)
            return cursor
        }

        override fun getType(uri: Uri): String? = null
        override fun insert(uri: Uri, values: ContentValues?): Uri? = null
        override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
        override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int = 0
    }

    class DatabaseHelper(context: Context) :
        SQLiteOpenHelper(context, "students.db", null, 1) {

        override fun onCreate(db: SQLiteDatabase) {
            db.execSQL("CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT)")
            db.execSQL("INSERT INTO students (name) VALUES ('Alice')")
            db.execSQL("INSERT INTO students (name) VALUES ('Bob')")
            db.execSQL("INSERT INTO students (name) VALUES ('Charlie')")
        }

        override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
            db.execSQL("DROP TABLE IF EXISTS students")
            onCreate(db)
        }
    }
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package org.owasp.mastestapp;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.util.Log;
import kotlin.Metadata;
import kotlin.jvm.internal.DefaultConstructorMarker;
import kotlin.jvm.internal.Intrinsics;

/* compiled from: MastgTest.kt */
@Metadata(d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0002\b\u0003\b\u0007\u0018\u00002\u00020\u0001:\u0002\b\tB\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\n"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "<init>", "(Landroid/content/Context;)V", "mastgTest", "", "StudentProvider", "DatabaseHelper", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
/* loaded from: classes3.dex */
public final class MastgTest {
    public static final int $stable = 8;
    private final Context context;

    public MastgTest(Context context) {
        Intrinsics.checkNotNullParameter(context, "context");
        this.context = context;
    }

    public final String mastgTest() {
        return "This app's content provider is vulnerable to SQL injection.\n\nSelection based SQL injection:\n# adb shell 'content query --uri content://org.owasp.mastestapp.provider/students --where \"name='\\''Bob'\\'' OR '\\''1'\\''='\\''1'\\''\"'\n\nPath based SQL injection:\n# adb shell 'content query --uri \"content://org.owasp.mastestapp.provider/students/filter/id%3D2%20OR%201%3D1\"'";
    }

    /* compiled from: MastgTest.kt */
    @Metadata(d1 = {"\u0000>\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000b\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0002\b\u0007\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\b\n\u0002\b\u0005\b\u0007\u0018\u0000 \u001c2\u00020\u0001:\u0001\u001cB\t\b\u0007¢\u0006\u0004\b\u0002\u0010\u0003J\b\u0010\u0006\u001a\u00020\u0007H\u0016JK\u0010\b\u001a\u0004\u0018\u00010\t2\u0006\u0010\n\u001a\u00020\u000b2\u000e\u0010\f\u001a\n\u0012\u0004\u0012\u00020\u000e\u0018\u00010\r2\b\u0010\u000f\u001a\u0004\u0018\u00010\u000e2\u000e\u0010\u0010\u001a\n\u0012\u0004\u0012\u00020\u000e\u0018\u00010\r2\b\u0010\u0011\u001a\u0004\u0018\u00010\u000eH\u0016¢\u0006\u0002\u0010\u0012J\u0012\u0010\u0013\u001a\u0004\u0018\u00010\u000e2\u0006\u0010\n\u001a\u00020\u000bH\u0016J\u001c\u0010\u0014\u001a\u0004\u0018\u00010\u000b2\u0006\u0010\n\u001a\u00020\u000b2\b\u0010\u0015\u001a\u0004\u0018\u00010\u0016H\u0016J1\u0010\u0017\u001a\u00020\u00182\u0006\u0010\n\u001a\u00020\u000b2\b\u0010\u000f\u001a\u0004\u0018\u00010\u000e2\u0010\u0010\u0010\u001a\f\u0012\u0006\b\u0001\u0012\u00020\u000e\u0018\u00010\rH\u0016¢\u0006\u0002\u0010\u0019J;\u0010\u001a\u001a\u00020\u00182\u0006\u0010\n\u001a\u00020\u000b2\b\u0010\u0015\u001a\u0004\u0018\u00010\u00162\b\u0010\u000f\u001a\u0004\u0018\u00010\u000e2\u0010\u0010\u0010\u001a\f\u0012\u0006\b\u0001\u0012\u00020\u000e\u0018\u00010\rH\u0016¢\u0006\u0002\u0010\u001bR\u000e\u0010\u0004\u001a\u00020\u0005X\u0082.¢\u0006\u0002\n\u0000¨\u0006\u001d"}, d2 = {"Lorg/owasp/mastestapp/MastgTest$StudentProvider;", "Landroid/content/ContentProvider;", "<init>", "()V", "dbHelper", "Lorg/owasp/mastestapp/MastgTest$DatabaseHelper;", "onCreate", "", "query", "Landroid/database/Cursor;", "uri", "Landroid/net/Uri;", "projection", "", "", "selection", "selectionArgs", "sortOrder", "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;", "getType", "insert", "values", "Landroid/content/ContentValues;", "delete", "", "(Landroid/net/Uri;Ljava/lang/String;[Ljava/lang/String;)I", "update", "(Landroid/net/Uri;Landroid/content/ContentValues;Ljava/lang/String;[Ljava/lang/String;)I", "Companion", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
    public static final class StudentProvider extends ContentProvider {
        public static final String AUTHORITY = "org.owasp.mastestapp.provider";
        public static final int STUDENTS = 1;
        public static final int STUDENT_FILTER = 3;
        public static final int STUDENT_ID = 2;
        private static final UriMatcher uriMatcher;
        private DatabaseHelper dbHelper;

        /* renamed from: Companion, reason: from kotlin metadata */
        public static final Companion INSTANCE = new Companion(null);
        public static final int $stable = 8;

        /* compiled from: MastgTest.kt */
        @Metadata(d1 = {"\u0000\"\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\b\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0002\b\u0003\b\u0086\u0003\u0018\u00002\u00020\u0001B\t\b\u0002¢\u0006\u0004\b\u0002\u0010\u0003R\u000e\u0010\u0004\u001a\u00020\u0005X\u0086T¢\u0006\u0002\n\u0000R\u000e\u0010\u0006\u001a\u00020\u0007X\u0086T¢\u0006\u0002\n\u0000R\u000e\u0010\b\u001a\u00020\u0007X\u0086T¢\u0006\u0002\n\u0000R\u000e\u0010\t\u001a\u00020\u0007X\u0086T¢\u0006\u0002\n\u0000R\u0011\u0010\n\u001a\u00020\u000b¢\u0006\b\n\u0000\u001a\u0004\b\f\u0010\r¨\u0006\u000e"}, d2 = {"Lorg/owasp/mastestapp/MastgTest$StudentProvider$Companion;", "", "<init>", "()V", "AUTHORITY", "", "STUDENTS", "", "STUDENT_ID", "STUDENT_FILTER", "uriMatcher", "Landroid/content/UriMatcher;", "getUriMatcher", "()Landroid/content/UriMatcher;", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
        public static final class Companion {
            public /* synthetic */ Companion(DefaultConstructorMarker defaultConstructorMarker) {
                this();
            }

            private Companion() {
            }

            public final UriMatcher getUriMatcher() {
                return StudentProvider.uriMatcher;
            }
        }

        static {
            UriMatcher $this$uriMatcher_u24lambda_u240 = new UriMatcher(-1);
            $this$uriMatcher_u24lambda_u240.addURI(AUTHORITY, "students", 1);
            $this$uriMatcher_u24lambda_u240.addURI(AUTHORITY, "students/#", 2);
            $this$uriMatcher_u24lambda_u240.addURI(AUTHORITY, "students/filter/*", 3);
            uriMatcher = $this$uriMatcher_u24lambda_u240;
        }

        @Override // android.content.ContentProvider
        public boolean onCreate() {
            Context context = getContext();
            Intrinsics.checkNotNull(context);
            this.dbHelper = new DatabaseHelper(context);
            return true;
        }

        @Override // android.content.ContentProvider
        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
            Intrinsics.checkNotNullParameter(uri, "uri");
            DatabaseHelper databaseHelper = this.dbHelper;
            if (databaseHelper == null) {
                Intrinsics.throwUninitializedPropertyAccessException("dbHelper");
                databaseHelper = null;
            }
            SQLiteDatabase db = databaseHelper.getReadableDatabase();
            SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
            qb.setTables("students");
            switch (uriMatcher.match(uri)) {
                case 1:
                    break;
                case 2:
                    String id = uri.getPathSegments().get(1);
                    qb.appendWhere("id=" + id);
                    break;
                case 3:
                    String filter = uri.getPathSegments().get(2);
                    qb.appendWhere(filter);
                    Log.e("SQLI", "Injected filter segment: " + filter);
                    break;
                default:
                    throw new IllegalArgumentException("Unknown URI: " + uri);
            }
            Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
            Context context = getContext();
            Intrinsics.checkNotNull(context);
            cursor.setNotificationUri(context.getContentResolver(), uri);
            return cursor;
        }

        @Override // android.content.ContentProvider
        public String getType(Uri uri) {
            Intrinsics.checkNotNullParameter(uri, "uri");
            return null;
        }

        @Override // android.content.ContentProvider
        public Uri insert(Uri uri, ContentValues values) {
            Intrinsics.checkNotNullParameter(uri, "uri");
            return null;
        }

        @Override // android.content.ContentProvider
        public int delete(Uri uri, String selection, String[] selectionArgs) {
            Intrinsics.checkNotNullParameter(uri, "uri");
            return 0;
        }

        @Override // android.content.ContentProvider
        public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
            Intrinsics.checkNotNullParameter(uri, "uri");
            return 0;
        }
    }

    /* compiled from: MastgTest.kt */
    @Metadata(d1 = {"\u0000(\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\u0002\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0010\u0010\u0006\u001a\u00020\u00072\u0006\u0010\b\u001a\u00020\tH\u0016J \u0010\n\u001a\u00020\u00072\u0006\u0010\b\u001a\u00020\t2\u0006\u0010\u000b\u001a\u00020\f2\u0006\u0010\r\u001a\u00020\fH\u0016¨\u0006\u000e"}, d2 = {"Lorg/owasp/mastestapp/MastgTest$DatabaseHelper;", "Landroid/database/sqlite/SQLiteOpenHelper;", "context", "Landroid/content/Context;", "<init>", "(Landroid/content/Context;)V", "onCreate", "", "db", "Landroid/database/sqlite/SQLiteDatabase;", "onUpgrade", "oldVersion", "", "newVersion", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
    public static final class DatabaseHelper extends SQLiteOpenHelper {
        public static final int $stable = 0;

        /* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
        public DatabaseHelper(Context context) {
            super(context, "students.db", (SQLiteDatabase.CursorFactory) null, 1);
            Intrinsics.checkNotNullParameter(context, "context");
        }

        @Override // android.database.sqlite.SQLiteOpenHelper
        public void onCreate(SQLiteDatabase db) throws SQLException {
            Intrinsics.checkNotNullParameter(db, "db");
            db.execSQL("CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT)");
            db.execSQL("INSERT INTO students (name) VALUES ('Alice')");
            db.execSQL("INSERT INTO students (name) VALUES ('Bob')");
            db.execSQL("INSERT INTO students (name) VALUES ('Charlie')");
        }

        @Override // android.database.sqlite.SQLiteOpenHelper
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) throws SQLException {
            Intrinsics.checkNotNullParameter(db, "db");
            db.execSQL("DROP TABLE IF EXISTS students");
            onCreate(db);
        }
    }
}

Steps

Let's run our semgrep rule against the sample code.

../../../../rules/mastg-android-sql-injection-contentprovider.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
rules:
  - id: mastg-android-sql-injection-contentprovider
    severity: WARNING
    languages:
      - java
    metadata:
      summary: The rule detects possible SQL injection in Android ContentProviders via unvalidated URI path segment concatenation.
    message: "[MASVS-CODE-4] Possible SQL Injection: unvalidated user input from `Uri.getPathSegments()` concatenated into `SQLiteQueryBuilder.appendWhere()`"
    patterns:
      - pattern-either:
          - pattern: |
              $QB.appendWhere("..." + $VAR);
          - pattern: |
              $QB.appendWhere($VAR);
      - pattern-inside: |
          $VAR = $URI.getPathSegments().get(...);
          ...
run.sh
1
2
#!/bin/bash
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-sql-injection-contentprovider.yml ./MastgTest_reversed.java > output.txt

Observation

The rule identified two uses of data from Uri.getPathSegments() being passed into SQLiteQueryBuilder.appendWhere(...).

output.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
┌─────────────────┐
 2 Code Findings 
└─────────────────┘

    MastgTest_reversed.java
    ❯❱ rules.mastg-android-sql-injection-contentprovider
          [MASVS-CODE-4] Possible SQL Injection: unvalidated user input from `Uri.getPathSegments()`
          concatenated into `SQLiteQueryBuilder.appendWhere()`                                      

           95 qb.appendWhere("id=" + id);
            ⋮┆----------------------------------------
           99 qb.appendWhere(filter);

The first finding uses the students/# route, which is constrained by UriMatcher to numeric path segments. The second finding uses the students/filter/* route, which accepts arbitrary path input and is exploitable as path based SQL injection.

Evaluation

This test case fails because the application constructs SQL WHERE clauses using untrusted, user-controlled input from two different sources.

The vulnerable data flow is visible in the reversed code. The provider registers both a numeric route and an arbitrary path segment route:

$this$uriMatcher_u24lambda_u240.addURI(AUTHORITY, "students/#", 2);
$this$uriMatcher_u24lambda_u240.addURI(AUTHORITY, "students/filter/*", 3);
````

For the path-based case, the provider reads a user-controlled URI segment and appends it directly into the SQL query:

```java
String filter = uri.getPathSegments().get(2);
qb.appendWhere(filter);

For the selection-based case, the provider passes the user-controlled selection argument directly into the query without validation:

Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);

These two code paths create separate SQL injection vectors.

Exploitation

You can use Interacting with Android ContentProviders to interact with the ContentProvider and confirm the injection vulnerabilities.

Selection-based SQL Injection:

An attacker can inject SQL through the --where argument of the content command:

adb shell 'content query --uri content://org.owasp.mastestapp.provider/students --where "name='\''Bob'\'' OR '\''1'\''='\''1'\''"'

Output:

Row: 0 id=1, name=Alice
Row: 1 id=2, name=Bob
Row: 2 id=3, name=Charlie

Path-based SQL Injection:

An attacker can inject SQL through the URI path using the students/filter/* route:

adb shell 'content query --uri "content://org.owasp.mastestapp.provider/students/filter/id%3D2%20OR%201%3D1"'

Output:

Row: 0 id=1, name=Alice
Row: 1 id=2, name=Bob
Row: 2 id=3, name=Charlie

The students/# route is limited to numeric input by UriMatcher and isn't practically exploitable, but it's still flagged because it demonstrates unsafe concatenation of user-controlled data into a SQL query.

Both vulnerabilities arise from directly incorporating untrusted input into SQL statements instead of using parameterized queries or proper input validation.