I have different apps that share some data with each others, that's done through Content Provider but when I uploaded the apk I received an email saying "Your app(s) are using a content provider that contains a SQL Injection vulnerability."
There are a couple of ways for fixing this according to Google guide:
If an affected ContentProvider needs to be exposed to other apps:
You can prevent SQL Injection into SQLiteDatabase.query by using
strict mode with a projection map. Strict mode protects against
malicious selection clauses and projection map protects against
malicious projection clauses. You must use both of these features to ensure that your queries are safe.
You can prevent SQL Injection into SQLiteDatabase.update and SQLiteDatabase.delete by using a selection clause that uses '?' as a replaceable parameter and a separate array of selection arguments. Your selection clause should not be constructed from untrusted inputs.
But is not clear to me how to proceed with any of the solutions, I don't get exactly how to use the projection map or change the code using selection clause that uses '?'. I mean, I have seen some examples about ProjectionMap but what are the key/value it needs for the query? Do I need to write explicit the values I want? but what if it is a generic method and I don't know in that part of the code what do I want to get? Or how do I convert any query to a ProjectionMap?
I hope I'm explaining myself on this.
Here is my code:
#Override
public boolean onCreate() {
gOpenHelper = new GameDBHelper(getContext());
return true;
}
/**
* Builds a UriMatcher that is used to determine witch database request is being made.
*/
public static UriMatcher buildUriMatcher(){
String content = GamesContract.CONTENT_AUTHORITY;
// All paths to the UriMatcher have a corresponding code to return
// when a match is found (the ints above).
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
matcher.addURI(content, GamesContract.PATH_GAME, GAME);
matcher.addURI(content, GamesContract.PATH_GAME + "/#", GAME_ID);
return matcher;
}
#Override
public String getType(Uri uri) {
switch(sUriMatcher.match(uri))
{
case GAME:
return GameEntry.CONTENT_TYPE;
case GAME_ID:
return GameEntry.CONTENT_ITEM_TYPE;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
final SQLiteDatabase db = gOpenHelper.getWritableDatabase();
Cursor retCursor;
switch(sUriMatcher.match(uri))
{
case GAME:
retCursor = db.query(
GameEntry.TABLE_NAME,
projection,
selection,
selectionArgs,
null,
null,
sortOrder
);
break;
case GAME_ID:
long _id = ContentUris.parseId(uri);
retCursor = db.query(
GameEntry.TABLE_NAME,
projection,
GameEntry._ID + " = ?",
new String[]{String.valueOf(_id)},
null,
null,
sortOrder
);
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
retCursor.setNotificationUri(getContext().getContentResolver(), uri);
return retCursor;
}
#Override
public Uri insert(Uri uri, ContentValues values) {
final SQLiteDatabase db = gOpenHelper.getWritableDatabase();
long _id;
Uri returnUri;
switch(sUriMatcher.match(uri))
{
case GAME:
_id = db.insert(GameEntry.TABLE_NAME, null, values);
if(_id > 0){
returnUri = GameEntry.BuildGameUri(_id);
} else{
throw new UnsupportedOperationException("Unable to insert rows into: " + uri);
}
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return returnUri;
}
#Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
final SQLiteDatabase db = gOpenHelper.getWritableDatabase();
int rows; // Number of rows effected
switch(sUriMatcher.match(uri))
{
case GAME:
rows = db.delete(GameEntry.TABLE_NAME, selection, selectionArgs);
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
// Because null could delete all rows:
if(selection == null || rows != 0){
getContext().getContentResolver().notifyChange(uri, null);
}
return rows;
}
#Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
final SQLiteDatabase db = gOpenHelper.getWritableDatabase();
int rows;
switch(sUriMatcher.match(uri))
{
case GAME:
rows = db.update(GameEntry.TABLE_NAME, values, selection, selectionArgs);
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
if(rows != 0){
getContext().getContentResolver().notifyChange(uri, null);
}
return rows;
}
}
To anyone who can be looking for the answer, it was pretty simpe, I think it was getting hard for me because I wasn't familiar with many concepts about sql on Android.
The problem goes with the queries that require a 'where' clause; in my case update and delete, the things I needed to do in order to fix it was to not use the selection parameter as it is, I needed to do something like:
GamesContract.Games.GAME_NAME + " = ?"
db.delete(GamesContract.Games.TABLE_NAME, GamesContract.Games.GAME_NAME + " = ?", selectionArgs);
Basicaly the table name + " = ?" selection.
This way you are forcing to update only the needed table and prevent the sql injection to modify any place in your database through that same method.
I fixed it using SQLiteQueryBuilder
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
final SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(TABLE_NAME);
builder.setProjectionMap(buildProjectionMap());
builder.setStrict(true);
count = builder.delete(db, selection, selectionArgs);
}else {
count = db.delete(TABLE_NAME, selection, selectionArgs);
}
Related
When I try to update my data I get this error
java.lang.IllegalArgumentException: Update is not supported for content://com.example.recodedharran.booksinventory/books/3
I used the debugger and the match value appears to be -1 !
public int update(Uri uri, ContentValues contentValues, String selection,
String[] selectionArgs) {
final int match = sUriMatcher.match(uri);
switch (match) {
case BOOKS:
return updateBook(uri, contentValues, selection, selectionArgs);
case BOOKS_ID:
// For the PET_ID code, extract out the ID from the URI,
// so we know which row to update. Selection will be "_id=?" and selection
// arguments will be a String array containing the actual ID.
selection = BooksContract.BooksEntry._ID + "=?";
selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
return updateBook(uri, contentValues, selection, selectionArgs);
default:
throw new IllegalArgumentException("Update is not supported for " + uri);
}
}
private int updateBook(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
if (values.containsKey(BooksContract.BooksEntry.COLUMN_PRODUCT_NAME)) {
String name = values.getAsString(BooksContract.BooksEntry.COLUMN_PRODUCT_NAME);
if (name == null) {
throw new IllegalArgumentException("Pet requires a name");
}
if (values.size() == 0) {
return 0;
}
}
SQLiteDatabase database = mDbHelper.getReadableDatabase();
int rowsUpdated = database.update(BooksContract.BooksEntry.TABLE_NAME, values, selection, selectionArgs);
// If 1 or more rows were updated, then notify all listeners that the data at the
// given URI has changed
if (rowsUpdated != 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
// Return the number of rows updated
return rowsUpdated;
}
you are throwing this Exception here.
default:
throw new IllegalArgumentException("Update is not supported for " + uri);
It's clear that match does not equal the value in both cases (BOOKS_ID, BOOK)
check that (BOOKS_ID, BOOKS) always satisfy the switch statement or remove or change the
default:
throw new IllegalArgumentException("Update is not supported for " + uri);
to
default: return -1; //return `-1` for example or any integer that gives a relevant indication.
Here, this was the problem :]
sUriMatcher.addURI(BooksContract.CONTENT_AUTHORITY, BooksContract.PATH_BOOKS + "#/", BOOK_ID);
I am trying to retrieve the distinct values from my database using my contentprovider query and CursorLoader. While the CursorLoader does not allow a distinct specification, I discovered the setDistinct method that can be added to a querybuilder for adding this specification. I am not retrieving the desired result and curious as to why. My query looks like below
#Override
public Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder) {
// create SQLiteQueryBuilder for querying flower table
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(Flower.TABLE_NAME);
queryBuilder.setDistinct(true);
switch (uriMatcher.match(uri)) {
case OneFlower: // contact with specified id will be selected
queryBuilder.appendWhere(
Flower._ID + "=" + uri.getLastPathSegment());
break;
case CONTACTS: // all contacts will be selected
break;
default:
throw new UnsupportedOperationException(
getContext().getString(R.string.invalid_query_uri) + uri);
}
Cursor cursor = queryBuilder.query(dbHelper.getReadableDatabase(),
projection, selection, selectionArgs, null, null, sortOrder);
// configure to watch for content changes
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
while my CursorLoader looks like this
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
switch (id) {
case LOADER_ID:
return new CursorLoader(
getContext(),
DatabaseDescription.Flower.CONTENT_URI,
FROM_COLUMNS,
COLUMN_LOCATION + "<> ''",
null,
COLUMN_LOCATION + " ASC"
);
default:
if (BuildConfig.DEBUG)
throw new IllegalArgumentException("no id handled!");
return null;
}
}
I'm querying the ContactsContract.Data table to find phone records.
I get an error when I create a new CursorLoader:
java.lang.IllegalArgumentException: Invalid column deleted
My code:
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Data;
...
String[] projection = {
Phone.DELETED,
Phone.LOOKUP_KEY,
Phone.NUMBER,
Phone.TYPE,
Phone.LABEL,
Data.MIMETYPE,
Data.DISPLAY_NAME_PRIMARY
};
// "mimetype = ? AND deleted = ?"
String selection = Data.MIMETYPE + " = ? AND " Phone.DELETED + " = ?";
String[] args = {Phone.CONTENT_ITEM_TYPE, "0"};
return new CursorLoader(
this,
Data.CONTENT_URI,
projection,
selection,
args,
null);
Any idea why the Phone.DELETED column isn't included in the cursor? The documentation does say -
Some columns from the associated raw contact are also available
through an implicit join.
Looks like you've found a feature that has been documented in many places, but hadn't been implemented yet. I opened a bug for tracking this issue - lets see what AOSP guys have to say on the subject (bug report).
Meanwhile, you can use the following workaround:
Uri uri = ContactsContract.RawContactsEntity.CONTENT_URI;
String[] projection = {
Phone._ID,
Phone.DELETED,
//Phone.LOOKUP_KEY,
Phone.NUMBER,
Phone.TYPE,
Phone.LABEL,
Data.MIMETYPE,
Data.DISPLAY_NAME_PRIMARY
};
String selection = Data.MIMETYPE + " = ? AND " + Data.DELETED + " = ?";
String[] args = {
Phone.CONTENT_ITEM_TYPE, "0"
};
return new CursorLoader(
this,
uri,
projection,
selection,
args,
null);
Changes:
Use RawContactsEntity's URI
LOOKUP_KEY is not accessible via above URI - you'll have to execute additional query if you absolutely need this column
_ID column will be required if you are going to use the resulting Cursor in CursorAdapter.
Edit: following #MichaelAlanHuff's request I'm posting the parts of code which this answer is based upon
From com.android.providers.contacts.ContactsProvider2#queryLocal() (source code of ContactsProvider2):
protected Cursor queryLocal(final Uri uri, final String[] projection, String selection,
String[] selectionArgs, String sortOrder, final long directoryId,
final CancellationSignal cancellationSignal) {
final SQLiteDatabase db = mDbHelper.get().getReadableDatabase();
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
String groupBy = null;
String having = null;
String limit = getLimit(uri);
boolean snippetDeferred = false;
// The expression used in bundleLetterCountExtras() to get count.
String addressBookIndexerCountExpression = null;
final int match = sUriMatcher.match(uri);
switch (match) {
...
case DATA:
case PROFILE_DATA:
{
final String usageType = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
final int typeInt = getDataUsageFeedbackType(usageType, USAGE_TYPE_ALL);
setTablesAndProjectionMapForData(qb, uri, projection, false, typeInt);
if (uri.getBooleanQueryParameter(Data.VISIBLE_CONTACTS_ONLY, false)) {
qb.appendWhere(" AND " + Data.CONTACT_ID + " in " + Tables.DEFAULT_DIRECTORY);
}
break;
}
...
}
qb.setStrict(true);
// Auto-rewrite SORT_KEY_{PRIMARY, ALTERNATIVE} sort orders.
String localizedSortOrder = getLocalizedSortOrder(sortOrder);
Cursor cursor = query(db, qb, projection, selection, selectionArgs, localizedSortOrder, groupBy,
having, limit, cancellationSignal);
if (readBooleanQueryParameter(uri, Contacts.EXTRA_ADDRESS_BOOK_INDEX, false)) {
bundleFastScrollingIndexExtras(cursor, uri, db, qb, selection,
selectionArgs, sortOrder, addressBookIndexerCountExpression,
cancellationSignal);
}
if (snippetDeferred) {
cursor = addDeferredSnippetingExtra(cursor);
}
return cursor;
}
As you can see, there are two additional methods where SQLiteQueryBuilder used to build the query could be changed: setTablesAndProjectionMapForData() and additional query() method.
Source of com.android.providers.contacts.ContactsProvider2#setTablesAndProjectionMapForData():
private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
String[] projection, boolean distinct, boolean addSipLookupColumns, Integer usageType) {
StringBuilder sb = new StringBuilder();
sb.append(Views.DATA);
sb.append(" data");
appendContactPresenceJoin(sb, projection, RawContacts.CONTACT_ID);
appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
appendDataUsageStatJoin(
sb, usageType == null ? USAGE_TYPE_ALL : usageType, DataColumns.CONCRETE_ID);
qb.setTables(sb.toString());
boolean useDistinct = distinct || !ContactsDatabaseHelper.isInProjection(
projection, DISTINCT_DATA_PROHIBITING_COLUMNS);
qb.setDistinct(useDistinct);
final ProjectionMap projectionMap;
if (addSipLookupColumns) {
projectionMap =
useDistinct ? sDistinctDataSipLookupProjectionMap : sDataSipLookupProjectionMap;
} else {
projectionMap = useDistinct ? sDistinctDataProjectionMap : sDataProjectionMap;
}
qb.setProjectionMap(projectionMap);
appendAccountIdFromParameter(qb, uri);
}
Here you see the construction of table argument of the final query using StringBuilder which is being passed to several append*() methods. I'm not going to post their source code, but they really join the tables that appear in methods' names. If rawContacts table would be joined in, I'd expect to see a call to something like appendRawContactJoin() here...
For completeness: the other query() method that I mentioned does not modify table argument:
private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
String selection, String[] selectionArgs, String sortOrder, String groupBy,
String having, String limit, CancellationSignal cancellationSignal) {
if (projection != null && projection.length == 1
&& BaseColumns._COUNT.equals(projection[0])) {
qb.setProjectionMap(sCountProjectionMap);
}
final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, having,
sortOrder, limit, cancellationSignal);
if (c != null) {
c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
}
return c;
}
The inspection of the above chain of methods led me to the conclusion that there is an officially documented feature which is not implemented.
I have looked at this for a couple of days now and I completely can't work out why my content provider return 0 using the arguments I am passing it.
Here's my contentResolver code:
String[] expenditureProjection = {
BusinessOpsDatabase.COL_EXPEND_CAT_ID,
BusinessOpsDatabase.COL_EXPEND_DATE,
BusinessOpsDatabase.COL_EXPEND_AMOUNT,
BusinessOpsDatabase.COL_EXPEND_DESC,
BusinessOpsDatabase.COL_STERLING_EXCHANGE,
BusinessOpsDatabase.COL_COMPANY_ID,
BusinessOpsDatabase.CURRENCY_ID,
BusinessOpsDatabase.COL_MOD_DATE
};
// Defines a string to contain the selection clause
String selectionClause = null;
// An array to contain selection arguments
String[] selectionArgs = {expend_id.trim()};
selectionClause = BusinessOpsExpenditureProvider.EXPENDITURE_ID + "=?";
Log.d(TAG, expend_id+" Selected from list.");
Cursor expendCursor = getContentResolver().query(
BusinessOpsExpenditureProvider.CONTENT_URI, expenditureProjection, selectionClause, selectionArgs, null);
if (null == expendCursor) {
Log.d(TAG, "Expenditure cursor: Is null");
} else if (expendCursor.getCount() < 1) {
Log.d(TAG,"Expenditure cursor: Search was unsuccessful: "+expendCursor.getCount());
} else {
Log.d(TAG,"Expenditure cursor: Contains results");
int i=0;
expendCursor.moveToFirst();
// loop through cursor and populate country array
while (expendCursor.isAfterLast() == false)
{
expend_date_edit.setText(expendCursor.getString(1));
expend_amount_edit.setText(expendCursor.getString(3));
expend_desc_edit.setText(expendCursor.getString(4));
i++;
expendCursor.moveToNext();
}
}
Here's my content provider query method:
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = mDB.getWritableDatabase();
// A convenience class to help build the query
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(BusinessOpsDatabase.TABLE_EXPENDITURE);
switch (sURIMatcher.match(uri)) {
case EXPENDITURE:
if(selection != null && selectionArgs != null){
//values.get("company_contact");
String segment = uri.getLastPathSegment();
Log.d(TAG, "Last path segment: "+ segment);
String whereClause = BusinessOpsDatabase.EXPENDITURE_ID + "="+ selectionArgs[0];
Log.d(TAG, "Where clause: "+whereClause);
}
break;
case EXPENDITURE_ID:
// If this is a request for an individual status, limit the result set to that ID
qb.appendWhere(BusinessOpsDatabase.EXPENDITURE_ID + "=" + uri.getLastPathSegment());
break;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
// Query the underlying database
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, null);
// Notify the context's ContentResolver if the cursor result set changes
c.setNotificationUri(getContext().getContentResolver(), uri);
// Return the cursor to the result set
return c;
}
I'm printing the whereclause to the log and I see '_id=3' which should be fine because I have pulled off a copy of my SQLite database and I can see that the expenditure table has an _id 3 row in it. Any Ideas?
What an epic problem this has been. I found the error in my ContentResolver code.
selectionClause = BusinessOpsExpenditureProvider.EXPENDITURE_ID + "=?";
I was using the EXPENDITURE_ID variable from the provider rather than the database class. The line now reads.
selectionClause = BusinessOpsDatabase.EXPENDITURE_ID + "=?";
And works!
Strange behaviour of SQLite update in ContentProvider.
Update method:
#Override
public int update(Uri uri, ContentValues updateValues, String whereClause, String[] whereValues) {
SQLiteDatabase db = TasksContentProvider.dbHelper.getWritableDatabase();
int updatedRowsCount;
String finalWhere;
db.beginTransaction();
// Perform the update based on the incoming URI's pattern
try {
switch (uriMatcher.match(uri)) {
case MATCHER_TASKS:
updatedRowsCount = db.update(TasksTable.TABLE_NAME, updateValues, whereClause, whereValues);
break;
case MATCHER_TASK:
String id = uri.getPathSegments().get(TasksTable.TASK_ID_PATH_POSITION);
finalWhere = TasksTable._ID + " = " + id;
// if we were passed a 'where' arg, add that to our 'finalWhere'
if (whereClause != null) {
finalWhere = finalWhere + " AND " + whereClause;
}
updatedRowsCount = db.update(TasksTable.TABLE_NAME, updateValues, finalWhere, whereValues);
break;
default:
// Incoming URI pattern is invalid: halt & catch fire.
throw new IllegalArgumentException("Unknown URI " + uri);
}
} finally {
db.endTransaction();
}
if (updatedRowsCount > 0) {
DVSApplication.getContext().getContentResolver().notifyChange(uri, null);
}
return updatedRowsCount;
}
Query method:
#Override
public Cursor query(Uri uri, String[] selectedColumns, String whereClause, String[] whereValues, String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
// Choose the projection and adjust the "where" clause based on URI pattern-matching.
switch (uriMatcher.match(uri)) {
case MATCHER_TASKS:
qb.setTables(TasksTable.TABLE_NAME);
qb.setProjectionMap(tasksProjection);
break;
// asking for a single comic - use the rage comics projection, but add a where clause to only return the one
// comic
case MATCHER_TASK:
qb.setTables(TasksTable.TABLE_NAME);
qb.setProjectionMap(tasksProjection);
// Find the comic ID itself in the incoming URI
String taskId = uri.getPathSegments().get(TasksTable.TASK_ID_PATH_POSITION);
qb.appendWhere(TasksTable._ID + "=" + taskId);
break;
case MATCHER_TASK_COMMENTS:
qb.setTables(TaskCommentsTable.TABLE_NAME);
qb.setProjectionMap(taskCommentsProjection);
break;
case MATCHER_TASK_COMMENT:
qb.setTables(TaskCommentsTable.TABLE_NAME);
qb.setProjectionMap(taskCommentsProjection);
String commentId = uri.getPathSegments().get(TaskCommentsTable.TASK_COMMENT_ID_PATH_POSITION);
qb.appendWhere(TaskCommentsTable._ID + "=" + commentId);
break;
default:
// If the URI doesn't match any of the known patterns, throw an exception.
throw new IllegalArgumentException("Unknown URI " + uri);
}
SQLiteDatabase db = TasksContentProvider.dbHelper.getReadableDatabase();
// the two nulls here are 'grouping' and 'filtering by group'
Cursor cursor = qb.query(db, selectedColumns, whereClause, whereValues, null, null, sortOrder);
// Tell the Cursor about the URI to watch, so it knows when its source data changes
cursor.setNotificationUri(DVSApplication.getContext().getContentResolver(), uri);
return cursor;
}
Trying to update and row.
int affectedRowsCount = provider.update(Uri.parse(TasksTable.CONTENT_URI.toString() + "/"+ taskId), task.getContentValues(), null, null);
affectedRowsCount is eqaul to 1
Check if row is updated
Cursor cs = provider.query(TasksTable.CONTENT_URI, new String[] {TasksTable.TASK_STATE_VALUE}, TasksTable._ID +" = ?", new String[] {String.valueOf(taskId)}, null);
if(cs.moveToFirst()) {
String state = cs.getString(cs.getColumnIndex(TasksTable.TASK_STATE_VALUE));
}
state is the same as before update. Though update went succesful because affectedRowsCount is equal to 1 but selecting by the same id the same row seems that row wasn't updated at all.
In your update method you are using a transaction, but you never set the result as successful, so everytime you reach db.endTransaction() a rollback is performed. That's why your update isn't stored.
The changes will be rolled back if any transaction is ended without
being marked as clean (by calling setTransactionSuccessful). Otherwise
they will be committed.
You need to use
db.setTransactionSuccessful();
when your update is finished without errors. In your code, it should be after both your db.update.