Skip to content
Open
23 changes: 23 additions & 0 deletions best-practices/MASTG-BEST-XXXX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
title: Prevent SQL Injection in ContentProviders
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title: Prevent SQL Injection in ContentProviders
title: Prevent SQL Injection in Content Providers

alias: prevent-sqli-contentprovider
id: MASTG-BEST-XXXX
platform: android
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map to knowledge

---

The `ContentProvider` enables Android applications to share data with other applications and system components. If a `ContentProvider` constructs SQL queries using untrusted input from URIs, IPC calls, or Intents without validation or parameterization, it becomes vulnerable to SQL injection. Attackers can take advantage of this vulnerability to bypass access controls and extract sensitive data. Improper handling of URI path segments, query parameters, or `selection` arguments in `ContentProvider` queries can lead to arbitrary SQL execution.

- **Use Parameterized Queries** : Instead of building SQL using string concatenation, use `selection` and `selectionArgs` parameters.

For example:

```kotlin
val idSegment = uri.getPathSegments()[1]
val selection = "id = ?"
val selectionArgs = arrayOf(idSegment)
val cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder)
```

- **Use Prepared Statements**: When performing insert, update, or delete operations, use SQLite prepared statements (for example, `SQLiteStatement` or `SQLiteDatabase` methods that support argument binding) instead of dynamically constructed SQL. Prepared statements ensure that untrusted input is bound as parameters and cannot alter the structure of the SQL query, effectively preventing SQL injection even when input originates from URIs or IPC calls.

Refer to ["Protect against malicious input"](https://developer.android.com/guide/topics/providers/content-provider-basics#Injection) for more information.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0"
android:compileSdkVersion="35"
android:compileSdkVersionCodename="15"
package="org.owasp.mastestapp"
platformBuildVersionCode="35"
platformBuildVersionName="15">
<uses-sdk
android:minSdkVersion="29"
android:targetSdkVersion="35"/>
<permission
android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
android:protectionLevel="signature"/>
<uses-permission android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
<application
android:theme="@style/Theme.MASTestApp"
android:label="MASTG SQLi Test 2"
android:debuggable="true"
android:testOnly="true"
android:allowBackup="true"
android:supportsRtl="true"
android:extractNativeLibs="false"
android:appComponentFactory="androidx.core.app.CoreComponentFactory">
<activity
android:name="org.owasp.mastestapp.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<provider
android:name="org.owasp.mastestapp.MastgTest.StudentProvider"
android:exported="true"
android:authorities="org.owasp.mastestapp.provider"/>
<activity
android:name="androidx.compose.ui.tooling.PreviewActivity"
android:exported="true"/>
<activity
android:name="androidx.activity.ComponentActivity"
android:exported="true"/>
<provider
android:name="androidx.startup.InitializationProvider"
android:exported="false"
android:authorities="org.owasp.mastestapp.androidx-startup">
<meta-data
android:name="androidx.emoji2.text.EmojiCompatInitializer"
android:value="androidx.startup"/>
<meta-data
android:name="androidx.lifecycle.ProcessLifecycleInitializer"
android:value="androidx.startup"/>
<meta-data
android:name="androidx.profileinstaller.ProfileInstallerInitializer"
android:value="androidx.startup"/>
</provider>
<receiver
android:name="androidx.profileinstaller.ProfileInstallReceiver"
android:permission="android.permission.DUMP"
android:enabled="true"
android:exported="true"
android:directBootAware="false">
<intent-filter>
<action android:name="androidx.profileinstaller.action.INSTALL_PROFILE"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.SKIP_FILE"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.SAVE_PROFILE"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION"/>
</intent-filter>
</receiver>
</application>
</manifest>
41 changes: 41 additions & 0 deletions demos/android/MASVS-CODE/MASTG-DEMO-00XX/MASTG-DEMO-00XX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
platform: android
title: Injection flaws in Android Content providers
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title: Injection flaws in Android Content providers
title: Injection Flaws in Android Content Providers

id: MASTG-DEMO-00XX
code: [kotlin]
test: MASTG-TEST-02XX
status: new
---

### Sample

The following code implements a vulnerable `ContentProvider` that appends user-controlled input from the URI path directly into a SQL query.

{{ MastgTest.kt # MastgTest_reversed.java }}

### Steps

Let's run our @MASTG-TOOL-0110 rule against the sample code.

{{ ../../../../rules/mastg-android-sql-injection-contentprovider.yml }}

{{ run.sh }}

### Observation

The rule has identified the use of untrusted input from `Uri.getPathSegments().get(...)` being concatenated and passed into `SQLiteQueryBuilder.appendWhere(...)`, which is a known vector for SQL injection in exported `ContentProviders`.

{{ output.txt }}

### Evaluation

This test case fails because the application constructs a SQL `WHERE` clause by directly appending untrusted user input from the URI without any validation or sanitization. This approach allows attackers to perform SQL injection by crafting a malicious `content://` URI to manipulate the query logic. For example, the following content query command can be used to list all names:

```bash
$ content query --uri content://org.owasp.mastestapp.provider/students --where "name='Bob' OR '1'='1'"
Row: 0 id=1, name=Alice
Row: 1 id=2, name=Bob
Row: 2 id=3, name=Charlie
```

Refer to @MASTG-TECH-XXXX, to know more on using content query.
96 changes: 96 additions & 0 deletions demos/android/MASVS-CODE/MASTG-DEMO-00XX/MastgTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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 SQLI.

Test on adb shell with:
# content query --uri content://org.owasp.mastestapp.provider/students --where "name='Bob' OR '1'='1'"
""".trimIndent()
}

// Vulnerable ContentProvider with path-based SQL injection
class StudentProvider : ContentProvider() {

companion object {
const val AUTHORITY = "org.owasp.mastestapp.provider"
const val STUDENTS = 1
const val STUDENT_ID = 2
val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, "students", STUDENTS)
addURI(AUTHORITY, "students/#", STUDENT_ID)
}
}

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 -> {
// No filtering — all rows
}
STUDENT_ID -> {
// Vulnerable: unvalidated input from path used in query
val id = uri.getPathSegments().get(1)
qb.appendWhere("id=" + id)
Log.e("SQLI", "Injected ID segment: $id")
}
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
}

// DB helper for student data
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)
}
}
}
Loading
Loading