summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2019-07-17 18:54:49 -0600
committerBryan Ferris <bferris@google.com>2019-09-10 20:46:41 +0000
commitffec00b013a34800681ec90c88ad40337aa20c1a (patch)
tree5163edb302254b9dd7de196e9cd23de5aa1a6d41
parent14a5b9171ae9817e03bfef4daf988ab1e9528e59 (diff)
downloadandroid_packages_providers_DownloadProvider-ffec00b013a34800681ec90c88ad40337aa20c1a.zip
android_packages_providers_DownloadProvider-ffec00b013a34800681ec90c88ad40337aa20c1a.tar.gz
android_packages_providers_DownloadProvider-ffec00b013a34800681ec90c88ad40337aa20c1a.tar.bz2
RESTRICT AUTOMERGE Enable stricter SQLiteQueryBuilder options.
Malicious callers can leak side-channel information by using subqueries in any untrusted inputs where SQLite allows "expr" values. This change starts using setStrictColumns() and setStrictGrammar() on SQLiteQueryBuilder to block this class of attacks. This means we now need to define the projection mapping of valid columns, which consists of both the columns defined in the public API and columns read internally by DownloadInfo.Reader. We're okay growing sAppReadableColumnsSet like this, since we're relying on our trusted WHERE clause to filter away any rows that don't belong to the calling UID. Remove the legacy Lexer code, since we're now internally relying on the robust and well-tested SQLiteTokenizer logic. Bug: 135270103, 135269143 Test: atest DownloadProviderTests Test: atest CtsAppTestCases:android.app.cts.DownloadManagerTest Change-Id: I8e595e1470df586a3d593b7851305da413e44347
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java187
-rw-r--r--src/com/android/providers/downloads/Helpers.java262
2 files changed, 107 insertions, 342 deletions
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index eb0313c..c68d702 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -83,7 +83,6 @@ import com.android.internal.util.Preconditions;
import libcore.io.IoUtils;
-import com.google.android.collect.Maps;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
@@ -96,7 +95,6 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
-import java.util.List;
import java.util.Map;
/**
@@ -156,48 +154,108 @@ public final class DownloadProvider extends ContentProvider {
Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
};
- private static final String[] sAppReadableColumnsArray = new String[] {
- Downloads.Impl._ID,
- Downloads.Impl.COLUMN_APP_DATA,
- Downloads.Impl._DATA,
- Downloads.Impl.COLUMN_MIME_TYPE,
- Downloads.Impl.COLUMN_VISIBILITY,
- Downloads.Impl.COLUMN_DESTINATION,
- Downloads.Impl.COLUMN_CONTROL,
- Downloads.Impl.COLUMN_STATUS,
- Downloads.Impl.COLUMN_LAST_MODIFICATION,
- Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE,
- Downloads.Impl.COLUMN_NOTIFICATION_CLASS,
- Downloads.Impl.COLUMN_TOTAL_BYTES,
- Downloads.Impl.COLUMN_CURRENT_BYTES,
- Downloads.Impl.COLUMN_TITLE,
- Downloads.Impl.COLUMN_DESCRIPTION,
- Downloads.Impl.COLUMN_URI,
- Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
- Downloads.Impl.COLUMN_FILE_NAME_HINT,
- Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
- Downloads.Impl.COLUMN_DELETED,
- OpenableColumns.DISPLAY_NAME,
- OpenableColumns.SIZE,
- };
+ private static void addMapping(Map<String, String> map, String column) {
+ if (!map.containsKey(column)) {
+ map.put(column, column);
+ }
+ }
- private static final HashSet<String> sAppReadableColumnsSet;
- private static final HashMap<String, String> sColumnsMap;
+ private static void addMapping(Map<String, String> map, String column, String rawColumn) {
+ if (!map.containsKey(column)) {
+ map.put(column, rawColumn + " AS " + column);
+ }
+ }
+ private static final Map<String, String> sDownloadsMap = new ArrayMap<>();
static {
- sAppReadableColumnsSet = new HashSet<String>();
- for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
- sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
- }
+ final Map<String, String> map = sDownloadsMap;
+
+ // Columns defined by public API
+ addMapping(map, DownloadManager.COLUMN_ID,
+ Downloads.Impl._ID);
+ addMapping(map, DownloadManager.COLUMN_LOCAL_FILENAME,
+ Downloads.Impl._DATA);
+ addMapping(map, DownloadManager.COLUMN_MEDIAPROVIDER_URI);
+ addMapping(map, DownloadManager.COLUMN_DESTINATION);
+ addMapping(map, DownloadManager.COLUMN_TITLE);
+ addMapping(map, DownloadManager.COLUMN_DESCRIPTION);
+ addMapping(map, DownloadManager.COLUMN_URI);
+ addMapping(map, DownloadManager.COLUMN_STATUS);
+ addMapping(map, DownloadManager.COLUMN_FILE_NAME_HINT);
+ addMapping(map, DownloadManager.COLUMN_MEDIA_TYPE,
+ Downloads.Impl.COLUMN_MIME_TYPE);
+ addMapping(map, DownloadManager.COLUMN_TOTAL_SIZE_BYTES,
+ Downloads.Impl.COLUMN_TOTAL_BYTES);
+ addMapping(map, DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP,
+ Downloads.Impl.COLUMN_LAST_MODIFICATION);
+ addMapping(map, DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR,
+ Downloads.Impl.COLUMN_CURRENT_BYTES);
+ addMapping(map, DownloadManager.COLUMN_ALLOW_WRITE);
+ addMapping(map, DownloadManager.COLUMN_LOCAL_URI,
+ "'placeholder'");
+ addMapping(map, DownloadManager.COLUMN_REASON,
+ "'placeholder'");
+
+ // Columns defined by OpenableColumns
+ addMapping(map, OpenableColumns.DISPLAY_NAME,
+ Downloads.Impl.COLUMN_TITLE);
+ addMapping(map, OpenableColumns.SIZE,
+ Downloads.Impl.COLUMN_TOTAL_BYTES);
+
+ // Allow references to all other columns to support DownloadInfo.Reader;
+ // we're already using SQLiteQueryBuilder to block access to other rows
+ // that don't belong to the calling UID.
+ addMapping(map, Downloads.Impl._ID);
+ addMapping(map, Downloads.Impl._DATA);
+ addMapping(map, Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
+ addMapping(map, Downloads.Impl.COLUMN_ALLOW_METERED);
+ addMapping(map, Downloads.Impl.COLUMN_ALLOW_ROAMING);
+ addMapping(map, Downloads.Impl.COLUMN_ALLOW_WRITE);
+ addMapping(map, Downloads.Impl.COLUMN_APP_DATA);
+ addMapping(map, Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
+ addMapping(map, Downloads.Impl.COLUMN_CONTROL);
+ addMapping(map, Downloads.Impl.COLUMN_COOKIE_DATA);
+ addMapping(map, Downloads.Impl.COLUMN_CURRENT_BYTES);
+ addMapping(map, Downloads.Impl.COLUMN_DELETED);
+ addMapping(map, Downloads.Impl.COLUMN_DESCRIPTION);
+ addMapping(map, Downloads.Impl.COLUMN_DESTINATION);
+ addMapping(map, Downloads.Impl.COLUMN_ERROR_MSG);
+ addMapping(map, Downloads.Impl.COLUMN_FAILED_CONNECTIONS);
+ addMapping(map, Downloads.Impl.COLUMN_FILE_NAME_HINT);
+ addMapping(map, Downloads.Impl.COLUMN_FLAGS);
+ addMapping(map, Downloads.Impl.COLUMN_IS_PUBLIC_API);
+ addMapping(map, Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
+ addMapping(map, Downloads.Impl.COLUMN_LAST_MODIFICATION);
+ addMapping(map, Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
+ addMapping(map, Downloads.Impl.COLUMN_MEDIA_SCANNED);
+ addMapping(map, Downloads.Impl.COLUMN_MEDIASTORE_URI);
+ addMapping(map, Downloads.Impl.COLUMN_MIME_TYPE);
+ addMapping(map, Downloads.Impl.COLUMN_NO_INTEGRITY);
+ addMapping(map, Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
+ addMapping(map, Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS);
+ addMapping(map, Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
+ addMapping(map, Downloads.Impl.COLUMN_OTHER_UID);
+ addMapping(map, Downloads.Impl.COLUMN_REFERER);
+ addMapping(map, Downloads.Impl.COLUMN_STATUS);
+ addMapping(map, Downloads.Impl.COLUMN_TITLE);
+ addMapping(map, Downloads.Impl.COLUMN_TOTAL_BYTES);
+ addMapping(map, Downloads.Impl.COLUMN_URI);
+ addMapping(map, Downloads.Impl.COLUMN_USER_AGENT);
+ addMapping(map, Downloads.Impl.COLUMN_VISIBILITY);
+
+ addMapping(map, Constants.ETAG);
+ addMapping(map, Constants.RETRY_AFTER_X_REDIRECT_COUNT);
+ addMapping(map, Constants.UID);
+ }
- sColumnsMap = Maps.newHashMap();
- sColumnsMap.put(OpenableColumns.DISPLAY_NAME,
- Downloads.Impl.COLUMN_TITLE + " AS " + OpenableColumns.DISPLAY_NAME);
- sColumnsMap.put(OpenableColumns.SIZE,
- Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + OpenableColumns.SIZE);
+ private static final Map<String, String> sHeadersMap = new ArrayMap<>();
+ static {
+ final Map<String, String> map = sHeadersMap;
+ addMapping(map, "id");
+ addMapping(map, Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID);
+ addMapping(map, Downloads.Impl.RequestHeaders.COLUMN_HEADER);
+ addMapping(map, Downloads.Impl.RequestHeaders.COLUMN_VALUE);
}
- private static final List<String> downloadManagerColumnsList =
- Arrays.asList(DownloadManager.UNDERLYING_COLUMNS);
@VisibleForTesting
SystemFacade mSystemFacade;
@@ -1304,28 +1362,6 @@ public final class DownloadProvider extends ContentProvider {
return qb.query(db, projection, null, null, null, null, null);
}
- if (shouldRestrictVisibility()) {
- if (projection == null) {
- projection = sAppReadableColumnsArray.clone();
- } else {
- // check the validity of the columns in projection
- for (int i = 0; i < projection.length; ++i) {
- if (!sAppReadableColumnsSet.contains(projection[i]) &&
- !downloadManagerColumnsList.contains(projection[i])) {
- throw new IllegalArgumentException(
- "column " + projection[i] + " is not allowed in queries");
- }
- }
- }
-
- for (int i = 0; i < projection.length; i++) {
- final String newColumn = sColumnsMap.get(projection[i]);
- if (newColumn != null) {
- projection[i] = newColumn;
- }
- }
- }
-
if (Constants.LOGVV) {
logVerboseQueryInfo(projection, selection, selectionArgs, sort, db);
}
@@ -1418,26 +1454,11 @@ public final class DownloadProvider extends ContentProvider {
}
/**
- * @return true if we should restrict the columns readable by this caller
- */
- private boolean shouldRestrictVisibility() {
- int callingUid = Binder.getCallingUid();
- return Binder.getCallingPid() != Process.myPid()
- && callingUid != mSystemUid
- && callingUid != Process.SHELL_UID
- && callingUid != Process.ROOT_UID;
- }
-
- /**
* Updates a row in the database
*/
@Override
public int update(final Uri uri, final ContentValues values,
final String where, final String[] whereArgs) {
- if (shouldRestrictVisibility()) {
- Helpers.validateSelection(where, sAppReadableColumnsSet);
- }
-
final Context context = getContext();
final ContentResolver resolver = context.getContentResolver();
@@ -1602,6 +1623,8 @@ public final class DownloadProvider extends ContentProvider {
*/
private SQLiteQueryBuilder getQueryBuilder(final Uri uri, int match) {
final String table;
+ final Map<String, String> projectionMap;
+
final StringBuilder where = new StringBuilder();
switch (match) {
// The "my_downloads" view normally limits the caller to operating
@@ -1612,6 +1635,7 @@ public final class DownloadProvider extends ContentProvider {
// fall-through
case MY_DOWNLOADS:
table = DB_TABLE;
+ projectionMap = sDownloadsMap;
if (getContext().checkCallingOrSelfPermission(
PERMISSION_ACCESS_ALL) != PackageManager.PERMISSION_GRANTED) {
appendWhereExpression(where, Constants.UID + "=" + Binder.getCallingUid()
@@ -1627,6 +1651,7 @@ public final class DownloadProvider extends ContentProvider {
// fall-through
case ALL_DOWNLOADS:
table = DB_TABLE;
+ projectionMap = sDownloadsMap;
break;
// Headers are limited to callers holding the ACCESS_ALL_DOWNLOADS
@@ -1634,6 +1659,7 @@ public final class DownloadProvider extends ContentProvider {
case MY_DOWNLOADS_ID_HEADERS:
case ALL_DOWNLOADS_ID_HEADERS:
table = Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE;
+ projectionMap = sHeadersMap;
appendWhereExpression(where, Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "="
+ getDownloadIdFromUri(uri));
break;
@@ -1643,8 +1669,11 @@ public final class DownloadProvider extends ContentProvider {
}
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- qb.setStrict(true);
qb.setTables(table);
+ qb.setProjectionMap(projectionMap);
+ qb.setStrict(true);
+ qb.setStrictColumns(true);
+ qb.setStrictGrammar(true);
qb.appendWhere(where);
return qb;
}
@@ -1661,10 +1690,6 @@ public final class DownloadProvider extends ContentProvider {
*/
@Override
public int delete(final Uri uri, final String where, final String[] whereArgs) {
- if (shouldRestrictVisibility()) {
- Helpers.validateSelection(where, sAppReadableColumnsSet);
- }
-
final Context context = getContext();
final ContentResolver resolver = context.getContentResolver();
final JobScheduler scheduler = context.getSystemService(JobScheduler.class);
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 565aa52..226fb48 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -62,6 +62,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Random;
import java.util.Set;
+import java.util.function.BiConsumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -679,265 +680,4 @@ public class Helpers {
// For permission related purposes, any package belonging to the given uid should work.
return packages[0];
}
-
- /**
- * Checks whether this looks like a legitimate selection parameter
- */
- public static void validateSelection(String selection, Set<String> allowedColumns) {
- try {
- if (selection == null || selection.isEmpty()) {
- return;
- }
- Lexer lexer = new Lexer(selection, allowedColumns);
- parseExpression(lexer);
- if (lexer.currentToken() != Lexer.TOKEN_END) {
- throw new IllegalArgumentException("syntax error");
- }
- } catch (RuntimeException ex) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex);
- } else if (false) {
- Log.d(Constants.TAG, "invalid selection triggered " + ex);
- }
- throw ex;
- }
-
- }
-
- // expression <- ( expression ) | statement [AND_OR ( expression ) | statement] *
- // | statement [AND_OR expression]*
- private static void parseExpression(Lexer lexer) {
- for (;;) {
- // ( expression )
- if (lexer.currentToken() == Lexer.TOKEN_OPEN_PAREN) {
- lexer.advance();
- parseExpression(lexer);
- if (lexer.currentToken() != Lexer.TOKEN_CLOSE_PAREN) {
- throw new IllegalArgumentException("syntax error, unmatched parenthese");
- }
- lexer.advance();
- } else {
- // statement
- parseStatement(lexer);
- }
- if (lexer.currentToken() != Lexer.TOKEN_AND_OR) {
- break;
- }
- lexer.advance();
- }
- }
-
- // statement <- COLUMN COMPARE VALUE
- // | COLUMN IS NULL
- private static void parseStatement(Lexer lexer) {
- // both possibilities start with COLUMN
- if (lexer.currentToken() != Lexer.TOKEN_COLUMN) {
- throw new IllegalArgumentException("syntax error, expected column name");
- }
- lexer.advance();
-
- // statement <- COLUMN COMPARE VALUE
- if (lexer.currentToken() == Lexer.TOKEN_COMPARE) {
- lexer.advance();
- if (lexer.currentToken() != Lexer.TOKEN_VALUE) {
- throw new IllegalArgumentException("syntax error, expected quoted string");
- }
- lexer.advance();
- return;
- }
-
- // statement <- COLUMN IS NULL
- if (lexer.currentToken() == Lexer.TOKEN_IS) {
- lexer.advance();
- if (lexer.currentToken() != Lexer.TOKEN_NULL) {
- throw new IllegalArgumentException("syntax error, expected NULL");
- }
- lexer.advance();
- return;
- }
-
- // didn't get anything good after COLUMN
- throw new IllegalArgumentException("syntax error after column name");
- }
-
- /**
- * A simple lexer that recognizes the words of our restricted subset of SQL where clauses
- */
- private static class Lexer {
- public static final int TOKEN_START = 0;
- public static final int TOKEN_OPEN_PAREN = 1;
- public static final int TOKEN_CLOSE_PAREN = 2;
- public static final int TOKEN_AND_OR = 3;
- public static final int TOKEN_COLUMN = 4;
- public static final int TOKEN_COMPARE = 5;
- public static final int TOKEN_VALUE = 6;
- public static final int TOKEN_IS = 7;
- public static final int TOKEN_NULL = 8;
- public static final int TOKEN_END = 9;
-
- private final String mSelection;
- private final Set<String> mAllowedColumns;
- private int mOffset = 0;
- private int mCurrentToken = TOKEN_START;
- private final char[] mChars;
-
- public Lexer(String selection, Set<String> allowedColumns) {
- mSelection = selection;
- mAllowedColumns = allowedColumns;
- mChars = new char[mSelection.length()];
- mSelection.getChars(0, mChars.length, mChars, 0);
- advance();
- }
-
- public int currentToken() {
- return mCurrentToken;
- }
-
- public void advance() {
- char[] chars = mChars;
-
- // consume whitespace
- while (mOffset < chars.length && chars[mOffset] == ' ') {
- ++mOffset;
- }
-
- // end of input
- if (mOffset == chars.length) {
- mCurrentToken = TOKEN_END;
- return;
- }
-
- // "("
- if (chars[mOffset] == '(') {
- ++mOffset;
- mCurrentToken = TOKEN_OPEN_PAREN;
- return;
- }
-
- // ")"
- if (chars[mOffset] == ')') {
- ++mOffset;
- mCurrentToken = TOKEN_CLOSE_PAREN;
- return;
- }
-
- // "?"
- if (chars[mOffset] == '?') {
- ++mOffset;
- mCurrentToken = TOKEN_VALUE;
- return;
- }
-
- // "=" and "=="
- if (chars[mOffset] == '=') {
- ++mOffset;
- mCurrentToken = TOKEN_COMPARE;
- if (mOffset < chars.length && chars[mOffset] == '=') {
- ++mOffset;
- }
- return;
- }
-
- // ">" and ">="
- if (chars[mOffset] == '>') {
- ++mOffset;
- mCurrentToken = TOKEN_COMPARE;
- if (mOffset < chars.length && chars[mOffset] == '=') {
- ++mOffset;
- }
- return;
- }
-
- // "<", "<=" and "<>"
- if (chars[mOffset] == '<') {
- ++mOffset;
- mCurrentToken = TOKEN_COMPARE;
- if (mOffset < chars.length && (chars[mOffset] == '=' || chars[mOffset] == '>')) {
- ++mOffset;
- }
- return;
- }
-
- // "!="
- if (chars[mOffset] == '!') {
- ++mOffset;
- mCurrentToken = TOKEN_COMPARE;
- if (mOffset < chars.length && chars[mOffset] == '=') {
- ++mOffset;
- return;
- }
- throw new IllegalArgumentException("Unexpected character after !");
- }
-
- // columns and keywords
- // first look for anything that looks like an identifier or a keyword
- // and then recognize the individual words.
- // no attempt is made at discarding sequences of underscores with no alphanumeric
- // characters, even though it's not clear that they'd be legal column names.
- if (isIdentifierStart(chars[mOffset])) {
- int startOffset = mOffset;
- ++mOffset;
- while (mOffset < chars.length && isIdentifierChar(chars[mOffset])) {
- ++mOffset;
- }
- String word = mSelection.substring(startOffset, mOffset);
- if (mOffset - startOffset <= 4) {
- if (word.equals("IS")) {
- mCurrentToken = TOKEN_IS;
- return;
- }
- if (word.equals("OR") || word.equals("AND")) {
- mCurrentToken = TOKEN_AND_OR;
- return;
- }
- if (word.equals("NULL")) {
- mCurrentToken = TOKEN_NULL;
- return;
- }
- }
- if (mAllowedColumns.contains(word)) {
- mCurrentToken = TOKEN_COLUMN;
- return;
- }
- throw new IllegalArgumentException("unrecognized column or keyword: " + word);
- }
-
- // quoted strings
- if (chars[mOffset] == '\'') {
- ++mOffset;
- while (mOffset < chars.length) {
- if (chars[mOffset] == '\'') {
- if (mOffset + 1 < chars.length && chars[mOffset + 1] == '\'') {
- ++mOffset;
- } else {
- break;
- }
- }
- ++mOffset;
- }
- if (mOffset == chars.length) {
- throw new IllegalArgumentException("unterminated string");
- }
- ++mOffset;
- mCurrentToken = TOKEN_VALUE;
- return;
- }
-
- // anything we don't recognize
- throw new IllegalArgumentException("illegal character: " + chars[mOffset]);
- }
-
- private static final boolean isIdentifierStart(char c) {
- return c == '_' ||
- (c >= 'A' && c <= 'Z') ||
- (c >= 'a' && c <= 'z');
- }
-
- private static final boolean isIdentifierChar(char c) {
- return c == '_' ||
- (c >= 'A' && c <= 'Z') ||
- (c >= 'a' && c <= 'z') ||
- (c >= '0' && c <= '9');
- }
- }
}