blob: 9be49fecfa0096efde0913eccd96608bc89a4d9e [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.providers.telephony;
import android.annotation.Nullable;
import java.util.function.Consumer;
/**
* Simple SQL parser to check statements for usage of prohibited/sensitive fields. Mostly copied
* from
* packages/providers/ContactsProvider/src/com/android/providers/contacts/sqlite/SqlChecker.java
*/
public class SqlTokenFinder{
private static boolean isAlpha(char ch) {
return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || (ch == '_');
}
private static boolean isNum(char ch) {
return ('0' <= ch && ch <= '9');
}
private static boolean isAlNum(char ch) {
return isAlpha(ch) || isNum(ch);
}
private static boolean isAnyOf(char ch, String set) {
return set.indexOf(ch) >= 0;
}
private static char peek(String s, int index) {
return index < s.length() ? s.charAt(index) : '\0';
}
/**
* SQL Tokenizer specialized to extract tokens from SQL (snippets).
*
* Based on sqlite3GetToken() in tokenzie.c in SQLite.
*
* Source for v3.8.6 (which android uses): http://www.sqlite.org/src/artifact/ae45399d6252b4d7
* (Latest source as of now: http://www.sqlite.org/src/artifact/78c8085bc7af1922)
*
* Also draft spec: http://www.sqlite.org/draft/tokenreq.html
*/
public static void findTokens(@Nullable String sql, Consumer<String> checker) {
if (sql == null) {
return;
}
int pos = 0;
final int len = sql.length();
while (pos < len) {
final char ch = peek(sql, pos);
// Regular token.
if (isAlpha(ch)) {
final int start = pos;
pos++;
while (isAlNum(peek(sql, pos))) {
pos++;
}
final int end = pos;
final String token = sql.substring(start, end);
checker.accept(token);
continue;
}
// Handle quoted tokens
if (isAnyOf(ch, "'\"`")) {
final int quoteStart = pos;
pos++;
for (;;) {
pos = sql.indexOf(ch, pos);
if (pos < 0) {
throw new IllegalArgumentException("Unterminated quote in" + sql);
}
if (peek(sql, pos + 1) != ch) {
break;
}
// Quoted quote char -- e.g. "abc""def" is a single string.
pos += 2;
}
final int quoteEnd = pos;
pos++;
if (ch != '\'') {
// Extract the token
final String tokenUnquoted = sql.substring(quoteStart + 1, quoteEnd);
final String token;
// Unquote if needed. i.e. "aa""bb" -> aa"bb
if (tokenUnquoted.indexOf(ch) >= 0) {
token = tokenUnquoted.replaceAll(
String.valueOf(ch) + ch, String.valueOf(ch));
} else {
token = tokenUnquoted;
}
checker.accept(token);
}
continue;
}
// Handle tokens enclosed in [...]
if (ch == '[') {
final int quoteStart = pos;
pos++;
pos = sql.indexOf(']', pos);
if (pos < 0) {
throw new IllegalArgumentException("Unterminated quote in" + sql);
}
final int quoteEnd = pos;
pos++;
final String token = sql.substring(quoteStart + 1, quoteEnd);
checker.accept(token);
continue;
}
// Detect comments.
if (ch == '-' && peek(sql, pos + 1) == '-') {
pos += 2;
pos = sql.indexOf('\n', pos);
if (pos < 0) {
// We disallow strings ending in an inline comment.
throw new IllegalArgumentException("Unterminated comment in" + sql);
}
pos++;
continue;
}
if (ch == '/' && peek(sql, pos + 1) == '*') {
pos += 2;
pos = sql.indexOf("*/", pos);
if (pos < 0) {
throw new IllegalArgumentException("Unterminated comment in" + sql);
}
pos += 2;
continue;
}
// Semicolon is never allowed.
if (ch == ';') {
throw new IllegalArgumentException("Semicolon is not allowed in " + sql);
}
// For this purpose, we can simply ignore other characters.
// (Note it doesn't handle the X'' literal properly and reports this X as a token,
// but that should be fine...)
pos++;
}
}
}