enforce stricter CallLogProvider query

changes:
- phoneNumber is now a selectionArgument
- if the user makes a query request for the CALLS_FILTER case,
  throw a SE if the cursor is empty && SQL is detected

Bug: 224771921
Test: 2 manual,
	manual 1: test app 1 can still make valid call filter query
	manual 2: test app 2 with invalid query crashes b/c of SE

      2 CTS tests,
      test 1: ensures the existing functionality still works
      test 2: ensures a SE is thrown on an invalid query for call filter

Change-Id: Ia445bb59581abb14e247aa8d9f0177e02307cf96
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java
index 14f8666..b267483 100644
--- a/src/com/android/providers/contacts/CallLogProvider.java
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -40,6 +40,7 @@
 import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
+import android.database.sqlite.SQLiteTokenizer;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
@@ -85,6 +86,7 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
@@ -503,10 +505,11 @@
                 List<String> pathSegments = uri.getPathSegments();
                 String phoneNumber = pathSegments.size() >= 2 ? pathSegments.get(2) : null;
                 if (!TextUtils.isEmpty(phoneNumber)) {
-                    qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
-                    qb.appendWhereEscapeString(phoneNumber);
+                    qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ?");
                     qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)"
                             : ", 0, " + mMinMatch + ")");
+                    selectionArgs = copyArrayAndAppendElement(selectionArgs,
+                            "'" + phoneNumber + "'");
                 } else {
                     qb.appendWhere(Calls.NUMBER_PRESENTATION + "!="
                             + Calls.PRESENTATION_ALLOWED);
@@ -528,12 +531,68 @@
         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
         final Cursor c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null,
                 null, sortOrder, limitClause);
+
+        if (match == CALLS_FILTER && selectionArgs.length > 0) {
+            // throw SE if the user is sending requests that try to bypass voicemail permissions
+            examineEmptyCursorCause(c, selectionArgs[selectionArgs.length - 1]);
+        }
+
         if (c != null) {
             c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI);
         }
         return c;
     }
 
+    /**
+     * Helper method for queryInternal that appends an extra argument to the existing selection
+     * arguments array.
+     *
+     * @param oldSelectionArguments the existing selection argument array in queryInternal
+     * @param phoneNumber           the phoneNumber that was passed into queryInternal
+     * @return the new selection argument array with the phoneNumber as the last argument
+     */
+    private String[] copyArrayAndAppendElement(String[] oldSelectionArguments, String phoneNumber) {
+        if (oldSelectionArguments == null) {
+            return new String[]{phoneNumber};
+        }
+        String[] newSelectionArguments = new String[oldSelectionArguments.length + 1];
+        System.arraycopy(oldSelectionArguments, 0, newSelectionArguments, 0,
+                oldSelectionArguments.length);
+        newSelectionArguments[oldSelectionArguments.length] = phoneNumber;
+        return newSelectionArguments;
+    }
+
+    /**
+     * Helper that throws a Security Exception if the Cursor object is empty && the phoneNumber
+     * appears to have SQL.
+     *
+     * @param cursor      returned from the query.
+     * @param phoneNumber string to check for SQL.
+     */
+    private void examineEmptyCursorCause(Cursor cursor, String phoneNumber) {
+        // checks if the cursor is empty
+        if ((cursor == null) || !cursor.moveToFirst()) {
+            try {
+                // tokenize the phoneNumber and run each token through a checker
+                SQLiteTokenizer.tokenize(phoneNumber, SQLiteTokenizer.OPTION_NONE,
+                        this::enforceStrictPhoneNumber);
+            } catch (IllegalArgumentException e) {
+                EventLog.writeEvent(0x534e4554, "224771921", Binder.getCallingUid(),
+                        ("invalid phoneNumber passed to queryInternal"));
+                throw new SecurityException("invalid phoneNumber passed to queryInternal");
+            }
+        }
+    }
+
+    private void enforceStrictPhoneNumber(String token) {
+        boolean isAllowedKeyword = SQLiteTokenizer.isKeyword(token);
+        Set<String> lookupTable = Set.of("UNION", "SELECT", "FROM", "WHERE",
+                "GROUP", "HAVING", "WINDOW", "VALUES", "ORDER", "LIMIT");
+        if (!isAllowedKeyword || lookupTable.contains(token.toUpperCase(Locale.US))) {
+            throw new IllegalArgumentException("Invalid token " + token);
+        }
+    }
+
     private void queryForTesting(Uri uri) {
         if (!uri.getBooleanQueryParameter(PARAM_KEY_QUERY_FOR_TESTING, false)) {
             return;