+
+
+ SQLite is a lightweight database engine commonly used in Android devices to store data. By itself, SQLite does not offer any encryption mechanism by default and stores all data in cleartext, which introduces a risk if sensitive data like credentials, authentication tokens or personal identifiable information (PII) are directly stored in a SQLite database. The information could be accessed by any process or user in rooted devices, or can be disclosed through chained vulnerabilities, like unexpected access to the private storage through exposed components.
+
+
+
+
+
+ Use SQLCipher
or similar libraries to add encryption capabilities to SQLite. Alternatively, encrypt sensitive data using cryptographically secure algorithms before storing it in the database.
+
+
+
+
+
+ In the first example, sensitive user information is stored in cleartext.
+
+
+
+ In the second and third examples, the code encrypts sensitive information before saving it to the database.
+
+
+
+
+
+
+ Android Developers:
+ Work with data more securely
+
+
+ SQLCipher:
+ Android Application Integration
+
+
+
diff --git a/java/ql/src/Security/CWE/CWE-312/CleartextStorageAndroidDatabase.ql b/java/ql/src/Security/CWE/CWE-312/CleartextStorageAndroidDatabase.ql
new file mode 100644
index 000000000000..df394db8b4cc
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-312/CleartextStorageAndroidDatabase.ql
@@ -0,0 +1,23 @@
+/**
+ * @name Cleartext storage of sensitive information using a local database on Android
+ * @description Cleartext Storage of Sensitive Information using
+ * a local database on Android allows access for users with root
+ * privileges or unexpected exposure from chained vulnerabilities.
+ * @kind problem
+ * @problem.severity warning
+ * @precision medium
+ * @id java/android/cleartext-storage-database
+ * @tags security
+ * external/cwe/cwe-312
+ */
+
+import java
+import semmle.code.java.security.CleartextStorageAndroidDatabaseQuery
+
+from SensitiveSource data, LocalDatabaseOpenMethodAccess s, Expr input, Expr store
+where
+ input = s.getAnInput() and
+ store = s.getAStore() and
+ data.flowsTo(input)
+select store, "SQLite database $@ containing $@ is stored $@. Data was added $@.", s, s.toString(),
+ data, "sensitive data", store, "here", input, "here"
diff --git a/java/ql/src/change-notes/2021-08-17-cleartext-storage-database-query.md b/java/ql/src/change-notes/2021-08-17-cleartext-storage-database-query.md
new file mode 100644
index 000000000000..ce71dca1a5fc
--- /dev/null
+++ b/java/ql/src/change-notes/2021-08-17-cleartext-storage-database-query.md
@@ -0,0 +1,4 @@
+---
+category: newQuery
+---
+* A new query "Cleartext storage of sensitive information using a local database on Android" (`java/android/cleartext-storage-database`) has been added. This query finds instances of sensitive data being stored in local databases without encryption, which may expose it to attackers or malicious applications.
diff --git a/java/ql/test/query-tests/security/CWE-312/CleartextStorageAndroidDatabaseTest.expected b/java/ql/test/query-tests/security/CWE-312/CleartextStorageAndroidDatabaseTest.expected
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/java/ql/test/query-tests/security/CWE-312/CleartextStorageAndroidDatabaseTest.java b/java/ql/test/query-tests/security/CWE-312/CleartextStorageAndroidDatabaseTest.java
new file mode 100644
index 000000000000..8dc61543ed65
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-312/CleartextStorageAndroidDatabaseTest.java
@@ -0,0 +1,144 @@
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Base64;
+import android.app.Activity;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+
+public class CleartextStorageAndroidDatabaseTest extends Activity {
+
+ public void testCleartextStorageAndroiDatabaseSafe1(Context ctx, String name, String password) {
+ SQLiteDatabase db = ctx.openOrCreateDatabase("test", Context.MODE_PRIVATE, null);
+ db.execSQL("CREATE TABLE IF NOT EXISTS users(user VARCHAR, password VARCHAR);"); // Safe
+ }
+
+ public void testCleartextStorageAndroiDatabaseSafe2(Context ctx, String name, String password) {
+ SQLiteDatabase db = ctx.openOrCreateDatabase("test", Context.MODE_PRIVATE, null);
+ db.execSQL("DROP TABLE passwords;"); // Safe - no sensitive value being stored
+ }
+
+ public void testCleartextStorageAndroiDatabase0(Context ctx, String name, String password) {
+ SQLiteDatabase db = ctx.openOrCreateDatabase("test", Context.MODE_PRIVATE, null);
+ String query = "INSERT INTO users VALUES ('" + name + "', '" + password + "');";
+ db.execSQL(query); // $ hasCleartextStorageAndroidDatabase
+ }
+
+ public void testCleartextStorageAndroiDatabase1(Context ctx, String name, String password) {
+ SQLiteDatabase db = SQLiteDatabase.openDatabase("", null, 0);
+ String query = "INSERT INTO users VALUES ('" + name + "', '" + password + "');";
+ db.execSQL(query); // $ hasCleartextStorageAndroidDatabase
+ }
+
+ public void testCleartextStorageAndroiDatabase2(String name, String password) {
+ SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase("", null);
+ String query = "INSERT INTO users VALUES (?, ?)";
+ db.execSQL(query, new String[] {name, password}); // $ hasCleartextStorageAndroidDatabase
+ }
+
+ //@formatter:off
+ public void testCleartextStorageAndroiDatabase3(String name, String password) {
+ SQLiteDatabase db = SQLiteDatabase.create(null);
+ String query = "INSERT INTO users VALUES (?, ?)";
+ db.execPerConnectionSQL(query, new String[] {name, password}); // $ hasCleartextStorageAndroidDatabase
+ }
+ //@formatter:on
+
+ public void testCleartextStorageAndroiDatabaseSafe3(String name, String password) {
+ SQLiteDatabase db = SQLiteDatabase.openDatabase("", null, 0);
+ ContentValues cv = new ContentValues();
+ cv.put("username", name);
+ cv.put("password", password); // Safe - ContentValues aren't added to any database
+ }
+
+ public void testCleartextStorageAndroiDatabase4(String name, String password) {
+ SQLiteDatabase db = SQLiteDatabase.openDatabase("", null, 0);
+ ContentValues cv = new ContentValues();
+ cv.put("username", name);
+ cv.put("password", password); // $ hasCleartextStorageAndroidDatabase
+ db.insert("table", null, cv);
+ }
+
+ public void testCleartextStorageAndroiDatabase5(String name, String password) {
+ SQLiteDatabase db = SQLiteDatabase.openDatabase("", null, 0);
+ ContentValues cv = new ContentValues();
+ cv.put("username", name);
+ cv.put("password", password); // $ hasCleartextStorageAndroidDatabase
+ db.insertOrThrow("table", null, cv);
+ }
+
+ public void testCleartextStorageAndroiDatabase6(String name, String password) {
+ SQLiteDatabase db = SQLiteDatabase.openDatabase("", null, 0);
+ ContentValues cv = new ContentValues();
+ cv.put("username", name);
+ cv.put("password", password); // $ hasCleartextStorageAndroidDatabase
+ db.insertWithOnConflict("table", null, cv, 0);
+ }
+
+ public void testCleartextStorageAndroiDatabase7(String name, String password) {
+ SQLiteDatabase db = SQLiteDatabase.openDatabase("", null, 0);
+ ContentValues cv = new ContentValues();
+ cv.put("username", name);
+ cv.put("password", password); // $ hasCleartextStorageAndroidDatabase
+ db.replace("table", null, cv);
+ }
+
+ public void testCleartextStorageAndroiDatabase8(String name, String password) {
+ SQLiteDatabase db = SQLiteDatabase.openDatabase("", null, 0);
+ ContentValues cv = new ContentValues();
+ cv.put("username", name);
+ cv.put("password", password); // $ hasCleartextStorageAndroidDatabase
+ db.replaceOrThrow("table", null, cv);
+ }
+
+ public void testCleartextStorageAndroiDatabase9(String name, String password) {
+ SQLiteDatabase db = SQLiteDatabase.openDatabase("", null, 0);
+ ContentValues cv = new ContentValues();
+ cv.put("username", name);
+ cv.put("password", password); // $ hasCleartextStorageAndroidDatabase
+ db.update("table", cv, "", new String[] {});
+ }
+
+ public void testCleartextStorageAndroiDatabase10(String name, String password) {
+ SQLiteDatabase db = SQLiteDatabase.openDatabase("", null, 0);
+ ContentValues cv = new ContentValues();
+ cv.put("username", name);
+ cv.put("password", password); // $ hasCleartextStorageAndroidDatabase
+ db.updateWithOnConflict("table", cv, "", new String[] {}, 0);
+ }
+
+ public void testCleartextStorageAndroiDatabaseSafe4(SQLiteDatabase db, String name,
+ String password) {
+ String query = "INSERT INTO users VALUES ('" + name + "', '" + password + "');";
+ SQLiteStatement stmt = db.compileStatement(query); // Safe - statement isn't executed
+ }
+
+ public void testCleartextStorageAndroiDatabase11(SQLiteDatabase db, String name,
+ String password) {
+ String query = "INSERT INTO users VALUES ('" + name + "', '" + password + "');";
+ SQLiteStatement stmt = db.compileStatement(query); // $ hasCleartextStorageAndroidDatabase
+ stmt.executeUpdateDelete();
+ }
+
+ public void testCleartextStorageAndroiDatabase12(SQLiteDatabase db, String name,
+ String password) {
+ String query = "INSERT INTO users VALUES ('" + name + "', '" + password + "');";
+ SQLiteStatement stmt = db.compileStatement(query); // $ hasCleartextStorageAndroidDatabase
+ stmt.executeInsert();
+ }
+
+ public void testCleartextStorageAndroiDatabaseSafe5(String name, String password)
+ throws Exception {
+ SQLiteDatabase db = SQLiteDatabase.create(null);
+ String query = "INSERT INTO users VALUES (?, ?)";
+ db.execSQL(query, new String[] {name, encrypt(password)}); // Safe
+ }
+
+ private static String encrypt(String cleartext) throws Exception {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(cleartext.getBytes(StandardCharsets.UTF_8));
+ String encoded = Base64.getEncoder().encodeToString(hash);
+ return encoded;
+ }
+}
diff --git a/java/ql/test/query-tests/security/CWE-312/CleartextStorageAndroidDatabaseTest.ql b/java/ql/test/query-tests/security/CWE-312/CleartextStorageAndroidDatabaseTest.ql
new file mode 100644
index 000000000000..421b3a408c40
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-312/CleartextStorageAndroidDatabaseTest.ql
@@ -0,0 +1,22 @@
+import java
+import semmle.code.java.security.CleartextStorageAndroidDatabaseQuery
+import TestUtilities.InlineExpectationsTest
+
+class CleartextStorageAndroidDatabaseTest extends InlineExpectationsTest {
+ CleartextStorageAndroidDatabaseTest() { this = "CleartextStorageAndroidDatabaseTest" }
+
+ override string getARelevantTag() { result = "hasCleartextStorageAndroidDatabase" }
+
+ override predicate hasActualResult(Location location, string element, string tag, string value) {
+ tag = "hasCleartextStorageAndroidDatabase" and
+ exists(SensitiveSource data, LocalDatabaseOpenMethodAccess s, Expr input, Expr store |
+ input = s.getAnInput() and
+ store = s.getAStore() and
+ data.flowsTo(input)
+ |
+ input.getLocation() = location and
+ element = input.toString() and
+ value = ""
+ )
+ }
+}
diff --git a/java/ql/test/stubs/google-android-9.0.0/android/annotation/IntRange.java b/java/ql/test/stubs/google-android-9.0.0/android/annotation/IntRange.java
new file mode 100644
index 000000000000..fdd1786ea5e8
--- /dev/null
+++ b/java/ql/test/stubs/google-android-9.0.0/android/annotation/IntRange.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2015 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.annotation;
+
+public @interface IntRange {
+ long from() default Long.MIN_VALUE;
+
+ long to() default Long.MAX_VALUE;
+
+}
diff --git a/java/ql/test/stubs/google-android-9.0.0/android/database/DatabaseUtils.java b/java/ql/test/stubs/google-android-9.0.0/android/database/DatabaseUtils.java
new file mode 100644
index 000000000000..0d0414c9fdfe
--- /dev/null
+++ b/java/ql/test/stubs/google-android-9.0.0/android/database/DatabaseUtils.java
@@ -0,0 +1,46 @@
+package android.database;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.ParcelFileDescriptor;
+
+public class DatabaseUtils {
+
+ public static ParcelFileDescriptor blobFileDescriptorForQuery(SQLiteDatabase db, String query,
+ String[] selectionArgs) {
+ return null;
+ }
+
+ public static long longForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ return 0;
+
+ }
+
+ public static String stringForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ return null;
+
+ }
+
+ public static void createDbFromSqlStatements(Context context, String dbName, int dbVersion, String sqlStatements) {
+
+ }
+
+ public static int queryNumEntries(SQLiteDatabase db, String table, String selection) {
+ return 0;
+
+ }
+
+ public static int queryNumEntries(SQLiteDatabase db, String table, String selection, String[] selectionArgs) {
+ return 0;
+
+ }
+
+ public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) {
+ return null;
+ }
+
+ public static String concatenateWhere(String a, String b) {
+ return null;
+ }
+
+}
diff --git a/java/ql/test/stubs/google-android-9.0.0/android/database/SQLException.java b/java/ql/test/stubs/google-android-9.0.0/android/database/SQLException.java
new file mode 100644
index 000000000000..87da8071d5e5
--- /dev/null
+++ b/java/ql/test/stubs/google-android-9.0.0/android/database/SQLException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2006 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;
+
+public class SQLException extends RuntimeException {
+ public SQLException() {
+ }
+
+ public SQLException(String error) {
+ }
+
+ public SQLException(String error, Throwable cause) {
+ }
+
+}
diff --git a/java/ql/test/stubs/google-android-9.0.0/android/database/sqlite/SQLiteException.java b/java/ql/test/stubs/google-android-9.0.0/android/database/sqlite/SQLiteException.java
new file mode 100644
index 000000000000..323251bc00b6
--- /dev/null
+++ b/java/ql/test/stubs/google-android-9.0.0/android/database/sqlite/SQLiteException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2006 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 android.database.SQLException;
+
+public class SQLiteException extends SQLException {
+ public SQLiteException() {
+ }
+
+ public SQLiteException(String error) {
+ }
+
+ public SQLiteException(String error, Throwable cause) {
+ }
+
+}
diff --git a/java/ql/test/stubs/google-android-9.0.0/android/database/sqlite/SQLiteQueryBuilder.java b/java/ql/test/stubs/google-android-9.0.0/android/database/sqlite/SQLiteQueryBuilder.java
new file mode 100644
index 000000000000..01b8942c6d72
--- /dev/null
+++ b/java/ql/test/stubs/google-android-9.0.0/android/database/sqlite/SQLiteQueryBuilder.java
@@ -0,0 +1,57 @@
+package android.database.sqlite;
+
+import java.util.Map;
+import java.util.Set;
+
+import android.content.ContentValues;
+import android.os.CancellationSignal;
+
+public abstract class SQLiteQueryBuilder {
+ public abstract void delete(SQLiteDatabase db, String selection, String[] selectionArgs);
+
+ public abstract void insert(SQLiteDatabase db, ContentValues values);
+
+ public abstract void query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs,
+ String groupBy, String having, String sortOrder);
+
+ public abstract void query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs,
+ String groupBy, String having, String sortOrder, String limit);
+
+ public abstract void query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs,
+ String groupBy, String having, String sortOrder, String limit, CancellationSignal cancellationSignal);
+
+ public abstract void update(SQLiteDatabase db, ContentValues values, String selection, String[] selectionArgs);
+
+ public static String buildQueryString(boolean distinct, String tables, String[] columns, String where,
+ String groupBy, String having, String orderBy, String limit) {
+ return null;
+ }
+
+ public abstract String buildQuery(String[] projectionIn, String selection, String groupBy, String having, String sortOrder,
+ String limit);
+
+ public abstract String buildQuery(String[] projectionIn, String selection, String[] selectionArgs, String groupBy,
+ String having, String sortOrder, String limit);
+
+ public abstract String buildUnionQuery(String[] subQueries, String sortOrder, String limit);
+
+ public abstract String buildUnionSubQuery(String typeDiscriminatorColumn, String[] unionColumns,
+ Set