I have a Content Provider which I tested with the Drozer framework and it turned out that the projection in the query() method is vulnerable to injection. Including "* FROM SQLITE_MASTER --" lists all tables. Which is the best way to guard against this? I added filtering of certain characters:
public Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
/*
* Filter queries that contain certain characters to guard against SQL injection
*/
for (String query : projection) {
if (query.contains("*") || query.contains(";") || query.contains("'") || query.contains("\"")) {
//Possible SQL injection attack, leave the query
return null;
}
}
I understand that blacklist filtering is not the way to go and parameterizing the projection would be better. However I can't see a way to do this in the query() method. It doesn't have two arguments like the selection has "selection" then "selectionArgs". How best to guard against injection without blacklisting?
If you want to allow your content provider's clients to use arbitrarily complex SQL, there is nothing much you can do; a projection like SomeColumn AS "* FROM sqlite_master -- ;'""" would be perfectly valid and harmless.
To prevent access to sensitive data, that data would have to be in another database.
However, you could impose the restriction that clients can read only a predefined set of columns, unchanged.
To enforce this, check that all strings in projection are equal to one of those column names.
Probably easily way to prevent against SQL injection in projection parameter is to use setProjectionMap method on SQLiteQueryBuilder. As stated in documentation,
If a projection map is set it must contain all column names the user may request, even if the key and value are the same.
I tried to fix a vulnerable content provider by adding this method, and here is what drozer showed me:
dz> run app.provider.query content://myContentProvider/myPath/ --projection "'"
Invalid column '
Related
I have been studying Content Providers by reading Android Developer Fundamentals (Version 1).
Under section "11.1 Share Data Through Content Providers" / "The query() method" there is a note that states
Note: The insert, delete, and update methods are provided for
convenience and clarity. Technically, the query method could handle
all requests, including those to insert, delete, and update data.
How can query method be used to insert / delete / and update data? The query method has the following signature and does not take in custom SQL strings.
public Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder){}
I have not been able to find any resources that shows how this can be done. All seems to utilize the provided methods.
Frankly, that is a really poor job in that training guide. All it will do is confuse people.
I presume what they mean is that there is nothing magic about query() that forces it to only perform SQL SELECT statements. A ContentProvider is a facade — you can use it to store and retrieve from pretty much whatever you want, not just SQLite.
So, you could have your query() method:
Examine the Uri
Look for it to be of the form content://your.authority/something/insert/put/data/here
Parse the Uri to get the put, data, and here values
Insert those in a table under some pre-determined columns
Return an empty MatrixCursor
Or, it could:
Examine the Uri
Look for it to be of the form content://your.authority/something/insert
Insert a row using the projection for the columns and the selectionArgs as the values to put in those columns
Return an empty MatrixCursor
I do not know why anyone would do that, but it is certainly possible.
I have several questions related to the official LoaderManager example when implementing filtering in an application (i.e. SQLite database, using Content Provider, using CONTENT_URI when no filtering is used, or forming the URI for filtering via concatenation of the CONTENT_FILTER_URI and the content of the mCurFilter variable.
Fistly, I need confirmation, that I understand the filtering URI correctly. Earlier, I did use the CONTENT_URI and set the selection, selectionArgs, and sortOrder when creating new CursorLoader(...) in the onCreateLoader method of the activity that implements LoaderManager.LoaderCallbacks<Cursor>. I did not understand why others use a special CONTENT_FILTER_URI.
The official example passes null for the selectionArgs, and the filter value is probably extracted as filter = uri.getLastPathSegment(); and added used for the selectionArgs. In my case, I am passing null also for the selection and forming it in the content provider like:
selection = ProductTable.COLUMN_NAME + " like ?";
selectionArgs = new String[]{ "%" + filter +"%" };
My understanding now is that:
The filter value can be used also other ways than simply put to a simple and single SQL command, and this is the reason to use the CONTENT_FILTER_URI.
The CONTENT_FILTER_URI makes getting the information more abstract, i.e. less SQL specific, more web-service like access to the data.
With respect to that, I am still confused why also the sortOrder is not expressed in the URI somehow. Is it a kind of tradeoff? (I hope the official example simply does not take a slopy approach, and there is real reason to form the select argument outside the content provider.)
Should I always parse the user-entered filter value? The reason is that a user can enter whatever filter value -- also the forbidden characters can be inserted. This way the unknown URI path can be formed and the URI may not be recognized. Or even worse, there may be a chance that some kind of SQL injection can be done, or at least the application can be crashed. Am I too paranoic when thinking about URI and the injection errors? Are there some rules or tools mentioned at developer.android.com or elsewhere that are related to the problem?
i would like sort the call logs by cached name. The cached name can be null.
So in the case of a null cached name, i would like to have the phone number for an alias.
in sqlite, there is the ifnull() function.
ifnull details
I try :
String[] projections = new String[] { Calls._ID, Calls.NUMBER, Calls.DATE, Calls.TYPE, Calls.DURATION, "ifnull("+Calls.CACHED_NAME+","+Calls.NUMBER+") as display_name" };
Cursor cursor_call = ctx.getContentResolver().query(URI_CALL_LOGS,
projections,
null,
null,
null);
But i have an error with my use of ifnull an i don't find a sample of this function.
java.lang.IllegalArgumentException: Invalid column ifnull(name,number) as display_name
Because of the way you're creating the query, things like parentheses are being auto-escaped to prevent SQL-injection attacks.
In situations where you have direct access to the raw DB (not through a contentProvider), you can use a SQLiteDatabase.rawQuery, like so:
myDB.rawquery("select * from a, b where a.id=b.someid", null);
But in this case it looks like you want to access the contact app's call_log database, which you can only do through the provided ContentResolver. Since it's intentionally designed to only let you send values and not SQL commands as variables, ifnull(...) isn't going to work, and you'll need to choose between name and number using logic when you're pulling data from the cursor.
I am looking to perform the following query (in pseudo-code) on Android:
SELECT C.ID, C.NAME, CASE ISNULL(G.GROUPID,0) = 0 THEN 0 ELSE 1 END INGROUP
FROM CONTACTS C
LEFT JOIN GROUPMEMBERSHIP G ON G.CONTACTID = C.ID AND G.GROUPID = ?
I am looking to select the ID and Name of ALL contacts in the system address book, via the default Contacts ContentProvider, along with a
0/1 field indicating whether the contact is a member of group ? .
I could of course get all contacts easily enough, then loop through and query the membership separately easy enough in my Adapter class, but I'd imagine performing the two queries as one outer joined query would yield much better performance.
Can I do this with the standard high-level string-projection and ContentResolver.query() method? Or would this kind of query require digging into more direct SQL execution?
Edit: Okay, so this doesn't actually solve the question asked, because eidylon is tied to an existing ContentProvider as mentioned in their question. However, this does cover how you do a JOIN if you own the ContentProvider source and API. So I'll leave it for those who want to know how to handle that case.
This is easy! But unintuitive... :)
query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
Okay, so what is URI? Typically, you have one URI per table.
content://com.example.coolapp.contacts serves data out of your CONTACTS table.
content://com.example.coolapp.groupmembers serves data out of your GROUPMEMBERSHIP table.
But URI is really just a string. Use it however you like. Make a block of code in your ContentProvider that responds to content://com.example.coolapp.contacts_in_group. Within that block of code in the ContentProvider, you can get raw access to your SQLite DB, unfettered by the limited query() data model. Feel free to use it!
Define your selection fields however you like. They don't have to map to table column names -- map them how you need to, in order to get your parameters in.
Define your projection how you need -- It may contain columns from both tables after the join.
Bing, you're done. Google does this same model internally in their own code -- Go look at the Contacts provider API -- you see "bla.RawContact" and "bla.Contact" and etc as content URIs. Each serves data out of the same table in the DB -- the different URIs just provide different views of that same table!
Nope, you can't do that kind of queries with the ContentResolver.query() method.
You will need to write something like this:
SQLiteDatabase db = YourActivity.getDbHelper().getReadableDatabase();
String query = yourLongQuery;
Cursor c = db.rawQuery(query, null);
YourActivity.startManagingCursor(c);
c.setNotificationUri(YourActivity.getContentResolver(), YourContentProvider.CONTENT_URI);
You can't do that because ContentResolver has only one query method:
query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
there's no parameter for tables or FROM clauses.
Android's API provides a clean mechanism via SQLite to make queries into the contact list. However, I am not sure how to limit the results:
Cursor cur = ((Activity)mCtx).managedQuery(
People.CONTENT_URI,
columns,
"LIMIT ? OFFSET ?",
new String[] { Integer.toString(limit), Integer.toString(offset) },
null
);
Doesn't work.
Actually, depending on the provider you can append a limit to the URI as follows:
uri.buildUpon().appendQueryParameter("limit", "40").build()
I know the MediaProvider handles this and from looking at recent code it seems you can do it with contacts too.
You are accessing a ContentProvider, not SQLite, when you query the Contacts ContentProvider. The ContentProvider interface does not support a LIMIT clause directly.
If you are directly accessing a SQLite database of your own, use the rawQuery() method on SQLiteDatabase and add a LIMIT clause.
I found out from this bug that Android uses the following regex to parse the LIMIT clause of a query:
From <framework/base/core/java/android/database/sqlite/SQLiteQueryBuilder.java>
LIMIT clause is checked with following sLimitPattern.
private static final Pattern sLimitPattern = Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?");
Note that the regex does accept the format offsetNumber,limitNumber even though it doesn't accept the OFFSET statement directly.
I think you have to do this sort of manually. The Cursor object that is returned from the managedQuery call doesn't execute a full query right off. You have to use the Cursor.move*() methods to jump around the result set.
If you want to limit it, then create your own limit while looping through the results. If you need paging, then you can use the Cursor.moveToPosition(startIndex) and start reading from there.
You can specify the "limit" parameter in the "order" parameter, maybe even inside other parameters if you don't want to sort, because you'll have to specify a column to sort by then:
mContentResolver.query(uri, columnNames, null, null, "id LIMIT 1");