Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions best-practices/MASTG-BEST-0015.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
title: Prevent SQL Injection in ContentProviders
alias: prevent-sqli-contentprovider
id: MASTG-BEST-0015
platform: android
---

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)

```

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>
35 changes: 35 additions & 0 deletions demos/android/MASVS-CODE/MASTG-DEMO-0062/MASTG-DEMO-0062.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
platform: android
title: Injection flaws in android Content providers
id: MASTG-DEMO-0061
code: [kotlin]
test: MASTG-TEST-0288
status: new
profiles: [L1, L2]
---

### Sample

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

{{ MastgTest.kt # MastgTest_reversed.kt }}

### 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:

`content query --uri content://org.owasp.mastestapp.provider/students --where "name='Bob' OR '1'='1'"`
96 changes: 96 additions & 0 deletions demos/android/MASVS-CODE/MASTG-DEMO-0062/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