1 /* 2 * Copyright (C) 2019 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.tv.util; 18 19 20 import android.annotation.Nullable; 21 22 import android.util.Pair; 23 import java.util.function.Consumer; 24 25 /** 26 * Simple SQL parser to check statements for usage of prohibited/sensitive fields. Modified from 27 * packages/providers/ContactsProvider/src/com/android/providers/contacts/sqlite/SqlChecker.java 28 */ 29 public class SqliteTokenFinder { 30 public static final int TYPE_REGULAR = 0; 31 public static final int TYPE_IN_SINGLE_QUOTES = 1; 32 public static final int TYPE_IN_DOUBLE_QUOTES = 2; 33 public static final int TYPE_IN_BACKQUOTES = 3; 34 public static final int TYPE_IN_BRACKETS = 4; 35 isAlpha(char ch)36 private static boolean isAlpha(char ch) { 37 return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || (ch == '_'); 38 } 39 isNum(char ch)40 private static boolean isNum(char ch) { 41 return ('0' <= ch && ch <= '9'); 42 } 43 isAlNum(char ch)44 private static boolean isAlNum(char ch) { 45 return isAlpha(ch) || isNum(ch); 46 } 47 isAnyOf(char ch, String set)48 private static boolean isAnyOf(char ch, String set) { 49 return set.indexOf(ch) >= 0; 50 } 51 peek(String s, int index)52 private static char peek(String s, int index) { 53 return index < s.length() ? s.charAt(index) : '\0'; 54 } 55 56 /** 57 * SQL Tokenizer specialized to extract tokens from SQL (snippets). 58 * 59 * Based on sqlite3GetToken() in tokenzie.c in SQLite. 60 * 61 * Source for v3.8.6 (which android uses): http://www.sqlite.org/src/artifact/ae45399d6252b4d7 62 * (Latest source as of now: http://www.sqlite.org/src/artifact/78c8085bc7af1922) 63 * 64 * Also draft spec: http://www.sqlite.org/draft/tokenreq.html 65 * 66 * @param sql the SQL clause to be tokenized. 67 * @param checker the {@link Consumer} to check each token. The input of the checker is a pair 68 * of token type and the token. 69 */ findTokens(@ullable String sql, Consumer<Pair<Integer, String>> checker)70 public static void findTokens(@Nullable String sql, Consumer<Pair<Integer, String>> checker) { 71 if (sql == null) { 72 return; 73 } 74 int pos = 0; 75 final int len = sql.length(); 76 while (pos < len) { 77 final char ch = peek(sql, pos); 78 79 // Regular token. 80 if (isAlpha(ch)) { 81 final int start = pos; 82 pos++; 83 while (isAlNum(peek(sql, pos))) { 84 pos++; 85 } 86 final int end = pos; 87 88 final String token = sql.substring(start, end); 89 checker.accept(Pair.create(TYPE_REGULAR, token)); 90 91 continue; 92 } 93 94 // Handle quoted tokens 95 if (isAnyOf(ch, "'\"`")) { 96 final int quoteStart = pos; 97 pos++; 98 99 for (;;) { 100 pos = sql.indexOf(ch, pos); 101 if (pos < 0) { 102 throw new IllegalArgumentException("Unterminated quote in" + sql); 103 } 104 if (peek(sql, pos + 1) != ch) { 105 break; 106 } 107 // Quoted quote char -- e.g. "abc""def" is a single string. 108 pos += 2; 109 } 110 final int quoteEnd = pos; 111 pos++; 112 113 // Extract the token 114 String token = sql.substring(quoteStart + 1, quoteEnd); 115 // Unquote if needed. i.e. "aa""bb" -> aa"bb 116 if (token.indexOf(ch) >= 0) { 117 token = token.replaceAll(String.valueOf(ch) + ch, String.valueOf(ch)); 118 } 119 int type = TYPE_REGULAR; 120 switch (ch) { 121 case '\'': 122 type = TYPE_IN_SINGLE_QUOTES; 123 break; 124 case '\"': 125 type = TYPE_IN_DOUBLE_QUOTES; 126 break; 127 case '`': 128 type = TYPE_IN_BACKQUOTES; 129 break; 130 } 131 checker.accept(Pair.create(type, token)); 132 continue; 133 } 134 // Handle tokens enclosed in [...] 135 if (ch == '[') { 136 final int quoteStart = pos; 137 pos++; 138 139 pos = sql.indexOf(']', pos); 140 if (pos < 0) { 141 throw new IllegalArgumentException("Unterminated quote in" + sql); 142 } 143 final int quoteEnd = pos; 144 pos++; 145 146 final String token = sql.substring(quoteStart + 1, quoteEnd); 147 148 checker.accept(Pair.create(TYPE_IN_BRACKETS, token)); 149 continue; 150 } 151 152 // Detect comments. 153 if (ch == '-' && peek(sql, pos + 1) == '-') { 154 pos += 2; 155 pos = sql.indexOf('\n', pos); 156 if (pos < 0) { 157 // strings ending in an inline comment. 158 break; 159 } 160 pos++; 161 162 continue; 163 } 164 if (ch == '/' && peek(sql, pos + 1) == '*') { 165 pos += 2; 166 pos = sql.indexOf("*/", pos); 167 if (pos < 0) { 168 throw new IllegalArgumentException("Unterminated comment in" + sql); 169 } 170 pos += 2; 171 172 continue; 173 } 174 175 // For this purpose, we can simply ignore other characters. 176 // (Note it doesn't handle the X'' literal properly and reports this X as a token, 177 // but that should be fine...) 178 pos++; 179 } 180 } 181 } 182