diff options
3 files changed, 484 insertions, 0 deletions
diff --git a/src/com/android/providers/tv/TvProvider.java b/src/com/android/providers/tv/TvProvider.java index b324ba3..78dd724 100644 --- a/src/com/android/providers/tv/TvProvider.java +++ b/src/com/android/providers/tv/TvProvider.java @@ -63,6 +63,8 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; import com.android.providers.tv.util.SqlParams; +import com.android.providers.tv.util.SqliteTokenFinder; +import java.util.Locale; import libcore.io.IoUtils; import java.io.ByteArrayOutputStream; @@ -1667,11 +1669,25 @@ public class TvProvider extends ContentProvider { // database. value = "NULL AS " + DatabaseUtils.sqlEscapeString(columnName); columnProjectionMap.put(columnName, value); + + if (needEventLog(columnName)) { + android.util.EventLog.writeEvent(0x534e4554, "135269669", -1, ""); + } } } return columnProjectionMap; } + private boolean needEventLog(String columnName) { + for (int i = 0; i < columnName.length(); i++) { + char c = columnName.charAt(i); + if (!Character.isLetterOrDigit(c) && c != '_') { + return true; + } + } + return false; + } + private void filterContentValues(ContentValues values, Map<String, String> projectionMap) { Iterator<String> iter = values.keySet().iterator(); while (iter.hasNext()) { @@ -1685,6 +1701,18 @@ public class TvProvider extends ContentProvider { private SqlParams createSqlParams(String operation, Uri uri, String selection, String[] selectionArgs) { int match = sUriMatcher.match(uri); + + SqliteTokenFinder.findTokens(selection, p -> { + if (p.first == SqliteTokenFinder.TYPE_REGULAR + && TextUtils.equals(p.second.toUpperCase(Locale.US), "SELECT")) { + // only when a keyword is not in quotes or brackets + // see https://www.sqlite.org/lang_keywords.html + android.util.EventLog.writeEvent(0x534e4554, "135269669", -1, ""); + throw new SecurityException( + "Subquery is not allowed in selection: " + selection); + } + }); + SqlParams params = new SqlParams(null, selection, selectionArgs); // Control access to EPG data (excluding watched programs) when the caller doesn't have all diff --git a/src/com/android/providers/tv/util/SqliteTokenFinder.java b/src/com/android/providers/tv/util/SqliteTokenFinder.java new file mode 100644 index 0000000..bb0bac2 --- /dev/null +++ b/src/com/android/providers/tv/util/SqliteTokenFinder.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.providers.tv.util; + + +import android.annotation.Nullable; + +import android.util.Pair; +import java.util.function.Consumer; + +/** + * Simple SQL parser to check statements for usage of prohibited/sensitive fields. Modified from + * packages/providers/ContactsProvider/src/com/android/providers/contacts/sqlite/SqlChecker.java + */ +public class SqliteTokenFinder { + public static final int TYPE_REGULAR = 0; + public static final int TYPE_IN_SINGLE_QUOTES = 1; + public static final int TYPE_IN_DOUBLE_QUOTES = 2; + public static final int TYPE_IN_BACKQUOTES = 3; + public static final int TYPE_IN_BRACKETS = 4; + + private static boolean isAlpha(char ch) { + return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || (ch == '_'); + } + + private static boolean isNum(char ch) { + return ('0' <= ch && ch <= '9'); + } + + private static boolean isAlNum(char ch) { + return isAlpha(ch) || isNum(ch); + } + + private static boolean isAnyOf(char ch, String set) { + return set.indexOf(ch) >= 0; + } + + private static char peek(String s, int index) { + return index < s.length() ? s.charAt(index) : '\0'; + } + + /** + * SQL Tokenizer specialized to extract tokens from SQL (snippets). + * + * Based on sqlite3GetToken() in tokenzie.c in SQLite. + * + * Source for v3.8.6 (which android uses): http://www.sqlite.org/src/artifact/ae45399d6252b4d7 + * (Latest source as of now: http://www.sqlite.org/src/artifact/78c8085bc7af1922) + * + * Also draft spec: http://www.sqlite.org/draft/tokenreq.html + * + * @param sql the SQL clause to be tokenized. + * @param checker the {@link Consumer} to check each token. The input of the checker is a pair + * of token type and the token. + */ + public static void findTokens(@Nullable String sql, Consumer<Pair<Integer, String>> checker) { + if (sql == null) { + return; + } + int pos = 0; + final int len = sql.length(); + while (pos < len) { + final char ch = peek(sql, pos); + + // Regular token. + if (isAlpha(ch)) { + final int start = pos; + pos++; + while (isAlNum(peek(sql, pos))) { + pos++; + } + final int end = pos; + + final String token = sql.substring(start, end); + checker.accept(Pair.create(TYPE_REGULAR, token)); + + continue; + } + + // Handle quoted tokens + if (isAnyOf(ch, "'\"`")) { + final int quoteStart = pos; + pos++; + + for (;;) { + pos = sql.indexOf(ch, pos); + if (pos < 0) { + throw new IllegalArgumentException("Unterminated quote in" + sql); + } + if (peek(sql, pos + 1) != ch) { + break; + } + // Quoted quote char -- e.g. "abc""def" is a single string. + pos += 2; + } + final int quoteEnd = pos; + pos++; + + // Extract the token + String token = sql.substring(quoteStart + 1, quoteEnd); + // Unquote if needed. i.e. "aa""bb" -> aa"bb + if (token.indexOf(ch) >= 0) { + token = token.replaceAll(String.valueOf(ch) + ch, String.valueOf(ch)); + } + int type = TYPE_REGULAR; + switch (ch) { + case '\'': + type = TYPE_IN_SINGLE_QUOTES; + break; + case '\"': + type = TYPE_IN_DOUBLE_QUOTES; + break; + case '`': + type = TYPE_IN_BACKQUOTES; + break; + } + checker.accept(Pair.create(type, token)); + continue; + } + // Handle tokens enclosed in [...] + if (ch == '[') { + final int quoteStart = pos; + pos++; + + pos = sql.indexOf(']', pos); + if (pos < 0) { + throw new IllegalArgumentException("Unterminated quote in" + sql); + } + final int quoteEnd = pos; + pos++; + + final String token = sql.substring(quoteStart + 1, quoteEnd); + + checker.accept(Pair.create(TYPE_IN_BRACKETS, token)); + continue; + } + + // Detect comments. + if (ch == '-' && peek(sql, pos + 1) == '-') { + pos += 2; + pos = sql.indexOf('\n', pos); + if (pos < 0) { + // strings ending in an inline comment. + break; + } + pos++; + + continue; + } + if (ch == '/' && peek(sql, pos + 1) == '*') { + pos += 2; + pos = sql.indexOf("*/", pos); + if (pos < 0) { + throw new IllegalArgumentException("Unterminated comment in" + sql); + } + pos += 2; + + continue; + } + + // For this purpose, we can simply ignore other characters. + // (Note it doesn't handle the X'' literal properly and reports this X as a token, + // but that should be fine...) + pos++; + } + } +} diff --git a/tests/src/com/android/providers/tv/util/SqliteTokenFinderTest.java b/tests/src/com/android/providers/tv/util/SqliteTokenFinderTest.java new file mode 100644 index 0000000..f228080 --- /dev/null +++ b/tests/src/com/android/providers/tv/util/SqliteTokenFinderTest.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.providers.tv.util; + +import android.annotation.Nullable; +import android.test.AndroidTestCase; +import android.util.Pair; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +/** + * Tests for {@link SqliteTokenFinder}. + * + * Modified from + * packages/providers/ContactsProvider/tests/src/com/android/providers/contacts/sqlite/SqlCheckerTest.java + */ +public class SqliteTokenFinderTest extends AndroidTestCase { + private List<Pair<Integer, String>> getTokens(String sql) { + List<Pair<Integer, String>> tokens = new ArrayList<>(); + SqliteTokenFinder.findTokens(sql, new Consumer<Pair<Integer, String>>() { + @Override + public void accept(Pair<Integer, String> pair) { + tokens.add(pair); + } + }); + return tokens; + } + + private void checkTokens(String sql, Pair<Integer, String>... tokens) { + final List<Pair<Integer, String>> expected = Arrays.asList(tokens); + + assertEquals(expected, getTokens(sql)); + } + + private void checkTokensRegular(String sql, @Nullable String tokens) { + List<Pair<Integer, String>> expected = new ArrayList<>(); + + if (tokens != null) { + for (String token : tokens.split(" ")) { + expected.add(Pair.create(SqliteTokenFinder.TYPE_REGULAR, token)); + } + } + + assertEquals(expected, getTokens(sql)); + } + + private void assertInvalidSql(String sql, String message) { + try { + getTokens(sql); + fail("Didn't throw Exception"); + } catch (Exception e) { + assertTrue("Expected " + e.getMessage() + " to contain " + message, + e.getMessage().contains(message)); + } + } + + public void testWhitespaces() { + checkTokensRegular(" select \t\r\n a\n\n ", "select a"); + checkTokensRegular("a b", "a b"); + } + + public void testComment() { + checkTokensRegular("--\n", null); + checkTokensRegular("a--\n", "a"); + checkTokensRegular("a--abcdef\n", "a"); + checkTokensRegular("a--abcdef\nx", "a x"); + checkTokensRegular("a--\nx", "a x"); + checkTokensRegular("a--abcdef", "a"); + checkTokensRegular("a--abcdef\ndef--", "a def"); + + checkTokensRegular("/**/", null); + assertInvalidSql("/*", "Unterminated comment"); + assertInvalidSql("/*/", "Unterminated comment"); + assertInvalidSql("/*\n* /*a", "Unterminated comment"); + checkTokensRegular("a/**/", "a"); + checkTokensRegular("/**/b", "b"); + checkTokensRegular("a/**/b", "a b"); + checkTokensRegular("a/* -- \n* /* **/b", "a b"); + } + + public void testSingleQuotes() { + assertInvalidSql("'", "Unterminated quote"); + assertInvalidSql("a'", "Unterminated quote"); + assertInvalidSql("a'''", "Unterminated quote"); + assertInvalidSql("a''' ", "Unterminated quote"); + checkTokens("''", Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "")); + + // 2 consecutive quotes inside quotes stands for a quote. e.g.'let''s go' -> let's go + checkTokens( + "''''", + Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "\'")); + checkTokens( + "a''''b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "\'"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens( + "a' '' 'b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, " \' "), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens("'abc'", Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "abc")); + checkTokens("'abc\ndef'", Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "abc\ndef")); + checkTokens( + "a'abc\ndef'", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "abc\ndef")); + checkTokens( + "'abc\ndef'b", + Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "abc\ndef"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens("a'abc\ndef'b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "abc\ndef"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens( + "a'''abc\nd''ef'''b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "\'abc\nd\'ef\'"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + } + + public void testDoubleQuotes() { + assertInvalidSql("\"", "Unterminated quote"); + assertInvalidSql("a\"", "Unterminated quote"); + assertInvalidSql("a\"\"\"", "Unterminated quote"); + assertInvalidSql("a\"\"\" ", "Unterminated quote"); + checkTokens("\"\"", Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "")); + checkTokens("\"\"\"\"", Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "\"")); + checkTokens( + "a\"\"\"\"b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "\""), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens("a\"\t\"\"\t\"b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "\t\"\t"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens("\"abc\"", Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "abc")); + checkTokens( + "\"abc\ndef\"", + Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "abc\ndef")); + checkTokens( + "a\"abc\ndef\"", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "abc\ndef")); + checkTokens( + "\"abc\ndef\"b", + Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "abc\ndef"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens("a\"abc\ndef\"b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "abc\ndef"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens("a\"\"\"abc\nd\"\"ef\"\"\"b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "\"abc\nd\"ef\""), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + } + + public void testBackquotes() { + assertInvalidSql("`", "Unterminated quote"); + assertInvalidSql("a`", "Unterminated quote"); + assertInvalidSql("a```", "Unterminated quote"); + assertInvalidSql("a``` ", "Unterminated quote"); + checkTokens("``", Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "")); + checkTokens("````", Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "`")); + checkTokens( + "a````b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "`"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens( + "a`\t``\t`b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "\t`\t"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens("`abc`", Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "abc")); + checkTokens( + "`abc\ndef`", + Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "abc\ndef")); + checkTokens( + "a`abc\ndef`", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "abc\ndef")); + checkTokens( + "`abc\ndef`b", + Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "abc\ndef"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens( + "a`abc\ndef`b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "abc\ndef"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens( + "a```abc\nd``ef```b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "`abc\nd`ef`"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + } + + public void testBrackets() { + assertInvalidSql("[", "Unterminated quote"); + assertInvalidSql("a[", "Unterminated quote"); + assertInvalidSql("a[ ", "Unterminated quote"); + assertInvalidSql("a[[ ", "Unterminated quote"); + checkTokens("[]", Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "")); + checkTokens("[[]", Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "[")); + checkTokens( + "a[[]b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "["), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens( + "a[\t[\t]b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "\t[\t"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens("[abc]", Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "abc")); + checkTokens( + "[abc\ndef]", + Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "abc\ndef")); + checkTokens( + "a[abc\ndef]", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "abc\ndef")); + checkTokens( + "[abc\ndef]b", + Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "abc\ndef"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens( + "a[abc\ndef]b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "abc\ndef"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + checkTokens( + "a[[abc\nd[ef[]b", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "[abc\nd[ef["), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b")); + } + + public void testTokens() { + checkTokensRegular("a,abc,a00b,_1,_123,abcdef", "a abc a00b _1 _123 abcdef"); + checkTokens( + "a--\nabc/**/a00b''_1'''ABC'''`_123`abc[d]\"e\"f", + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "abc"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a00b"), + Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, ""), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "_1"), + Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "'ABC'"), + Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "_123"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "abc"), + Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "d"), + Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "e"), + Pair.create(SqliteTokenFinder.TYPE_REGULAR, "f")); + } +} |