Loading core/java/android/database/sqlite/SQLiteQueryBuilder.java +309 −42 Original line number Diff line number Diff line Loading @@ -28,14 +28,19 @@ import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; import com.android.internal.util.ArrayUtils; import libcore.util.EmptyArray; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** Loading @@ -45,15 +50,24 @@ import java.util.regex.Pattern; public class SQLiteQueryBuilder { private static final String TAG = "SQLiteQueryBuilder"; private static final Pattern sLimitPattern = Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?"); private Map<String, String> mProjectionMap = null; private static final Pattern sAggregationPattern = Pattern.compile( "(?i)(AVG|COUNT|MAX|MIN|SUM|TOTAL|GROUP_CONCAT)\\((.+)\\)"); private List<Pattern> mProjectionGreylist = null; private String mTables = ""; private StringBuilder mWhereClause = null; // lazily created private boolean mDistinct; private SQLiteDatabase.CursorFactory mFactory; private boolean mStrict; private static final int STRICT_PARENTHESES = 1 << 0; private static final int STRICT_COLUMNS = 1 << 1; private static final int STRICT_GRAMMAR = 1 << 2; private int mStrictFlags; public SQLiteQueryBuilder() { mDistinct = false; Loading Loading @@ -138,6 +152,37 @@ public class SQLiteQueryBuilder mProjectionMap = columnMap; } /** * Gets the projection map for the query, as last configured by * {@link #setProjectionMap(Map)}. * * @hide */ public @Nullable Map<String, String> getProjectionMap() { return mProjectionMap; } /** * Sets a projection greylist of columns that will be allowed through, even * when {@link #setStrict(boolean)} is enabled. This provides a way for * abusive custom columns like {@code COUNT(*)} to continue working. * * @hide */ public void setProjectionGreylist(@Nullable List<Pattern> projectionGreylist) { mProjectionGreylist = projectionGreylist; } /** * Gets the projection greylist for the query, as last configured by * {@link #setProjectionGreylist(List)}. * * @hide */ public @Nullable List<Pattern> getProjectionGreylist() { return mProjectionGreylist; } /** * Sets the cursor factory to be used for the query. You can use * one factory for all queries on a database but it is normally Loading Loading @@ -170,8 +215,90 @@ public class SQLiteQueryBuilder * </ul> * By default, this value is false. */ public void setStrict(boolean flag) { mStrict = flag; public void setStrict(boolean strict) { if (strict) { mStrictFlags |= STRICT_PARENTHESES; } else { mStrictFlags &= ~STRICT_PARENTHESES; } } /** * Get if the query is marked as strict, as last configured by * {@link #setStrict(boolean)}. * * @hide */ public boolean isStrict() { return (mStrictFlags & STRICT_PARENTHESES) != 0; } /** * When enabled, verify that all projections and {@link ContentValues} only * contain valid columns as defined by {@link #setProjectionMap(Map)}. * <p> * This enforcement applies to {@link #insert}, {@link #query}, and * {@link #update} operations. Any enforcement failures will throw an * {@link IllegalArgumentException}. * * @hide */ public void setStrictColumns(boolean strictColumns) { if (strictColumns) { mStrictFlags |= STRICT_COLUMNS; } else { mStrictFlags &= ~STRICT_COLUMNS; } } /** * Get if the query is marked as strict, as last configured by * {@link #setStrictColumns(boolean)}. * * @hide */ public boolean isStrictColumns() { return (mStrictFlags & STRICT_COLUMNS) != 0; } /** * When enabled, verify that all untrusted SQL conforms to a restricted SQL * grammar. Here are the restrictions applied: * <ul> * <li>In {@code WHERE} and {@code HAVING} clauses: subqueries, raising, and * windowing terms are rejected. * <li>In {@code GROUP BY} clauses: only valid columns are allowed. * <li>In {@code ORDER BY} clauses: only valid columns, collation, and * ordering terms are allowed. * <li>In {@code LIMIT} clauses: only numerical values and offset terms are * allowed. * </ul> * All column references must be valid as defined by * {@link #setProjectionMap(Map)}. * <p> * This enforcement applies to {@link #query}, {@link #update} and * {@link #delete} operations. This enforcement does not apply to trusted * inputs, such as those provided by {@link #appendWhere}. Any enforcement * failures will throw an {@link IllegalArgumentException}. * * @hide */ public void setStrictGrammar(boolean strictGrammar) { if (strictGrammar) { mStrictFlags |= STRICT_GRAMMAR; } else { mStrictFlags &= ~STRICT_GRAMMAR; } } /** * Get if the query is marked as strict, as last configured by * {@link #setStrictGrammar(boolean)}. * * @hide */ public boolean isStrictGrammar() { return (mStrictFlags & STRICT_GRAMMAR) != 0; } /** Loading Loading @@ -207,9 +334,6 @@ public class SQLiteQueryBuilder throw new IllegalArgumentException( "HAVING clauses are only permitted when using a groupBy clause"); } if (!TextUtils.isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) { throw new IllegalArgumentException("invalid LIMIT clauses:" + limit); } StringBuilder query = new StringBuilder(120); Loading Loading @@ -383,7 +507,13 @@ public class SQLiteQueryBuilder projectionIn, selection, groupBy, having, sortOrder, limit); if (mStrict && selection != null && selection.length() > 0) { if (isStrictColumns()) { enforceStrictColumns(projectionIn); } if (isStrictGrammar()) { enforceStrictGrammar(selection, groupBy, having, sortOrder, limit); } if (isStrict()) { // Validate the user-supplied selection to detect syntactic anomalies // in the selection string that could indicate a SQL injection attempt. // The idea is to ensure that the selection clause is a valid SQL expression Loading @@ -401,7 +531,7 @@ public class SQLiteQueryBuilder // Execute wrapped query for extra protection final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy, having, sortOrder, limit); wrap(having), sortOrder, limit); sql = wrappedSql; } else { // Execute unwrapped query Loading Loading @@ -446,7 +576,13 @@ public class SQLiteQueryBuilder final String sql; final String unwrappedSql = buildUpdate(values, selection); if (mStrict) { if (isStrictColumns()) { enforceStrictColumns(values); } if (isStrictGrammar()) { enforceStrictGrammar(selection, null, null, null, null); } if (isStrict()) { // Validate the user-supplied selection to detect syntactic anomalies // in the selection string that could indicate a SQL injection attempt. // The idea is to ensure that the selection clause is a valid SQL expression Loading Loading @@ -516,7 +652,10 @@ public class SQLiteQueryBuilder final String sql; final String unwrappedSql = buildDelete(selection); if (mStrict) { if (isStrictGrammar()) { enforceStrictGrammar(selection, null, null, null, null); } if (isStrict()) { // Validate the user-supplied selection to detect syntactic anomalies // in the selection string that could indicate a SQL injection attempt. // The idea is to ensure that the selection clause is a valid SQL expression Loading Loading @@ -551,6 +690,82 @@ public class SQLiteQueryBuilder return db.executeSql(sql, sqlArgs); } private void enforceStrictColumns(@Nullable String[] projection) { Objects.requireNonNull(mProjectionMap, "No projection map defined"); computeProjection(projection); } private void enforceStrictColumns(@NonNull ContentValues values) { Objects.requireNonNull(mProjectionMap, "No projection map defined"); final Set<String> rawValues = values.keySet(); final Iterator<String> rawValuesIt = rawValues.iterator(); while (rawValuesIt.hasNext()) { final String column = rawValuesIt.next(); if (!mProjectionMap.containsKey(column)) { throw new IllegalArgumentException("Invalid column " + column); } } } private void enforceStrictGrammar(@Nullable String selection, @Nullable String groupBy, @Nullable String having, @Nullable String sortOrder, @Nullable String limit) { SQLiteTokenizer.tokenize(selection, SQLiteTokenizer.OPTION_NONE, this::enforceStrictGrammarWhereHaving); SQLiteTokenizer.tokenize(groupBy, SQLiteTokenizer.OPTION_NONE, this::enforceStrictGrammarGroupBy); SQLiteTokenizer.tokenize(having, SQLiteTokenizer.OPTION_NONE, this::enforceStrictGrammarWhereHaving); SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE, this::enforceStrictGrammarOrderBy); SQLiteTokenizer.tokenize(limit, SQLiteTokenizer.OPTION_NONE, this::enforceStrictGrammarLimit); } private void enforceStrictGrammarWhereHaving(@NonNull String token) { if (isTableOrColumn(token)) return; if (SQLiteTokenizer.isFunction(token)) return; if (SQLiteTokenizer.isType(token)) return; // NOTE: we explicitly don't allow SELECT subqueries, since they could // leak data that should have been filtered by the trusted where clause switch (token.toUpperCase(Locale.US)) { case "AND": case "AS": case "BETWEEN": case "BINARY": case "CASE": case "CAST": case "COLLATE": case "DISTINCT": case "ELSE": case "END": case "ESCAPE": case "EXISTS": case "GLOB": case "IN": case "IS": case "ISNULL": case "LIKE": case "MATCH": case "NOCASE": case "NOT": case "NOTNULL": case "NULL": case "OR": case "REGEXP": case "RTRIM": case "THEN": case "WHEN": return; } throw new IllegalArgumentException("Invalid token " + token); } private void enforceStrictGrammarGroupBy(@NonNull String token) { if (isTableOrColumn(token)) return; throw new IllegalArgumentException("Invalid token " + token); } private void enforceStrictGrammarOrderBy(@NonNull String token) { if (isTableOrColumn(token)) return; switch (token.toUpperCase(Locale.US)) { case "COLLATE": case "ASC": case "DESC": case "BINARY": case "RTRIM": case "NOCASE": return; } throw new IllegalArgumentException("Invalid token " + token); } private void enforceStrictGrammarLimit(@NonNull String token) { switch (token.toUpperCase(Locale.US)) { case "OFFSET": return; } throw new IllegalArgumentException("Invalid token " + token); } /** * Construct a SELECT statement suitable for use in a group of * SELECT statements that will be joined through UNION operators Loading Loading @@ -611,7 +826,7 @@ public class SQLiteQueryBuilder StringBuilder sql = new StringBuilder(120); sql.append("UPDATE "); sql.append(mTables); sql.append(SQLiteDatabase.findEditTable(mTables)); sql.append(" SET "); final String[] rawKeys = values.keySet().toArray(EmptyArray.STRING); Loading @@ -632,7 +847,7 @@ public class SQLiteQueryBuilder public String buildDelete(String selection) { StringBuilder sql = new StringBuilder(120); sql.append("DELETE FROM "); sql.append(mTables); sql.append(SQLiteDatabase.findEditTable(mTables)); final String where = computeWhere(selection); appendClause(sql, " WHERE ", where); Loading Loading @@ -763,35 +978,23 @@ public class SQLiteQueryBuilder return query.toString(); } private String[] computeProjection(String[] projectionIn) { if (projectionIn != null && projectionIn.length > 0) { if (mProjectionMap != null) { String[] projection = new String[projectionIn.length]; int length = projectionIn.length; for (int i = 0; i < length; i++) { String userColumn = projectionIn[i]; String column = mProjectionMap.get(userColumn); if (column != null) { projection[i] = column; continue; private static @NonNull String maybeWithOperator(@Nullable String operator, @NonNull String column) { if (operator != null) { return operator + "(" + column + ")"; } else { return column; } if (!mStrict && ( userColumn.contains(" AS ") || userColumn.contains(" as "))) { /* A column alias already exist */ projection[i] = userColumn; continue; } throw new IllegalArgumentException("Invalid column " + projectionIn[i]); } return projection; } else { return projectionIn; /** {@hide} */ public @Nullable String[] computeProjection(@Nullable String[] projectionIn) { if (!ArrayUtils.isEmpty(projectionIn)) { String[] projectionOut = new String[projectionIn.length]; for (int i = 0; i < projectionIn.length; i++) { projectionOut[i] = computeSingleProjectionOrThrow(projectionIn[i]); } return projectionOut; } else if (mProjectionMap != null) { // Return all columns in projection map. Set<Entry<String, String>> entrySet = mProjectionMap.entrySet(); Loading @@ -813,7 +1016,71 @@ public class SQLiteQueryBuilder return null; } private @Nullable String computeWhere(@Nullable String selection) { private @NonNull String computeSingleProjectionOrThrow(@NonNull String userColumn) { final String column = computeSingleProjection(userColumn); if (column != null) { return column; } else { throw new IllegalArgumentException("Invalid column " + userColumn); } } private @Nullable String computeSingleProjection(@NonNull String userColumn) { // When no mapping provided, anything goes if (mProjectionMap == null) { return userColumn; } String operator = null; String column = mProjectionMap.get(userColumn); // When no direct match found, look for aggregation if (column == null) { final Matcher matcher = sAggregationPattern.matcher(userColumn); if (matcher.matches()) { operator = matcher.group(1); userColumn = matcher.group(2); column = mProjectionMap.get(userColumn); } } if (column != null) { return maybeWithOperator(operator, column); } if (mStrictFlags == 0 && (userColumn.contains(" AS ") || userColumn.contains(" as "))) { /* A column alias already exist */ return maybeWithOperator(operator, userColumn); } // If greylist is configured, we might be willing to let // this custom column bypass our strict checks. if (mProjectionGreylist != null) { boolean match = false; for (Pattern p : mProjectionGreylist) { if (p.matcher(userColumn).matches()) { match = true; break; } } if (match) { Log.w(TAG, "Allowing abusive custom column: " + userColumn); return maybeWithOperator(operator, userColumn); } } return null; } private boolean isTableOrColumn(String token) { if (mTables.equals(token)) return true; return computeSingleProjection(token) != null; } /** {@hide} */ public @Nullable String computeWhere(@Nullable String selection) { final boolean hasInternal = !TextUtils.isEmpty(mWhereClause); final boolean hasExternal = !TextUtils.isEmpty(selection); Loading core/java/android/database/sqlite/SQLiteTokenizer.java 0 → 100644 +297 −0 File added.Preview size limit exceeded, changes collapsed. Show changes core/tests/coretests/src/android/database/sqlite/SQLiteTokenizerTest.java 0 → 100644 +169 −0 Original line number Diff line number Diff line /* * 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 android.database.sqlite; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class SQLiteTokenizerTest { private List<String> getTokens(String sql) { return SQLiteTokenizer.tokenize(sql, SQLiteTokenizer.OPTION_NONE); } private void checkTokens(String sql, String spaceSeparatedExpectedTokens) { final List<String> expected = spaceSeparatedExpectedTokens == null ? new ArrayList<>() : Arrays.asList(spaceSeparatedExpectedTokens.split(" +")); assertEquals(expected, getTokens(sql)); } private void assertInvalidSql(String sql, String message) { try { getTokens(sql); fail("Didn't throw InvalidSqlException"); } catch (IllegalArgumentException e) { assertTrue("Expected " + e.getMessage() + " to contain " + message, e.getMessage().contains(message)); } } @Test public void testWhitespaces() { checkTokens(" select \t\r\n a\n\n ", "select a"); checkTokens("a b", "a b"); } @Test public void testComment() { checkTokens("--\n", null); checkTokens("a--\n", "a"); checkTokens("a--abcdef\n", "a"); checkTokens("a--abcdef\nx", "a x"); checkTokens("a--\nx", "a x"); assertInvalidSql("a--abcdef", "Unterminated comment"); assertInvalidSql("a--abcdef\ndef--", "Unterminated comment"); checkTokens("/**/", null); assertInvalidSql("/*", "Unterminated comment"); assertInvalidSql("/*/", "Unterminated comment"); assertInvalidSql("/*\n* /*a", "Unterminated comment"); checkTokens("a/**/", "a"); checkTokens("/**/b", "b"); checkTokens("a/**/b", "a b"); checkTokens("a/* -- \n* /* **/b", "a b"); } @Test public void testStrings() { assertInvalidSql("'", "Unterminated quote"); assertInvalidSql("a'", "Unterminated quote"); assertInvalidSql("a'''", "Unterminated quote"); assertInvalidSql("a''' ", "Unterminated quote"); checkTokens("''", null); checkTokens("''''", null); checkTokens("a''''b", "a b"); checkTokens("a' '' 'b", "a b"); checkTokens("'abc'", null); checkTokens("'abc\ndef'", null); checkTokens("a'abc\ndef'", "a"); checkTokens("'abc\ndef'b", "b"); checkTokens("a'abc\ndef'b", "a b"); checkTokens("a'''abc\nd''ef'''b", "a b"); } @Test public void testDoubleQuotes() { assertInvalidSql("\"", "Unterminated quote"); assertInvalidSql("a\"", "Unterminated quote"); assertInvalidSql("a\"\"\"", "Unterminated quote"); assertInvalidSql("a\"\"\" ", "Unterminated quote"); checkTokens("\"\"", ""); checkTokens("\"\"\"\"", "\""); checkTokens("a\"\"\"\"b", "a \" b"); checkTokens("a\"\t\"\"\t\"b", "a \t\"\t b"); checkTokens("\"abc\"", "abc"); checkTokens("\"abc\ndef\"", "abc\ndef"); checkTokens("a\"abc\ndef\"", "a abc\ndef"); checkTokens("\"abc\ndef\"b", "abc\ndef b"); checkTokens("a\"abc\ndef\"b", "a abc\ndef b"); checkTokens("a\"\"\"abc\nd\"\"ef\"\"\"b", "a \"abc\nd\"ef\" b"); } @Test public void testBackQuotes() { assertInvalidSql("`", "Unterminated quote"); assertInvalidSql("a`", "Unterminated quote"); assertInvalidSql("a```", "Unterminated quote"); assertInvalidSql("a``` ", "Unterminated quote"); checkTokens("``", ""); checkTokens("````", "`"); checkTokens("a````b", "a ` b"); checkTokens("a`\t``\t`b", "a \t`\t b"); checkTokens("`abc`", "abc"); checkTokens("`abc\ndef`", "abc\ndef"); checkTokens("a`abc\ndef`", "a abc\ndef"); checkTokens("`abc\ndef`b", "abc\ndef b"); checkTokens("a`abc\ndef`b", "a abc\ndef b"); checkTokens("a```abc\nd``ef```b", "a `abc\nd`ef` b"); } @Test public void testBrackets() { assertInvalidSql("[", "Unterminated quote"); assertInvalidSql("a[", "Unterminated quote"); assertInvalidSql("a[ ", "Unterminated quote"); assertInvalidSql("a[[ ", "Unterminated quote"); checkTokens("[]", ""); checkTokens("[[]", "["); checkTokens("a[[]b", "a [ b"); checkTokens("a[\t[\t]b", "a \t[\t b"); checkTokens("[abc]", "abc"); checkTokens("[abc\ndef]", "abc\ndef"); checkTokens("a[abc\ndef]", "a abc\ndef"); checkTokens("[abc\ndef]b", "abc\ndef b"); checkTokens("a[abc\ndef]b", "a abc\ndef b"); checkTokens("a[[abc\nd[ef[]b", "a [abc\nd[ef[ b"); } @Test public void testSemicolons() { assertInvalidSql(";", "Semicolon is not allowed"); assertInvalidSql(" ;", "Semicolon is not allowed"); assertInvalidSql("; ", "Semicolon is not allowed"); assertInvalidSql("-;-", "Semicolon is not allowed"); checkTokens("--;\n", null); checkTokens("/*;*/", null); checkTokens("';'", null); checkTokens("[;]", ";"); checkTokens("`;`", ";"); } @Test public void testTokens() { checkTokens("a,abc,a00b,_1,_123,abcdef", "a abc a00b _1 _123 abcdef"); checkTokens("a--\nabc/**/a00b''_1'''ABC'''`_123`abc[d]\"e\"f", "a abc a00b _1 _123 abc d e f"); } } Loading
core/java/android/database/sqlite/SQLiteQueryBuilder.java +309 −42 Original line number Diff line number Diff line Loading @@ -28,14 +28,19 @@ import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; import com.android.internal.util.ArrayUtils; import libcore.util.EmptyArray; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** Loading @@ -45,15 +50,24 @@ import java.util.regex.Pattern; public class SQLiteQueryBuilder { private static final String TAG = "SQLiteQueryBuilder"; private static final Pattern sLimitPattern = Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?"); private Map<String, String> mProjectionMap = null; private static final Pattern sAggregationPattern = Pattern.compile( "(?i)(AVG|COUNT|MAX|MIN|SUM|TOTAL|GROUP_CONCAT)\\((.+)\\)"); private List<Pattern> mProjectionGreylist = null; private String mTables = ""; private StringBuilder mWhereClause = null; // lazily created private boolean mDistinct; private SQLiteDatabase.CursorFactory mFactory; private boolean mStrict; private static final int STRICT_PARENTHESES = 1 << 0; private static final int STRICT_COLUMNS = 1 << 1; private static final int STRICT_GRAMMAR = 1 << 2; private int mStrictFlags; public SQLiteQueryBuilder() { mDistinct = false; Loading Loading @@ -138,6 +152,37 @@ public class SQLiteQueryBuilder mProjectionMap = columnMap; } /** * Gets the projection map for the query, as last configured by * {@link #setProjectionMap(Map)}. * * @hide */ public @Nullable Map<String, String> getProjectionMap() { return mProjectionMap; } /** * Sets a projection greylist of columns that will be allowed through, even * when {@link #setStrict(boolean)} is enabled. This provides a way for * abusive custom columns like {@code COUNT(*)} to continue working. * * @hide */ public void setProjectionGreylist(@Nullable List<Pattern> projectionGreylist) { mProjectionGreylist = projectionGreylist; } /** * Gets the projection greylist for the query, as last configured by * {@link #setProjectionGreylist(List)}. * * @hide */ public @Nullable List<Pattern> getProjectionGreylist() { return mProjectionGreylist; } /** * Sets the cursor factory to be used for the query. You can use * one factory for all queries on a database but it is normally Loading Loading @@ -170,8 +215,90 @@ public class SQLiteQueryBuilder * </ul> * By default, this value is false. */ public void setStrict(boolean flag) { mStrict = flag; public void setStrict(boolean strict) { if (strict) { mStrictFlags |= STRICT_PARENTHESES; } else { mStrictFlags &= ~STRICT_PARENTHESES; } } /** * Get if the query is marked as strict, as last configured by * {@link #setStrict(boolean)}. * * @hide */ public boolean isStrict() { return (mStrictFlags & STRICT_PARENTHESES) != 0; } /** * When enabled, verify that all projections and {@link ContentValues} only * contain valid columns as defined by {@link #setProjectionMap(Map)}. * <p> * This enforcement applies to {@link #insert}, {@link #query}, and * {@link #update} operations. Any enforcement failures will throw an * {@link IllegalArgumentException}. * * @hide */ public void setStrictColumns(boolean strictColumns) { if (strictColumns) { mStrictFlags |= STRICT_COLUMNS; } else { mStrictFlags &= ~STRICT_COLUMNS; } } /** * Get if the query is marked as strict, as last configured by * {@link #setStrictColumns(boolean)}. * * @hide */ public boolean isStrictColumns() { return (mStrictFlags & STRICT_COLUMNS) != 0; } /** * When enabled, verify that all untrusted SQL conforms to a restricted SQL * grammar. Here are the restrictions applied: * <ul> * <li>In {@code WHERE} and {@code HAVING} clauses: subqueries, raising, and * windowing terms are rejected. * <li>In {@code GROUP BY} clauses: only valid columns are allowed. * <li>In {@code ORDER BY} clauses: only valid columns, collation, and * ordering terms are allowed. * <li>In {@code LIMIT} clauses: only numerical values and offset terms are * allowed. * </ul> * All column references must be valid as defined by * {@link #setProjectionMap(Map)}. * <p> * This enforcement applies to {@link #query}, {@link #update} and * {@link #delete} operations. This enforcement does not apply to trusted * inputs, such as those provided by {@link #appendWhere}. Any enforcement * failures will throw an {@link IllegalArgumentException}. * * @hide */ public void setStrictGrammar(boolean strictGrammar) { if (strictGrammar) { mStrictFlags |= STRICT_GRAMMAR; } else { mStrictFlags &= ~STRICT_GRAMMAR; } } /** * Get if the query is marked as strict, as last configured by * {@link #setStrictGrammar(boolean)}. * * @hide */ public boolean isStrictGrammar() { return (mStrictFlags & STRICT_GRAMMAR) != 0; } /** Loading Loading @@ -207,9 +334,6 @@ public class SQLiteQueryBuilder throw new IllegalArgumentException( "HAVING clauses are only permitted when using a groupBy clause"); } if (!TextUtils.isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) { throw new IllegalArgumentException("invalid LIMIT clauses:" + limit); } StringBuilder query = new StringBuilder(120); Loading Loading @@ -383,7 +507,13 @@ public class SQLiteQueryBuilder projectionIn, selection, groupBy, having, sortOrder, limit); if (mStrict && selection != null && selection.length() > 0) { if (isStrictColumns()) { enforceStrictColumns(projectionIn); } if (isStrictGrammar()) { enforceStrictGrammar(selection, groupBy, having, sortOrder, limit); } if (isStrict()) { // Validate the user-supplied selection to detect syntactic anomalies // in the selection string that could indicate a SQL injection attempt. // The idea is to ensure that the selection clause is a valid SQL expression Loading @@ -401,7 +531,7 @@ public class SQLiteQueryBuilder // Execute wrapped query for extra protection final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy, having, sortOrder, limit); wrap(having), sortOrder, limit); sql = wrappedSql; } else { // Execute unwrapped query Loading Loading @@ -446,7 +576,13 @@ public class SQLiteQueryBuilder final String sql; final String unwrappedSql = buildUpdate(values, selection); if (mStrict) { if (isStrictColumns()) { enforceStrictColumns(values); } if (isStrictGrammar()) { enforceStrictGrammar(selection, null, null, null, null); } if (isStrict()) { // Validate the user-supplied selection to detect syntactic anomalies // in the selection string that could indicate a SQL injection attempt. // The idea is to ensure that the selection clause is a valid SQL expression Loading Loading @@ -516,7 +652,10 @@ public class SQLiteQueryBuilder final String sql; final String unwrappedSql = buildDelete(selection); if (mStrict) { if (isStrictGrammar()) { enforceStrictGrammar(selection, null, null, null, null); } if (isStrict()) { // Validate the user-supplied selection to detect syntactic anomalies // in the selection string that could indicate a SQL injection attempt. // The idea is to ensure that the selection clause is a valid SQL expression Loading Loading @@ -551,6 +690,82 @@ public class SQLiteQueryBuilder return db.executeSql(sql, sqlArgs); } private void enforceStrictColumns(@Nullable String[] projection) { Objects.requireNonNull(mProjectionMap, "No projection map defined"); computeProjection(projection); } private void enforceStrictColumns(@NonNull ContentValues values) { Objects.requireNonNull(mProjectionMap, "No projection map defined"); final Set<String> rawValues = values.keySet(); final Iterator<String> rawValuesIt = rawValues.iterator(); while (rawValuesIt.hasNext()) { final String column = rawValuesIt.next(); if (!mProjectionMap.containsKey(column)) { throw new IllegalArgumentException("Invalid column " + column); } } } private void enforceStrictGrammar(@Nullable String selection, @Nullable String groupBy, @Nullable String having, @Nullable String sortOrder, @Nullable String limit) { SQLiteTokenizer.tokenize(selection, SQLiteTokenizer.OPTION_NONE, this::enforceStrictGrammarWhereHaving); SQLiteTokenizer.tokenize(groupBy, SQLiteTokenizer.OPTION_NONE, this::enforceStrictGrammarGroupBy); SQLiteTokenizer.tokenize(having, SQLiteTokenizer.OPTION_NONE, this::enforceStrictGrammarWhereHaving); SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE, this::enforceStrictGrammarOrderBy); SQLiteTokenizer.tokenize(limit, SQLiteTokenizer.OPTION_NONE, this::enforceStrictGrammarLimit); } private void enforceStrictGrammarWhereHaving(@NonNull String token) { if (isTableOrColumn(token)) return; if (SQLiteTokenizer.isFunction(token)) return; if (SQLiteTokenizer.isType(token)) return; // NOTE: we explicitly don't allow SELECT subqueries, since they could // leak data that should have been filtered by the trusted where clause switch (token.toUpperCase(Locale.US)) { case "AND": case "AS": case "BETWEEN": case "BINARY": case "CASE": case "CAST": case "COLLATE": case "DISTINCT": case "ELSE": case "END": case "ESCAPE": case "EXISTS": case "GLOB": case "IN": case "IS": case "ISNULL": case "LIKE": case "MATCH": case "NOCASE": case "NOT": case "NOTNULL": case "NULL": case "OR": case "REGEXP": case "RTRIM": case "THEN": case "WHEN": return; } throw new IllegalArgumentException("Invalid token " + token); } private void enforceStrictGrammarGroupBy(@NonNull String token) { if (isTableOrColumn(token)) return; throw new IllegalArgumentException("Invalid token " + token); } private void enforceStrictGrammarOrderBy(@NonNull String token) { if (isTableOrColumn(token)) return; switch (token.toUpperCase(Locale.US)) { case "COLLATE": case "ASC": case "DESC": case "BINARY": case "RTRIM": case "NOCASE": return; } throw new IllegalArgumentException("Invalid token " + token); } private void enforceStrictGrammarLimit(@NonNull String token) { switch (token.toUpperCase(Locale.US)) { case "OFFSET": return; } throw new IllegalArgumentException("Invalid token " + token); } /** * Construct a SELECT statement suitable for use in a group of * SELECT statements that will be joined through UNION operators Loading Loading @@ -611,7 +826,7 @@ public class SQLiteQueryBuilder StringBuilder sql = new StringBuilder(120); sql.append("UPDATE "); sql.append(mTables); sql.append(SQLiteDatabase.findEditTable(mTables)); sql.append(" SET "); final String[] rawKeys = values.keySet().toArray(EmptyArray.STRING); Loading @@ -632,7 +847,7 @@ public class SQLiteQueryBuilder public String buildDelete(String selection) { StringBuilder sql = new StringBuilder(120); sql.append("DELETE FROM "); sql.append(mTables); sql.append(SQLiteDatabase.findEditTable(mTables)); final String where = computeWhere(selection); appendClause(sql, " WHERE ", where); Loading Loading @@ -763,35 +978,23 @@ public class SQLiteQueryBuilder return query.toString(); } private String[] computeProjection(String[] projectionIn) { if (projectionIn != null && projectionIn.length > 0) { if (mProjectionMap != null) { String[] projection = new String[projectionIn.length]; int length = projectionIn.length; for (int i = 0; i < length; i++) { String userColumn = projectionIn[i]; String column = mProjectionMap.get(userColumn); if (column != null) { projection[i] = column; continue; private static @NonNull String maybeWithOperator(@Nullable String operator, @NonNull String column) { if (operator != null) { return operator + "(" + column + ")"; } else { return column; } if (!mStrict && ( userColumn.contains(" AS ") || userColumn.contains(" as "))) { /* A column alias already exist */ projection[i] = userColumn; continue; } throw new IllegalArgumentException("Invalid column " + projectionIn[i]); } return projection; } else { return projectionIn; /** {@hide} */ public @Nullable String[] computeProjection(@Nullable String[] projectionIn) { if (!ArrayUtils.isEmpty(projectionIn)) { String[] projectionOut = new String[projectionIn.length]; for (int i = 0; i < projectionIn.length; i++) { projectionOut[i] = computeSingleProjectionOrThrow(projectionIn[i]); } return projectionOut; } else if (mProjectionMap != null) { // Return all columns in projection map. Set<Entry<String, String>> entrySet = mProjectionMap.entrySet(); Loading @@ -813,7 +1016,71 @@ public class SQLiteQueryBuilder return null; } private @Nullable String computeWhere(@Nullable String selection) { private @NonNull String computeSingleProjectionOrThrow(@NonNull String userColumn) { final String column = computeSingleProjection(userColumn); if (column != null) { return column; } else { throw new IllegalArgumentException("Invalid column " + userColumn); } } private @Nullable String computeSingleProjection(@NonNull String userColumn) { // When no mapping provided, anything goes if (mProjectionMap == null) { return userColumn; } String operator = null; String column = mProjectionMap.get(userColumn); // When no direct match found, look for aggregation if (column == null) { final Matcher matcher = sAggregationPattern.matcher(userColumn); if (matcher.matches()) { operator = matcher.group(1); userColumn = matcher.group(2); column = mProjectionMap.get(userColumn); } } if (column != null) { return maybeWithOperator(operator, column); } if (mStrictFlags == 0 && (userColumn.contains(" AS ") || userColumn.contains(" as "))) { /* A column alias already exist */ return maybeWithOperator(operator, userColumn); } // If greylist is configured, we might be willing to let // this custom column bypass our strict checks. if (mProjectionGreylist != null) { boolean match = false; for (Pattern p : mProjectionGreylist) { if (p.matcher(userColumn).matches()) { match = true; break; } } if (match) { Log.w(TAG, "Allowing abusive custom column: " + userColumn); return maybeWithOperator(operator, userColumn); } } return null; } private boolean isTableOrColumn(String token) { if (mTables.equals(token)) return true; return computeSingleProjection(token) != null; } /** {@hide} */ public @Nullable String computeWhere(@Nullable String selection) { final boolean hasInternal = !TextUtils.isEmpty(mWhereClause); final boolean hasExternal = !TextUtils.isEmpty(selection); Loading
core/java/android/database/sqlite/SQLiteTokenizer.java 0 → 100644 +297 −0 File added.Preview size limit exceeded, changes collapsed. Show changes
core/tests/coretests/src/android/database/sqlite/SQLiteTokenizerTest.java 0 → 100644 +169 −0 Original line number Diff line number Diff line /* * 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 android.database.sqlite; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class SQLiteTokenizerTest { private List<String> getTokens(String sql) { return SQLiteTokenizer.tokenize(sql, SQLiteTokenizer.OPTION_NONE); } private void checkTokens(String sql, String spaceSeparatedExpectedTokens) { final List<String> expected = spaceSeparatedExpectedTokens == null ? new ArrayList<>() : Arrays.asList(spaceSeparatedExpectedTokens.split(" +")); assertEquals(expected, getTokens(sql)); } private void assertInvalidSql(String sql, String message) { try { getTokens(sql); fail("Didn't throw InvalidSqlException"); } catch (IllegalArgumentException e) { assertTrue("Expected " + e.getMessage() + " to contain " + message, e.getMessage().contains(message)); } } @Test public void testWhitespaces() { checkTokens(" select \t\r\n a\n\n ", "select a"); checkTokens("a b", "a b"); } @Test public void testComment() { checkTokens("--\n", null); checkTokens("a--\n", "a"); checkTokens("a--abcdef\n", "a"); checkTokens("a--abcdef\nx", "a x"); checkTokens("a--\nx", "a x"); assertInvalidSql("a--abcdef", "Unterminated comment"); assertInvalidSql("a--abcdef\ndef--", "Unterminated comment"); checkTokens("/**/", null); assertInvalidSql("/*", "Unterminated comment"); assertInvalidSql("/*/", "Unterminated comment"); assertInvalidSql("/*\n* /*a", "Unterminated comment"); checkTokens("a/**/", "a"); checkTokens("/**/b", "b"); checkTokens("a/**/b", "a b"); checkTokens("a/* -- \n* /* **/b", "a b"); } @Test public void testStrings() { assertInvalidSql("'", "Unterminated quote"); assertInvalidSql("a'", "Unterminated quote"); assertInvalidSql("a'''", "Unterminated quote"); assertInvalidSql("a''' ", "Unterminated quote"); checkTokens("''", null); checkTokens("''''", null); checkTokens("a''''b", "a b"); checkTokens("a' '' 'b", "a b"); checkTokens("'abc'", null); checkTokens("'abc\ndef'", null); checkTokens("a'abc\ndef'", "a"); checkTokens("'abc\ndef'b", "b"); checkTokens("a'abc\ndef'b", "a b"); checkTokens("a'''abc\nd''ef'''b", "a b"); } @Test public void testDoubleQuotes() { assertInvalidSql("\"", "Unterminated quote"); assertInvalidSql("a\"", "Unterminated quote"); assertInvalidSql("a\"\"\"", "Unterminated quote"); assertInvalidSql("a\"\"\" ", "Unterminated quote"); checkTokens("\"\"", ""); checkTokens("\"\"\"\"", "\""); checkTokens("a\"\"\"\"b", "a \" b"); checkTokens("a\"\t\"\"\t\"b", "a \t\"\t b"); checkTokens("\"abc\"", "abc"); checkTokens("\"abc\ndef\"", "abc\ndef"); checkTokens("a\"abc\ndef\"", "a abc\ndef"); checkTokens("\"abc\ndef\"b", "abc\ndef b"); checkTokens("a\"abc\ndef\"b", "a abc\ndef b"); checkTokens("a\"\"\"abc\nd\"\"ef\"\"\"b", "a \"abc\nd\"ef\" b"); } @Test public void testBackQuotes() { assertInvalidSql("`", "Unterminated quote"); assertInvalidSql("a`", "Unterminated quote"); assertInvalidSql("a```", "Unterminated quote"); assertInvalidSql("a``` ", "Unterminated quote"); checkTokens("``", ""); checkTokens("````", "`"); checkTokens("a````b", "a ` b"); checkTokens("a`\t``\t`b", "a \t`\t b"); checkTokens("`abc`", "abc"); checkTokens("`abc\ndef`", "abc\ndef"); checkTokens("a`abc\ndef`", "a abc\ndef"); checkTokens("`abc\ndef`b", "abc\ndef b"); checkTokens("a`abc\ndef`b", "a abc\ndef b"); checkTokens("a```abc\nd``ef```b", "a `abc\nd`ef` b"); } @Test public void testBrackets() { assertInvalidSql("[", "Unterminated quote"); assertInvalidSql("a[", "Unterminated quote"); assertInvalidSql("a[ ", "Unterminated quote"); assertInvalidSql("a[[ ", "Unterminated quote"); checkTokens("[]", ""); checkTokens("[[]", "["); checkTokens("a[[]b", "a [ b"); checkTokens("a[\t[\t]b", "a \t[\t b"); checkTokens("[abc]", "abc"); checkTokens("[abc\ndef]", "abc\ndef"); checkTokens("a[abc\ndef]", "a abc\ndef"); checkTokens("[abc\ndef]b", "abc\ndef b"); checkTokens("a[abc\ndef]b", "a abc\ndef b"); checkTokens("a[[abc\nd[ef[]b", "a [abc\nd[ef[ b"); } @Test public void testSemicolons() { assertInvalidSql(";", "Semicolon is not allowed"); assertInvalidSql(" ;", "Semicolon is not allowed"); assertInvalidSql("; ", "Semicolon is not allowed"); assertInvalidSql("-;-", "Semicolon is not allowed"); checkTokens("--;\n", null); checkTokens("/*;*/", null); checkTokens("';'", null); checkTokens("[;]", ";"); checkTokens("`;`", ";"); } @Test public void testTokens() { checkTokens("a,abc,a00b,_1,_123,abcdef", "a abc a00b _1 _123 abcdef"); checkTokens("a--\nabc/**/a00b''_1'''ABC'''`_123`abc[d]\"e\"f", "a abc a00b _1 _123 abc d e f"); } }