1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.media.util; 18 19 import static android.content.ContentResolver.QUERY_ARG_SQL_GROUP_BY; 20 import static android.content.ContentResolver.QUERY_ARG_SQL_HAVING; 21 import static android.content.ContentResolver.QUERY_ARG_SQL_LIMIT; 22 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION; 23 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS; 24 import static android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER; 25 26 import static com.android.providers.media.util.DatabaseUtils.bindSelection; 27 28 import android.annotation.NonNull; 29 import android.annotation.Nullable; 30 import android.content.ContentValues; 31 import android.database.Cursor; 32 import android.database.DatabaseUtils; 33 import android.database.sqlite.SQLiteDatabase; 34 import android.os.Build; 35 import android.os.Bundle; 36 import android.os.CancellationSignal; 37 import android.os.OperationCanceledException; 38 import android.provider.BaseColumns; 39 import android.provider.MediaStore.MediaColumns; 40 import android.text.TextUtils; 41 import android.util.ArrayMap; 42 import android.util.Log; 43 44 import androidx.annotation.VisibleForTesting; 45 46 import com.android.providers.media.DatabaseHelper; 47 48 import com.google.common.base.Strings; 49 50 import java.util.ArrayList; 51 import java.util.Arrays; 52 import java.util.Collection; 53 import java.util.Iterator; 54 import java.util.Locale; 55 import java.util.Map; 56 import java.util.Map.Entry; 57 import java.util.Objects; 58 import java.util.Set; 59 import java.util.regex.Matcher; 60 import java.util.regex.Pattern; 61 62 /** 63 * This is a convenience class that helps build SQL queries to be sent to 64 * {@link SQLiteDatabase} objects. 65 */ 66 public class SQLiteQueryBuilder { 67 68 public static final String ROWID_COLUMN = "rowid"; 69 70 private static final String TAG = "SQLiteQueryBuilder"; 71 72 private static final Pattern sAggregationPattern = Pattern.compile( 73 "(?i)(AVG|COUNT|MAX|MIN|SUM|TOTAL|GROUP_CONCAT|UNICODE)\\((.+)\\)"); 74 75 /** 76 * Narrow concessions to support legacy apps that aren't using proper SQL 77 * string substitution; these values come from specific bugs. 78 */ 79 private static final Pattern sPattern154193772 = Pattern.compile( 80 "(?i)%\\.(wmv|wm|wtv|asf|hls|mp4|m4v|mov|mp4v|3g2|3gp|3gp2|3gpp|mj2|qt|external|" 81 + "mov|asf|avi|divx|mpg|mpeg|mkv|webm|mk3d|mks|3gp|mpegts|ts|m2ts|m2t)"); 82 private static final Pattern sPattern156832140 = Pattern.compile( 83 "(?i)%com\\.gopro\\.smarty%"); 84 private static final Pattern sPattern158537159 = Pattern.compile("(?i)localized"); 85 86 private static final Pattern sCustomCollatorPattern = Pattern.compile( 87 "(?i)custom_[a-zA-Z]+"); 88 89 private Map<String, String> mProjectionMap = null; 90 private Collection<Pattern> mProjectionAllowlist = null; 91 92 private String mTables = ""; 93 private StringBuilder mWhereClause = null; // lazily created 94 private boolean mDistinct; 95 96 private static final int STRICT_PARENTHESES = 1 << 0; 97 private static final int STRICT_COLUMNS = 1 << 1; 98 private static final int STRICT_GRAMMAR = 1 << 2; 99 100 private int mStrictFlags; 101 102 private int mTargetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; 103 setTargetSdkVersion(int targetSdkVersion)104 public void setTargetSdkVersion(int targetSdkVersion) { 105 mTargetSdkVersion = targetSdkVersion; 106 } 107 108 /** 109 * Raw SQL clause to obtain the value of {@link MediaColumns#_ID} from custom database function 110 * {@code _GET_ID} for INSERT operation. 111 */ 112 private static final String GET_ID_FOR_INSERT_CLAUSE = "_GET_ID(?)"; 113 114 /** 115 * Raw SQL clause to obtain the value of {@link MediaColumns#_ID} from custom database function 116 * {@code _GET_ID} for UPDATE operation. 117 */ 118 private static final String GET_ID_FOR_UPDATE_CLAUSE = "ifnull(_GET_ID(?), _id)"; 119 SQLiteQueryBuilder()120 public SQLiteQueryBuilder() { 121 mDistinct = false; 122 } 123 124 /** 125 * Constructs a new SQLiteQueryBuilder instance as a copy of existing one. 126 * 127 * @param qb an instance to copy. 128 */ SQLiteQueryBuilder(SQLiteQueryBuilder qb)129 public SQLiteQueryBuilder(SQLiteQueryBuilder qb) { 130 if (qb != null) { 131 if (qb.mProjectionMap != null) { 132 final ArrayMap<String, String> projectionMapCopy = new ArrayMap<>(); 133 projectionMapCopy.putAll(qb.mProjectionMap); 134 this.mProjectionMap = projectionMapCopy; 135 } 136 137 if (qb.mProjectionAllowlist != null) { 138 this.mProjectionAllowlist = new ArrayList<>(qb.mProjectionAllowlist); 139 } 140 141 if (qb.mWhereClause != null) { 142 this.mWhereClause = new StringBuilder(qb.mWhereClause); 143 } 144 145 this.mTables = qb.mTables; 146 this.mDistinct = qb.mDistinct; 147 this.mStrictFlags = qb.mStrictFlags; 148 this.mTargetSdkVersion = qb.mTargetSdkVersion; 149 } 150 } 151 152 /** 153 * Mark the query as {@code DISTINCT}. 154 * 155 * @param distinct if true the query is {@code DISTINCT}, otherwise it isn't 156 */ setDistinct(boolean distinct)157 public void setDistinct(boolean distinct) { 158 mDistinct = distinct; 159 } 160 161 /** 162 * Get if the query is marked as {@code DISTINCT}, as last configured by 163 * {@link #setDistinct(boolean)}. 164 */ isDistinct()165 public boolean isDistinct() { 166 return mDistinct; 167 } 168 169 /** 170 * Returns the list of tables being queried 171 * 172 * @return the list of tables being queried 173 */ getTables()174 public @Nullable String getTables() { 175 return mTables; 176 } 177 178 /** 179 * Sets the list of tables to query. Multiple tables can be specified to perform a join. 180 * For example: 181 * setTables("foo, bar") 182 * setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)") 183 * 184 * @param inTables the list of tables to query on 185 */ setTables(@ullable String inTables)186 public void setTables(@Nullable String inTables) { 187 mTables = inTables; 188 } 189 190 /** 191 * Append a chunk to the {@code WHERE} clause of the query. All chunks appended are surrounded 192 * by parenthesis and {@code AND}ed with the selection passed to {@link #query}. The final 193 * {@code WHERE} clause looks like: 194 * <p> 195 * WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) 196 * 197 * @param inWhere the chunk of text to append to the {@code WHERE} clause. 198 */ appendWhere(@onNull CharSequence inWhere)199 public void appendWhere(@NonNull CharSequence inWhere) { 200 if (mWhereClause == null) { 201 mWhereClause = new StringBuilder(inWhere.length() + 16); 202 } 203 mWhereClause.append(inWhere); 204 } 205 206 /** 207 * Append a chunk to the {@code WHERE} clause of the query. All chunks appended are surrounded 208 * by parenthesis and ANDed with the selection passed to {@link #query}. The final 209 * {@code WHERE} clause looks like: 210 * <p> 211 * WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) 212 * 213 * @param inWhere the chunk of text to append to the {@code WHERE} clause. it will be escaped 214 * to avoid SQL injection attacks 215 */ appendWhereEscapeString(@onNull String inWhere)216 public void appendWhereEscapeString(@NonNull String inWhere) { 217 if (mWhereClause == null) { 218 mWhereClause = new StringBuilder(inWhere.length() + 16); 219 } 220 DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere); 221 } 222 223 /** 224 * Add a standalone chunk to the {@code WHERE} clause of this query. 225 * <p> 226 * This method differs from {@link #appendWhere(CharSequence)} in that it 227 * automatically appends {@code AND} to any existing {@code WHERE} clause 228 * already under construction before appending the given standalone 229 * expression wrapped in parentheses. 230 * 231 * @param inWhere the standalone expression to append to the {@code WHERE} 232 * clause. It will be wrapped in parentheses when it's appended. 233 */ appendWhereStandalone(@onNull CharSequence inWhere)234 public void appendWhereStandalone(@NonNull CharSequence inWhere) { 235 if (mWhereClause == null) { 236 mWhereClause = new StringBuilder(inWhere.length() + 16); 237 } 238 if (mWhereClause.length() > 0) { 239 mWhereClause.append(" AND "); 240 } 241 mWhereClause.append('(').append(inWhere).append(')'); 242 } 243 244 /** 245 * Sets the projection map for the query. The projection map maps 246 * from column names that the caller passes into query to database 247 * column names. This is useful for renaming columns as well as 248 * disambiguating column names when doing joins. For example you 249 * could map "name" to "people.name". If a projection map is set 250 * it must contain all column names the user may request, even if 251 * the key and value are the same. 252 * 253 * @param columnMap maps from the user column names to the database column names 254 */ setProjectionMap(@ullable Map<String, String> columnMap)255 public void setProjectionMap(@Nullable Map<String, String> columnMap) { 256 if (columnMap != null) { 257 mProjectionMap = new ArrayMap<String, String>(); 258 for (Entry<String, String> entry : columnMap.entrySet()) { 259 mProjectionMap.put(entry.getKey().toLowerCase(Locale.ROOT), entry.getValue()); 260 } 261 } else { 262 mProjectionMap = null; 263 } 264 } 265 266 /** Adds {@code column} to the projection map. */ allowColumn(String column)267 public void allowColumn(String column) { 268 if (mProjectionMap == null) { 269 mProjectionMap = new ArrayMap<>(); 270 } 271 mProjectionMap.put(column, column); 272 } 273 274 /** 275 * Gets the projection map for the query, as last configured by 276 * {@link #setProjectionMap(Map)}. 277 */ getProjectionMap()278 public @Nullable Map<String, String> getProjectionMap() { 279 return mProjectionMap; 280 } 281 282 /** 283 * Sets a projection allowlist of columns that will be allowed through, even 284 * when {@link #setStrict(boolean)} is enabled. This provides a way for 285 * abusive custom columns like {@code COUNT(*)} to continue working. 286 */ setProjectionAllowlist(@ullable Collection<Pattern> projectionAllowlist)287 public void setProjectionAllowlist(@Nullable Collection<Pattern> projectionAllowlist) { 288 mProjectionAllowlist = projectionAllowlist; 289 } 290 291 /** 292 * Gets the projection allowlist for the query, as last configured by 293 * {@link #setProjectionAllowlist}. 294 */ getProjectionAllowlist()295 public @Nullable Collection<Pattern> getProjectionAllowlist() { 296 return mProjectionAllowlist; 297 } 298 299 /** 300 * When set, the selection is verified against malicious arguments. When 301 * using this class to create a statement using 302 * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)}, 303 * non-numeric limits will raise an exception. If a projection map is 304 * specified, fields not in that map will be ignored. If this class is used 305 * to execute the statement directly using 306 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)} 307 * or 308 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)}, 309 * additionally also parenthesis escaping selection are caught. To 310 * summarize: To get maximum protection against malicious third party apps 311 * (for example content provider consumers), make sure to do the following: 312 * <ul> 313 * <li>Set this value to true</li> 314 * <li>Use a projection map</li> 315 * <li>Use one of the query overloads instead of getting the statement as a 316 * sql string</li> 317 * </ul> 318 * <p> 319 * This feature is disabled by default on each newly constructed 320 * {@link SQLiteQueryBuilder} and needs to be manually enabled. 321 */ setStrict(boolean strict)322 public void setStrict(boolean strict) { 323 if (strict) { 324 mStrictFlags |= STRICT_PARENTHESES; 325 } else { 326 mStrictFlags &= ~STRICT_PARENTHESES; 327 } 328 } 329 330 /** 331 * Get if the query is marked as strict, as last configured by 332 * {@link #setStrict(boolean)}. 333 */ isStrict()334 public boolean isStrict() { 335 return (mStrictFlags & STRICT_PARENTHESES) != 0; 336 } 337 338 /** 339 * When enabled, verify that all projections and {@link ContentValues} only 340 * contain valid columns as defined by {@link #setProjectionMap(Map)}. 341 * <p> 342 * This enforcement applies to {@link #insert}, {@link #query}, and 343 * {@link #update} operations. Any enforcement failures will throw an 344 * {@link IllegalArgumentException}. 345 * <p> 346 * This feature is disabled by default on each newly constructed 347 * {@link SQLiteQueryBuilder} and needs to be manually enabled. 348 */ setStrictColumns(boolean strictColumns)349 public void setStrictColumns(boolean strictColumns) { 350 if (strictColumns) { 351 mStrictFlags |= STRICT_COLUMNS; 352 } else { 353 mStrictFlags &= ~STRICT_COLUMNS; 354 } 355 } 356 357 /** 358 * Get if the query is marked as strict, as last configured by 359 * {@link #setStrictColumns(boolean)}. 360 */ isStrictColumns()361 public boolean isStrictColumns() { 362 return (mStrictFlags & STRICT_COLUMNS) != 0; 363 } 364 365 /** 366 * When enabled, verify that all untrusted SQL conforms to a restricted SQL 367 * grammar. Here are the restrictions applied: 368 * <ul> 369 * <li>In {@code WHERE} and {@code HAVING} clauses: subqueries, raising, and 370 * windowing terms are rejected. 371 * <li>In {@code GROUP BY} clauses: only valid columns are allowed. 372 * <li>In {@code ORDER BY} clauses: only valid columns, collation, and 373 * ordering terms are allowed. 374 * <li>In {@code LIMIT} clauses: only numerical values and offset terms are 375 * allowed. 376 * </ul> 377 * All column references must be valid as defined by 378 * {@link #setProjectionMap(Map)}. 379 * <p> 380 * This enforcement applies to {@link #query}, {@link #update} and 381 * {@link #delete} operations. This enforcement does not apply to trusted 382 * inputs, such as those provided by {@link #appendWhere}. Any enforcement 383 * failures will throw an {@link IllegalArgumentException}. 384 * <p> 385 * This feature is disabled by default on each newly constructed 386 * {@link SQLiteQueryBuilder} and needs to be manually enabled. 387 */ setStrictGrammar(boolean strictGrammar)388 public void setStrictGrammar(boolean strictGrammar) { 389 if (strictGrammar) { 390 mStrictFlags |= STRICT_GRAMMAR; 391 } else { 392 mStrictFlags &= ~STRICT_GRAMMAR; 393 } 394 } 395 396 /** 397 * Get if the query is marked as strict, as last configured by 398 * {@link #setStrictGrammar(boolean)}. 399 */ isStrictGrammar()400 public boolean isStrictGrammar() { 401 return (mStrictFlags & STRICT_GRAMMAR) != 0; 402 } 403 404 /** 405 * Build an SQL query string from the given clauses. 406 * 407 * @param distinct true if you want each row to be unique, false otherwise. 408 * @param tables The table names to compile the query against. 409 * @param columns A list of which columns to return. Passing null will 410 * return all columns, which is discouraged to prevent reading 411 * data from storage that isn't going to be used. 412 * @param where A filter declaring which rows to return, formatted as an SQL 413 * {@code WHERE} clause (excluding the {@code WHERE} itself). Passing {@code null} will 414 * return all rows for the given URL. 415 * @param groupBy A filter declaring how to group rows, formatted as an SQL 416 * {@code GROUP BY} clause (excluding the {@code GROUP BY} itself). Passing {@code null} 417 * will cause the rows to not be grouped. 418 * @param having A filter declare which row groups to include in the cursor, 419 * if row grouping is being used, formatted as an SQL {@code HAVING} 420 * clause (excluding the {@code HAVING} itself). Passing null will cause 421 * all row groups to be included, and is required when row 422 * grouping is not being used. 423 * @param orderBy How to order the rows, formatted as an SQL {@code ORDER BY} clause 424 * (excluding the {@code ORDER BY} itself). Passing null will use the 425 * default sort order, which may be unordered. 426 * @param limit Limits the number of rows returned by the query, 427 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause. 428 * @return the SQL query string 429 */ buildQueryString( boolean distinct, String tables, String[] columns, String where, String groupBy, String having, String orderBy, String limit)430 public static String buildQueryString( 431 boolean distinct, String tables, String[] columns, String where, 432 String groupBy, String having, String orderBy, String limit) { 433 if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) { 434 throw new IllegalArgumentException( 435 "HAVING clauses are only permitted when using a groupBy clause"); 436 } 437 438 StringBuilder query = new StringBuilder(120); 439 440 query.append("SELECT "); 441 if (distinct) { 442 query.append("DISTINCT "); 443 } 444 if (columns != null && columns.length != 0) { 445 appendColumns(query, columns); 446 } else { 447 query.append("* "); 448 } 449 query.append("FROM "); 450 query.append(tables); 451 appendClause(query, " WHERE ", where); 452 appendClause(query, " GROUP BY ", groupBy); 453 appendClause(query, " HAVING ", having); 454 appendClause(query, " ORDER BY ", orderBy); 455 appendClause(query, " LIMIT ", limit); 456 457 return query.toString(); 458 } 459 appendClause(StringBuilder s, String name, String clause)460 private static void appendClause(StringBuilder s, String name, String clause) { 461 if (!TextUtils.isEmpty(clause)) { 462 s.append(name); 463 s.append(clause); 464 } 465 } 466 467 /** 468 * Add the names that are non-null in columns to s, separating 469 * them with commas. 470 */ appendColumns(StringBuilder s, String[] columns)471 public static void appendColumns(StringBuilder s, String[] columns) { 472 int n = columns.length; 473 474 for (int i = 0; i < n; i++) { 475 String column = columns[i]; 476 477 if (column != null) { 478 if (i > 0) { 479 s.append(", "); 480 } 481 s.append(column); 482 } 483 } 484 s.append(' '); 485 } 486 query(DatabaseHelper helper, String[] projectionIn, Bundle queryArgs, CancellationSignal cancellationSignal)487 public Cursor query(DatabaseHelper helper, String[] projectionIn, Bundle queryArgs, 488 CancellationSignal cancellationSignal) { 489 final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION); 490 final String[] selectionArgs = queryArgs.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); 491 final String groupBy = queryArgs.getString(QUERY_ARG_SQL_GROUP_BY); 492 final String having = queryArgs.getString(QUERY_ARG_SQL_HAVING); 493 final String sortOrder = queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER); 494 final String limit = queryArgs.getString(QUERY_ARG_SQL_LIMIT); 495 return query(helper, projectionIn, selection, selectionArgs, groupBy, having, sortOrder, 496 limit, cancellationSignal); 497 } 498 query(DatabaseHelper helper, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit, CancellationSignal cancellationSignal)499 public Cursor query(DatabaseHelper helper, String[] projectionIn, 500 String selection, String[] selectionArgs, String groupBy, 501 String having, String sortOrder, String limit, CancellationSignal cancellationSignal) { 502 return helper.runWithoutTransaction((db) -> { 503 return query(db, projectionIn, selection, selectionArgs, groupBy, 504 having, sortOrder, limit, cancellationSignal); 505 }); 506 } 507 508 /** 509 * Perform a query by combining all current settings and the 510 * information passed into this method. 511 * 512 * @param db the database to query on 513 * @param projectionIn A list of which columns to return. Passing 514 * null will return all columns, which is discouraged to prevent 515 * reading data from storage that isn't going to be used. 516 * @param selection A filter declaring which rows to return, 517 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE} 518 * itself). Passing null will return all rows for the given URL. 519 * @param selectionArgs You may include ?s in selection, which 520 * will be replaced by the values from selectionArgs, in order 521 * that they appear in the selection. The values will be bound 522 * as Strings. 523 * @param groupBy A filter declaring how to group rows, formatted 524 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} 525 * itself). Passing null will cause the rows to not be grouped. 526 * @param having A filter declare which row groups to include in 527 * the cursor, if row grouping is being used, formatted as an 528 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing 529 * null will cause all row groups to be included, and is 530 * required when row grouping is not being used. 531 * @param sortOrder How to order the rows, formatted as an SQL 532 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null 533 * will use the default sort order, which may be unordered. 534 * @param limit Limits the number of rows returned by the query, 535 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause. 536 * @param cancellationSignal A signal to cancel the operation in progress, or null if none. 537 * If the operation is canceled, then {@link OperationCanceledException} will be thrown 538 * when the query is executed. 539 * @return a cursor over the result set 540 * @see android.content.ContentResolver#query(android.net.Uri, String[], 541 * String, String[], String) 542 */ query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit, CancellationSignal cancellationSignal)543 public Cursor query(SQLiteDatabase db, String[] projectionIn, 544 String selection, String[] selectionArgs, String groupBy, 545 String having, String sortOrder, String limit, CancellationSignal cancellationSignal) { 546 if (mTables == null) { 547 return null; 548 } 549 550 final String sql; 551 final String unwrappedSql = buildQuery( 552 projectionIn, selection, groupBy, having, 553 sortOrder, limit); 554 555 if (isStrictColumns()) { 556 enforceStrictColumns(projectionIn); 557 } 558 if (isStrictGrammar()) { 559 enforceStrictGrammar(selection, groupBy, having, sortOrder, limit); 560 } 561 if (isStrict() && hasUserWhere(selection)) { 562 // Validate the user-supplied selection to detect syntactic anomalies 563 // in the selection string that could indicate a SQL injection attempt. 564 // The idea is to ensure that the selection clause is a valid SQL expression 565 // by compiling it twice: once wrapped in parentheses and once as 566 // originally specified. An attacker cannot create an expression that 567 // would escape the SQL expression while maintaining balanced parentheses 568 // in both the wrapped and original forms. 569 570 // NOTE: The ordering of the below operations is important; we must 571 // execute the wrapped query to ensure the untrusted clause has been 572 // fully isolated. 573 574 // Validate the unwrapped query 575 db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid 576 577 // Execute wrapped query for extra protection 578 final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy, 579 wrap(having), sortOrder, limit); 580 sql = wrappedSql; 581 } else { 582 // Execute unwrapped query 583 sql = unwrappedSql; 584 } 585 586 final String[] sqlArgs = selectionArgs; 587 if (Log.isLoggable(TAG, Log.DEBUG)) { 588 if (Logging.IS_DEBUGGABLE) { 589 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); 590 } else { 591 Log.d(TAG, sql); 592 } 593 } 594 return db.rawQueryWithFactory( 595 null, sql, sqlArgs, 596 SQLiteDatabase.findEditTable(mTables), 597 cancellationSignal); // will throw if query is invalid 598 } 599 insert(@onNull DatabaseHelper helper, @NonNull ContentValues values)600 public long insert(@NonNull DatabaseHelper helper, @NonNull ContentValues values) { 601 // We force wrap in a transaction to ensure that all mutations increment 602 // the generation counter 603 return helper.runWithTransaction((db) -> { 604 return insert(db, values); 605 }); 606 } 607 608 /** 609 * Perform an insert by combining all current settings and the 610 * information passed into this method. 611 * 612 * @param db the database to insert on 613 * @return the row ID of the newly inserted row, or -1 if an error occurred 614 */ 615 public long insert(@NonNull SQLiteDatabase db, @NonNull ContentValues values) { 616 Objects.requireNonNull(mTables, "No tables defined"); 617 Objects.requireNonNull(db, "No database defined"); 618 Objects.requireNonNull(values, "No values defined"); 619 620 if (isStrictColumns()) { 621 enforceStrictColumns(values); 622 } 623 624 final String sql = buildInsert(values); 625 626 final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils 627 .getValues(values); 628 final int valuesLength = rawValues.size(); 629 final Object[] sqlArgs = new Object[valuesLength]; 630 for (int i = 0; i < sqlArgs.length; i++) { 631 sqlArgs[i] = rawValues.valueAt(i); 632 } 633 if (Log.isLoggable(TAG, Log.DEBUG)) { 634 if (Logging.IS_DEBUGGABLE) { 635 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); 636 } else { 637 Log.d(TAG, sql); 638 } 639 } 640 return com.android.providers.media.util.DatabaseUtils.executeInsert(db, sql, sqlArgs); 641 } 642 643 public int update(@NonNull DatabaseHelper helper, @NonNull ContentValues values, 644 @Nullable String selection, @Nullable String[] selectionArgs) { 645 // We force wrap in a transaction to ensure that all mutations increment 646 // the generation counter 647 return helper.runWithTransaction((db) -> { 648 return update(db, values, selection, selectionArgs); 649 }); 650 } 651 652 /** 653 * Perform an update by combining all current settings and the 654 * information passed into this method. 655 * 656 * @param db the database to update on 657 * @param selection A filter declaring which rows to return, 658 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE} 659 * itself). Passing null will return all rows for the given URL. 660 * @param selectionArgs You may include ?s in selection, which 661 * will be replaced by the values from selectionArgs, in order 662 * that they appear in the selection. The values will be bound 663 * as Strings. 664 * @return the number of rows updated 665 */ 666 public int update(@NonNull SQLiteDatabase db, @NonNull ContentValues values, 667 @Nullable String selection, @Nullable String[] selectionArgs) { 668 Objects.requireNonNull(mTables, "No tables defined"); 669 Objects.requireNonNull(db, "No database defined"); 670 Objects.requireNonNull(values, "No values defined"); 671 672 final String sql; 673 final String unwrappedSql = buildUpdate(values, selection); 674 675 if (isStrictColumns()) { 676 enforceStrictColumns(values); 677 } 678 if (isStrictGrammar()) { 679 enforceStrictGrammar(selection, null, null, null, null); 680 } 681 if (isStrict() && hasUserWhere(selection)) { 682 // Validate the user-supplied selection to detect syntactic anomalies 683 // in the selection string that could indicate a SQL injection attempt. 684 // The idea is to ensure that the selection clause is a valid SQL expression 685 // by compiling it twice: once wrapped in parentheses and once as 686 // originally specified. An attacker cannot create an expression that 687 // would escape the SQL expression while maintaining balanced parentheses 688 // in both the wrapped and original forms. 689 690 // NOTE: The ordering of the below operations is important; we must 691 // execute the wrapped query to ensure the untrusted clause has been 692 // fully isolated. 693 694 // Validate the unwrapped query 695 db.validateSql(unwrappedSql, null); // will throw if query is invalid 696 697 // Execute wrapped query for extra protection 698 final String wrappedSql = buildUpdate(values, wrap(selection)); 699 sql = wrappedSql; 700 } else { 701 // Execute unwrapped query 702 sql = unwrappedSql; 703 } 704 705 if (selectionArgs == null) { 706 selectionArgs = new String[0]; 707 } 708 final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils 709 .getValues(values); 710 final int valuesLength = rawValues.size(); 711 final Object[] sqlArgs = new Object[valuesLength + selectionArgs.length]; 712 for (int i = 0; i < sqlArgs.length; i++) { 713 if (i < valuesLength) { 714 sqlArgs[i] = rawValues.valueAt(i); 715 } else { 716 sqlArgs[i] = selectionArgs[i - valuesLength]; 717 } 718 } 719 if (Log.isLoggable(TAG, Log.DEBUG)) { 720 if (Logging.IS_DEBUGGABLE) { 721 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); 722 } else { 723 Log.d(TAG, sql); 724 } 725 } 726 return com.android.providers.media.util.DatabaseUtils.executeUpdateDelete(db, sql, sqlArgs); 727 } 728 729 public int delete(@NonNull DatabaseHelper helper, @Nullable String selection, 730 @Nullable String[] selectionArgs) { 731 // We force wrap in a transaction to ensure that all mutations increment 732 // the generation counter 733 return helper.runWithTransaction((db) -> { 734 return delete(db, selection, selectionArgs); 735 }); 736 } 737 738 /** 739 * Perform a delete by combining all current settings and the 740 * information passed into this method. 741 * 742 * @param db the database to delete on 743 * @param selection A filter declaring which rows to return, 744 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE} 745 * itself). Passing null will return all rows for the given URL. 746 * @param selectionArgs You may include ?s in selection, which 747 * will be replaced by the values from selectionArgs, in order 748 * that they appear in the selection. The values will be bound 749 * as Strings. 750 * @return the number of rows deleted 751 */ 752 public int delete(@NonNull SQLiteDatabase db, @Nullable String selection, 753 @Nullable String[] selectionArgs) { 754 Objects.requireNonNull(mTables, "No tables defined"); 755 Objects.requireNonNull(db, "No database defined"); 756 757 final String sql; 758 final String unwrappedSql = buildDelete(selection); 759 760 if (isStrictGrammar()) { 761 enforceStrictGrammar(selection, null, null, null, null); 762 } 763 if (isStrict() && hasUserWhere(selection)) { 764 // Validate the user-supplied selection to detect syntactic anomalies 765 // in the selection string that could indicate a SQL injection attempt. 766 // The idea is to ensure that the selection clause is a valid SQL expression 767 // by compiling it twice: once wrapped in parentheses and once as 768 // originally specified. An attacker cannot create an expression that 769 // would escape the SQL expression while maintaining balanced parentheses 770 // in both the wrapped and original forms. 771 772 // NOTE: The ordering of the below operations is important; we must 773 // execute the wrapped query to ensure the untrusted clause has been 774 // fully isolated. 775 776 // Validate the unwrapped query 777 db.validateSql(unwrappedSql, null); // will throw if query is invalid 778 779 // Execute wrapped query for extra protection 780 final String wrappedSql = buildDelete(wrap(selection)); 781 sql = wrappedSql; 782 } else { 783 // Execute unwrapped query 784 sql = unwrappedSql; 785 } 786 787 final String[] sqlArgs = selectionArgs; 788 if (Log.isLoggable(TAG, Log.DEBUG)) { 789 if (Logging.IS_DEBUGGABLE) { 790 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); 791 } else { 792 Log.d(TAG, sql); 793 } 794 } 795 return com.android.providers.media.util.DatabaseUtils.executeUpdateDelete(db, sql, sqlArgs); 796 } 797 798 private static boolean hasUserWhere(@Nullable String selection) { 799 return !Strings.isNullOrEmpty(selection); 800 } 801 802 private void enforceStrictColumns(@Nullable String[] projection) { 803 Objects.requireNonNull(mProjectionMap, "No projection map defined"); 804 if (!isStrictColumns()) { 805 return; 806 } 807 808 computeProjection(projection); 809 } 810 811 private void enforceStrictColumns(@NonNull ContentValues values) { 812 Objects.requireNonNull(mProjectionMap, "No projection map defined"); 813 if (!isStrictColumns()) { 814 return; 815 } 816 817 final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils 818 .getValues(values); 819 for (int i = 0; i < rawValues.size(); i++) { 820 final String column = rawValues.keyAt(i); 821 if (!mProjectionMap.containsKey(column)) { 822 throw new IllegalArgumentException("Invalid column " + column); 823 } 824 } 825 } 826 827 @VisibleForTesting 828 void enforceStrictGrammar(@Nullable String selection, @Nullable String groupBy, 829 @Nullable String having, @Nullable String sortOrder, @Nullable String limit) { 830 SQLiteTokenizer.tokenize(selection, SQLiteTokenizer.OPTION_NONE, 831 this::enforceStrictToken); 832 SQLiteTokenizer.tokenize(groupBy, SQLiteTokenizer.OPTION_NONE, 833 this::enforceStrictToken); 834 SQLiteTokenizer.tokenize(having, SQLiteTokenizer.OPTION_NONE, 835 this::enforceStrictToken); 836 SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE, 837 this::enforceStrictToken); 838 SQLiteTokenizer.tokenize(limit, SQLiteTokenizer.OPTION_NONE, 839 this::enforceStrictToken); 840 } 841 842 private void enforceStrictToken(@NonNull String token) { 843 if (TextUtils.isEmpty(token)) return; 844 if (isTableOrColumn(token)) return; 845 if (isCustomCollator(token)) return; 846 if (SQLiteTokenizer.isFunction(token)) return; 847 if (SQLiteTokenizer.isType(token)) return; 848 849 // Carefully block any tokens that are attempting to jump across query 850 // clauses or create subqueries, since they could leak data that should 851 // have been filtered by the trusted where clause 852 boolean isAllowedKeyword = SQLiteTokenizer.isKeyword(token); 853 switch (token.toUpperCase(Locale.ROOT)) { 854 case "SELECT": 855 case "FROM": 856 case "WHERE": 857 case "GROUP": 858 case "HAVING": 859 case "WINDOW": 860 case "VALUES": 861 case "ORDER": 862 case "LIMIT": 863 isAllowedKeyword = false; 864 break; 865 } 866 if (isAllowedKeyword) return; 867 868 if (mTargetSdkVersion < Build.VERSION_CODES.R) { 869 // Narrow concessions to support legacy apps that aren't using 870 // proper SQL string substitution 871 if (sPattern154193772.matcher(token).matches()) return; 872 if (sPattern156832140.matcher(token).matches()) return; 873 if (sPattern158537159.matcher(token).matches()) return; 874 } 875 876 throw new IllegalArgumentException("Invalid token " + token); 877 } 878 879 /** 880 * Construct a {@code SELECT} statement suitable for use in a group of 881 * {@code SELECT} statements that will be joined through {@code UNION} operators 882 * in buildUnionQuery. 883 * 884 * @param projectionIn A list of which columns to return. Passing 885 * null will return all columns, which is discouraged to 886 * prevent reading data from storage that isn't going to be 887 * used. 888 * @param selection A filter declaring which rows to return, 889 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE} 890 * itself). Passing null will return all rows for the given 891 * URL. 892 * @param groupBy A filter declaring how to group rows, formatted 893 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} itself). 894 * Passing null will cause the rows to not be grouped. 895 * @param having A filter declare which row groups to include in 896 * the cursor, if row grouping is being used, formatted as an 897 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing 898 * null will cause all row groups to be included, and is 899 * required when row grouping is not being used. 900 * @param sortOrder How to order the rows, formatted as an SQL 901 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null 902 * will use the default sort order, which may be unordered. 903 * @param limit Limits the number of rows returned by the query, 904 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause. 905 * @return the resulting SQL {@code SELECT} statement 906 */ 907 public String buildQuery( 908 String[] projectionIn, String selection, String groupBy, 909 String having, String sortOrder, String limit) { 910 String[] projection = computeProjection(projectionIn); 911 String where = computeWhere(selection); 912 913 return buildQueryString( 914 mDistinct, mTables, projection, where, 915 groupBy, having, sortOrder, limit); 916 } 917 918 /** {@hide} */ 919 public String buildInsert(ContentValues values) { 920 if (values == null || values.isEmpty()) { 921 throw new IllegalArgumentException("Empty values"); 922 } 923 924 StringBuilder sql = new StringBuilder(120); 925 sql.append("INSERT INTO "); 926 sql.append(SQLiteDatabase.findEditTable(mTables)); 927 sql.append(" ("); 928 929 final boolean hasGeneration = Objects.equals(mTables, "files"); 930 if (hasGeneration) { 931 values.remove(MediaColumns.GENERATION_ADDED); 932 values.remove(MediaColumns.GENERATION_MODIFIED); 933 } 934 935 final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils 936 .getValues(values); 937 for (int i = 0; i < rawValues.size(); i++) { 938 if (i > 0) { 939 sql.append(','); 940 } 941 sql.append(rawValues.keyAt(i)); 942 } 943 if (hasGeneration) { 944 sql.append(','); 945 sql.append(MediaColumns.GENERATION_ADDED); 946 sql.append(','); 947 sql.append(MediaColumns.GENERATION_MODIFIED); 948 } 949 if (shouldAppendRowId(values)) { 950 sql.append(','); 951 sql.append(MediaColumns._ID); 952 } 953 954 sql.append(") VALUES ("); 955 for (int i = 0; i < rawValues.size(); i++) { 956 if (i > 0) { 957 sql.append(','); 958 } 959 sql.append('?'); 960 } 961 if (hasGeneration) { 962 sql.append(','); 963 sql.append('('); 964 sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE); 965 sql.append(')'); 966 sql.append(','); 967 sql.append('('); 968 sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE); 969 sql.append(')'); 970 } 971 if (shouldAppendRowId(values)) { 972 sql.append(','); 973 sql.append(bindSelection(GET_ID_FOR_INSERT_CLAUSE, 974 values.getAsString(MediaColumns.DATA))); 975 } 976 sql.append(")"); 977 return sql.toString(); 978 } 979 980 /** {@hide} */ 981 public String buildUpdate(ContentValues values, String selection) { 982 if (values == null || values.isEmpty()) { 983 throw new IllegalArgumentException("Empty values"); 984 } 985 986 StringBuilder sql = new StringBuilder(120); 987 sql.append("UPDATE "); 988 sql.append(SQLiteDatabase.findEditTable(mTables)); 989 sql.append(" SET "); 990 991 final boolean hasGeneration = Objects.equals(mTables, "files"); 992 if (hasGeneration) { 993 values.remove(MediaColumns.GENERATION_ADDED); 994 values.remove(MediaColumns.GENERATION_MODIFIED); 995 } 996 997 final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils 998 .getValues(values); 999 for (int i = 0; i < rawValues.size(); i++) { 1000 if (i > 0) { 1001 sql.append(','); 1002 } 1003 sql.append(rawValues.keyAt(i)); 1004 sql.append("=?"); 1005 } 1006 if (hasGeneration) { 1007 sql.append(','); 1008 sql.append(MediaColumns.GENERATION_MODIFIED); 1009 sql.append('='); 1010 sql.append('('); 1011 sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE); 1012 sql.append(')'); 1013 } 1014 if (shouldAppendRowId(values)) { 1015 sql.append(','); 1016 sql.append(MediaColumns._ID); 1017 sql.append('='); 1018 sql.append(bindSelection(GET_ID_FOR_UPDATE_CLAUSE, 1019 values.getAsString(MediaColumns.DATA))); 1020 } 1021 1022 final String where = computeWhere(selection); 1023 appendClause(sql, " WHERE ", where); 1024 return sql.toString(); 1025 } 1026 1027 /** {@hide} */ 1028 public String buildDelete(String selection) { 1029 StringBuilder sql = new StringBuilder(120); 1030 sql.append("DELETE FROM "); 1031 sql.append(SQLiteDatabase.findEditTable(mTables)); 1032 1033 final String where = computeWhere(selection); 1034 appendClause(sql, " WHERE ", where); 1035 return sql.toString(); 1036 } 1037 1038 private static @NonNull String maybeWithOperator(@Nullable String operator, 1039 @NonNull String column) { 1040 if (operator != null) { 1041 return operator + "(" + column + ")"; 1042 } else { 1043 return column; 1044 } 1045 } 1046 1047 /** {@hide} */ 1048 public @Nullable String[] computeProjection(@Nullable String[] projectionIn) { 1049 if (projectionIn != null && projectionIn.length > 0) { 1050 String[] projectionOut = new String[projectionIn.length]; 1051 for (int i = 0; i < projectionIn.length; i++) { 1052 projectionOut[i] = computeSingleProjectionOrThrow(projectionIn[i]); 1053 } 1054 return projectionOut; 1055 } else { 1056 return getAllColumnsFromProjectionMap(); 1057 } 1058 } 1059 1060 /** {@hide} */ 1061 public String[] getAllColumnsFromProjectionMap() { 1062 if (mProjectionMap != null) { 1063 Set<Entry<String, String>> entrySet = mProjectionMap.entrySet(); 1064 String[] projection = new String[entrySet.size()]; 1065 Iterator<Entry<String, String>> entryIter = entrySet.iterator(); 1066 int i = 0; 1067 1068 while (entryIter.hasNext()) { 1069 Entry<String, String> entry = entryIter.next(); 1070 1071 // Don't include the _count column when people ask for no projection. 1072 if (entry.getKey().equals(BaseColumns._COUNT)) { 1073 continue; 1074 } 1075 projection[i++] = entry.getValue(); 1076 } 1077 return projection; 1078 } 1079 return null; 1080 } 1081 1082 private @NonNull String computeSingleProjectionOrThrow(@NonNull String userColumn) { 1083 final String column = computeSingleProjection(userColumn); 1084 if (column != null) { 1085 return column; 1086 } else { 1087 if (isStrictColumns()) { 1088 throw new IllegalArgumentException("Invalid column " + userColumn); 1089 } 1090 return userColumn; 1091 } 1092 } 1093 1094 private @Nullable String computeSingleProjection(@NonNull String userColumn) { 1095 // When no mapping provided, anything goes 1096 if (mProjectionMap == null) { 1097 return userColumn; 1098 } 1099 1100 String operator = null; 1101 String column = mProjectionMap.get(userColumn.toLowerCase(Locale.ROOT)); 1102 1103 // When no direct match found, look for aggregation 1104 if (column == null) { 1105 final Matcher matcher = sAggregationPattern.matcher(userColumn); 1106 if (matcher.matches()) { 1107 operator = matcher.group(1); 1108 userColumn = matcher.group(2); 1109 column = mProjectionMap.get(userColumn.toLowerCase(Locale.ROOT)); 1110 } 1111 } 1112 1113 if (column != null) { 1114 return maybeWithOperator(operator, column); 1115 } 1116 1117 if (mStrictFlags == 0 && 1118 (userColumn.contains(" AS ") || userColumn.contains(" as "))) { 1119 /* A column alias already exist */ 1120 return maybeWithOperator(operator, userColumn); 1121 } 1122 1123 // If allowlist is configured, we might be willing to let 1124 // this custom column bypass our strict checks. 1125 if (mProjectionAllowlist != null) { 1126 boolean match = false; 1127 for (Pattern p : mProjectionAllowlist) { 1128 if (p.matcher(userColumn).matches()) { 1129 match = true; 1130 break; 1131 } 1132 } 1133 1134 if (match) { 1135 Log.w(TAG, "Allowing abusive custom column: " + userColumn); 1136 return maybeWithOperator(operator, userColumn); 1137 } 1138 } 1139 1140 return null; 1141 } 1142 1143 private boolean isTableOrColumn(String token) { 1144 if (mTables.equals(token)) return true; 1145 return computeSingleProjection(token) != null; 1146 } 1147 1148 private boolean isCustomCollator(String token) { 1149 return sCustomCollatorPattern.matcher(token).matches(); 1150 } 1151 1152 /** {@hide} */ 1153 public @Nullable String computeWhere(@Nullable String selection) { 1154 final boolean hasInternal = !TextUtils.isEmpty(mWhereClause); 1155 final boolean hasExternal = !TextUtils.isEmpty(selection); 1156 1157 if (hasInternal || hasExternal) { 1158 final StringBuilder where = new StringBuilder(); 1159 if (hasInternal) { 1160 where.append('(').append(mWhereClause).append(')'); 1161 } 1162 if (hasInternal && hasExternal) { 1163 where.append(" AND "); 1164 } 1165 if (hasExternal) { 1166 where.append('(').append(selection).append(')'); 1167 } 1168 return where.toString(); 1169 } else { 1170 return null; 1171 } 1172 } 1173 1174 /** 1175 * Wrap given argument in parenthesis, unless it's {@code null} or 1176 * {@code ()}, in which case return it verbatim. 1177 */ 1178 private @Nullable String wrap(@Nullable String arg) { 1179 if (TextUtils.isEmpty(arg)) { 1180 return arg; 1181 } else { 1182 return "(" + arg + ")"; 1183 } 1184 } 1185 1186 @VisibleForTesting 1187 boolean shouldAppendRowId(ContentValues values) { 1188 // When no projectionMap provided, don't add the row 1189 final boolean hasIdInProjectionMap = mProjectionMap != null && mProjectionMap.containsKey( 1190 MediaColumns._ID) && TextUtils.equals(mProjectionMap.get(MediaColumns._ID), 1191 MediaColumns._ID); 1192 return !values.containsKey(MediaColumns._ID) && values.containsKey(MediaColumns.DATA) 1193 && hasIdInProjectionMap; 1194 } 1195 } 1196