diff --git a/java/ql/lib/semmle/code/java/dataflow/ExternalFlow.qll b/java/ql/lib/semmle/code/java/dataflow/ExternalFlow.qll index 839d1fa69e28..a95b27fca30c 100644 --- a/java/ql/lib/semmle/code/java/dataflow/ExternalFlow.qll +++ b/java/ql/lib/semmle/code/java/dataflow/ExternalFlow.qll @@ -78,10 +78,12 @@ private import FlowSummary private module Frameworks { private import internal.ContainerFlow private import semmle.code.java.frameworks.android.Android + private import semmle.code.java.frameworks.android.ContentProviders private import semmle.code.java.frameworks.android.Intent private import semmle.code.java.frameworks.android.Notifications private import semmle.code.java.frameworks.android.Slice private import semmle.code.java.frameworks.android.SQLite + private import semmle.code.java.frameworks.android.Widget private import semmle.code.java.frameworks.android.XssSinks private import semmle.code.java.frameworks.ApacheHttp private import semmle.code.java.frameworks.apache.Collections diff --git a/java/ql/lib/semmle/code/java/frameworks/Thrift.qll b/java/ql/lib/semmle/code/java/frameworks/Thrift.qll index bf826cfe47ea..a441c6f7e56d 100644 --- a/java/ql/lib/semmle/code/java/frameworks/Thrift.qll +++ b/java/ql/lib/semmle/code/java/frameworks/Thrift.qll @@ -25,6 +25,7 @@ class ThriftIface extends Interface { this.getFile() instanceof ThriftGeneratedFile } + /** Gets an implementation of a method of this interface. */ Method getAnImplementingMethod() { result.getDeclaringType().(Class).getASupertype+() = this and result.overrides+(this.getAMethod()) and diff --git a/java/ql/lib/semmle/code/java/frameworks/android/Android.qll b/java/ql/lib/semmle/code/java/frameworks/android/Android.qll index 41ad2d14968c..b1a0d49e96f4 100644 --- a/java/ql/lib/semmle/code/java/frameworks/android/Android.qll +++ b/java/ql/lib/semmle/code/java/frameworks/android/Android.qll @@ -177,42 +177,6 @@ private class UriModel extends SummaryModelCsv { } } -private class ContentProviderSourceModels extends SourceModelCsv { - override predicate row(string row) { - row = - [ - // ContentInterface models are here for backwards compatibility (it was removed in API 28) - "android.content;ContentInterface;true;call;(String,String,String,Bundle);;Parameter[0..3];contentprovider", - "android.content;ContentProvider;true;call;(String,String,String,Bundle);;Parameter[0..3];contentprovider", - "android.content;ContentProvider;true;call;(String,String,Bundle);;Parameter[0..2];contentprovider", - "android.content;ContentProvider;true;delete;(Uri,String,String[]);;Parameter[0..2];contentprovider", - "android.content;ContentInterface;true;delete;(Uri,Bundle);;Parameter[0..1];contentprovider", - "android.content;ContentProvider;true;delete;(Uri,Bundle);;Parameter[0..1];contentprovider", - "android.content;ContentInterface;true;getType;(Uri);;Parameter[0];contentprovider", - "android.content;ContentProvider;true;getType;(Uri);;Parameter[0];contentprovider", - "android.content;ContentInterface;true;insert;(Uri,ContentValues,Bundle);;Parameter[0];contentprovider", - "android.content;ContentProvider;true;insert;(Uri,ContentValues,Bundle);;Parameter[0..2];contentprovider", - "android.content;ContentProvider;true;insert;(Uri,ContentValues);;Parameter[0..1];contentprovider", - "android.content;ContentInterface;true;openAssetFile;(Uri,String,CancellationSignal);;Parameter[0];contentprovider", - "android.content;ContentProvider;true;openAssetFile;(Uri,String,CancellationSignal);;Parameter[0];contentprovider", - "android.content;ContentProvider;true;openAssetFile;(Uri,String);;Parameter[0];contentprovider", - "android.content;ContentInterface;true;openTypedAssetFile;(Uri,String,Bundle,CancellationSignal);;Parameter[0..2];contentprovider", - "android.content;ContentProvider;true;openTypedAssetFile;(Uri,String,Bundle,CancellationSignal);;Parameter[0..2];contentprovider", - "android.content;ContentProvider;true;openTypedAssetFile;(Uri,String,Bundle);;Parameter[0..2];contentprovider", - "android.content;ContentInterface;true;openFile;(Uri,String,CancellationSignal);;Parameter[0];contentprovider", - "android.content;ContentProvider;true;openFile;(Uri,String,CancellationSignal);;Parameter[0];contentprovider", - "android.content;ContentProvider;true;openFile;(Uri,String);;Parameter[0];contentprovider", - "android.content;ContentInterface;true;query;(Uri,String[],Bundle,CancellationSignal);;Parameter[0..2];contentprovider", - "android.content;ContentProvider;true;query;(Uri,String[],Bundle,CancellationSignal);;Parameter[0..2];contentprovider", - "android.content;ContentProvider;true;query;(Uri,String[],String,String[],String);;Parameter[0..4];contentprovider", - "android.content;ContentProvider;true;query;(Uri,String[],String,String[],String,CancellationSignal);;Parameter[0..4];contentprovider", - "android.content;ContentInterface;true;update;(Uri,ContentValues,Bundle);;Parameter[0..2];contentprovider", - "android.content;ContentProvider;true;update;(Uri,ContentValues,Bundle);;Parameter[0..2];contentprovider", - "android.content;ContentProvider;true;update;(Uri,ContentValues,String,String[]);;Parameter[0..3];contentprovider" - ] - } -} - /** Interface for classes whose instances can be written to and restored from a Parcel. */ class TypeParcelable extends Interface { TypeParcelable() { this.hasQualifiedName("android.os", "Parcelable") } diff --git a/java/ql/lib/semmle/code/java/frameworks/android/ContentProviders.qll b/java/ql/lib/semmle/code/java/frameworks/android/ContentProviders.qll new file mode 100644 index 000000000000..42c22fcf8713 --- /dev/null +++ b/java/ql/lib/semmle/code/java/frameworks/android/ContentProviders.qll @@ -0,0 +1,59 @@ +/** + * Provides classes and predicates for working with Content Providers. + */ + +import java +import semmle.code.java.dataflow.ExternalFlow + +/** The class `android.content.ContentValues`. */ +class ContentValues extends Class { + ContentValues() { this.hasQualifiedName("android.content", "ContentValues") } +} + +private class ContentProviderSourceModels extends SourceModelCsv { + override predicate row(string row) { + row = + [ + // ContentInterface models are here for backwards compatibility (it was removed in API 28) + "android.content;ContentInterface;true;call;(String,String,String,Bundle);;Parameter[0..3];contentprovider", + "android.content;ContentProvider;true;call;(String,String,String,Bundle);;Parameter[0..3];contentprovider", + "android.content;ContentProvider;true;call;(String,String,Bundle);;Parameter[0..2];contentprovider", + "android.content;ContentProvider;true;delete;(Uri,String,String[]);;Parameter[0..2];contentprovider", + "android.content;ContentInterface;true;delete;(Uri,Bundle);;Parameter[0..1];contentprovider", + "android.content;ContentProvider;true;delete;(Uri,Bundle);;Parameter[0..1];contentprovider", + "android.content;ContentInterface;true;getType;(Uri);;Parameter[0];contentprovider", + "android.content;ContentProvider;true;getType;(Uri);;Parameter[0];contentprovider", + "android.content;ContentInterface;true;insert;(Uri,ContentValues,Bundle);;Parameter[0];contentprovider", + "android.content;ContentProvider;true;insert;(Uri,ContentValues,Bundle);;Parameter[0..2];contentprovider", + "android.content;ContentProvider;true;insert;(Uri,ContentValues);;Parameter[0..1];contentprovider", + "android.content;ContentInterface;true;openAssetFile;(Uri,String,CancellationSignal);;Parameter[0];contentprovider", + "android.content;ContentProvider;true;openAssetFile;(Uri,String,CancellationSignal);;Parameter[0];contentprovider", + "android.content;ContentProvider;true;openAssetFile;(Uri,String);;Parameter[0];contentprovider", + "android.content;ContentInterface;true;openTypedAssetFile;(Uri,String,Bundle,CancellationSignal);;Parameter[0..2];contentprovider", + "android.content;ContentProvider;true;openTypedAssetFile;(Uri,String,Bundle,CancellationSignal);;Parameter[0..2];contentprovider", + "android.content;ContentProvider;true;openTypedAssetFile;(Uri,String,Bundle);;Parameter[0..2];contentprovider", + "android.content;ContentInterface;true;openFile;(Uri,String,CancellationSignal);;Parameter[0];contentprovider", + "android.content;ContentProvider;true;openFile;(Uri,String,CancellationSignal);;Parameter[0];contentprovider", + "android.content;ContentProvider;true;openFile;(Uri,String);;Parameter[0];contentprovider", + "android.content;ContentInterface;true;query;(Uri,String[],Bundle,CancellationSignal);;Parameter[0..2];contentprovider", + "android.content;ContentProvider;true;query;(Uri,String[],Bundle,CancellationSignal);;Parameter[0..2];contentprovider", + "android.content;ContentProvider;true;query;(Uri,String[],String,String[],String);;Parameter[0..4];contentprovider", + "android.content;ContentProvider;true;query;(Uri,String[],String,String[],String,CancellationSignal);;Parameter[0..4];contentprovider", + "android.content;ContentInterface;true;update;(Uri,ContentValues,Bundle);;Parameter[0..2];contentprovider", + "android.content;ContentProvider;true;update;(Uri,ContentValues,Bundle);;Parameter[0..2];contentprovider", + "android.content;ContentProvider;true;update;(Uri,ContentValues,String,String[]);;Parameter[0..3];contentprovider" + ] + } +} + +private class SummaryModels extends SummaryModelCsv { + override predicate row(string row) { + row = + [ + "android.content;ContentValues;false;put;;;Argument[0];MapKey of Argument[-1];value", + "android.content;ContentValues;false;put;;;Argument[1];MapValue of Argument[-1];value", + "android.content;ContentValues;false;putAll;;;MapKey of Argument[0];MapKey of Argument[-1];value", + "android.content;ContentValues;false;putAll;;;MapValue of Argument[0];MapValue of Argument[-1];value" + ] + } +} diff --git a/java/ql/lib/semmle/code/java/frameworks/android/SQLite.qll b/java/ql/lib/semmle/code/java/frameworks/android/SQLite.qll index 1d189cca5699..684df41ac56e 100644 --- a/java/ql/lib/semmle/code/java/frameworks/android/SQLite.qll +++ b/java/ql/lib/semmle/code/java/frameworks/android/SQLite.qll @@ -1,3 +1,5 @@ +/** Provides classes and predicates for working with SQLite databases. */ + import java import Android import semmle.code.java.dataflow.FlowSteps @@ -24,6 +26,20 @@ class TypeDatabaseUtils extends Class { TypeDatabaseUtils() { hasQualifiedName("android.database", "DatabaseUtils") } } +/** + * The class `android.database.sqlite.SQLiteOpenHelper`. + */ +class TypeSQLiteOpenHelper extends Class { + TypeSQLiteOpenHelper() { this.hasQualifiedName("android.database.sqlite", "SQLiteOpenHelper") } +} + +/** + * The class `android.database.sqlite.SQLiteStatement`. + */ +class TypeSQLiteStatement extends Class { + TypeSQLiteStatement() { this.hasQualifiedName("android.database.sqlite", "SQLiteStatement") } +} + private class SQLiteSinkCsv extends SinkModelCsv { override predicate row(string row) { row = diff --git a/java/ql/lib/semmle/code/java/frameworks/android/Widget.qll b/java/ql/lib/semmle/code/java/frameworks/android/Widget.qll new file mode 100644 index 000000000000..c5eda29547bc --- /dev/null +++ b/java/ql/lib/semmle/code/java/frameworks/android/Widget.qll @@ -0,0 +1,23 @@ +/** Provides classes and predicates for working with Android widgets. */ + +import java +import semmle.code.java.dataflow.ExternalFlow +import semmle.code.java.dataflow.FlowSources + +private class AndroidWidgetSourceModels extends SourceModelCsv { + override predicate row(string row) { + row = "android.widget;EditText;true;getText;;;ReturnValue;android-widget" + } +} + +private class DefaultAndroidWidgetSources extends RemoteFlowSource { + DefaultAndroidWidgetSources() { sourceNode(this, "android-widget") } + + override string getSourceType() { result = "Android widget source" } +} + +private class AndroidWidgetSummaryModels extends SummaryModelCsv { + override predicate row(string row) { + row = "android.widget;EditText;true;getText;;;Argument[-1];ReturnValue;taint" + } +} diff --git a/java/ql/lib/semmle/code/java/frameworks/struts/StrutsAnnotations.qll b/java/ql/lib/semmle/code/java/frameworks/struts/StrutsAnnotations.qll index 5ee8f25724e0..27ada4d6ff03 100644 --- a/java/ql/lib/semmle/code/java/frameworks/struts/StrutsAnnotations.qll +++ b/java/ql/lib/semmle/code/java/frameworks/struts/StrutsAnnotations.qll @@ -15,6 +15,7 @@ class StrutsAnnotation extends Annotation { class StrutsActionAnnotation extends StrutsAnnotation { StrutsActionAnnotation() { this.getType().hasName("Action") } + /** Gets a callable annotated with this annotation. */ Callable getActionCallable() { result = this.getAnnotatedElement() or diff --git a/java/ql/lib/semmle/code/java/security/CleartextStorageAndroidDatabaseQuery.qll b/java/ql/lib/semmle/code/java/security/CleartextStorageAndroidDatabaseQuery.qll new file mode 100644 index 000000000000..0a1f306677f9 --- /dev/null +++ b/java/ql/lib/semmle/code/java/security/CleartextStorageAndroidDatabaseQuery.qll @@ -0,0 +1,131 @@ +/** Provides classes and predicates to reason about cleartext storage in Android databases. */ + +import java +import semmle.code.java.dataflow.DataFlow +import semmle.code.java.frameworks.android.ContentProviders +import semmle.code.java.frameworks.android.Intent +import semmle.code.java.frameworks.android.SQLite +import semmle.code.java.security.CleartextStorageQuery + +private class LocalDatabaseCleartextStorageSink extends CleartextStorageSink { + LocalDatabaseCleartextStorageSink() { localDatabaseInput(_, this.asExpr()) } +} + +private class LocalDatabaseCleartextStorageStep extends CleartextStorageAdditionalTaintStep { + override predicate step(DataFlow::Node n1, DataFlow::Node n2) { + // EditText.getText() return type is parsed as `Object`, so we need to + // add a taint step for `Object.toString` to model `editText.getText().toString()` + exists(MethodAccess ma, Method m | + ma.getMethod() = m and + m.getDeclaringType() instanceof TypeObject and + m.hasName("toString") + | + n1.asExpr() = ma.getQualifier() and n2.asExpr() = ma + ) + } +} + +/** The creation of an object that can be used to store data in a local database. */ +class LocalDatabaseOpenMethodAccess extends Storable, Call { + LocalDatabaseOpenMethodAccess() { + exists(Method m | this.(MethodAccess).getMethod() = m | + m.getDeclaringType().getASupertype*() instanceof TypeSQLiteOpenHelper and + m.hasName("getWritableDatabase") + or + m.getDeclaringType() instanceof TypeSQLiteDatabase and + m.hasName(["create", "openDatabase", "openOrCreateDatabase", "compileStatement"]) + or + m.getDeclaringType().getASupertype*() instanceof TypeContext and + m.hasName("openOrCreateDatabase") + ) + or + this.(ClassInstanceExpr).getConstructedType() instanceof ContentValues + } + + override Expr getAnInput() { + exists(LocalDatabaseFlowConfig config, DataFlow::Node database | + localDatabaseInput(database, result) and + config.hasFlow(DataFlow::exprNode(this), database) + ) + } + + override Expr getAStore() { + exists(LocalDatabaseFlowConfig config, DataFlow::Node database | + localDatabaseStore(database, result) and + config.hasFlow(DataFlow::exprNode(this), database) + ) + } +} + +/** A method that is both a database input and a database store. */ +private class LocalDatabaseInputStoreMethod extends Method { + LocalDatabaseInputStoreMethod() { + this.getDeclaringType() instanceof TypeSQLiteDatabase and + this.getName().matches("exec%SQL") + } +} + +/** + * Holds if `input` is a value being prepared for being stored into the SQLite dataabse `database`. + * This can be done using prepared statements, using the class `ContentValues`, or directly + * appending `input` to a SQL query. + */ +private predicate localDatabaseInput(DataFlow::Node database, Argument input) { + exists(Method m | input.getCall().getCallee() = m | + m instanceof LocalDatabaseInputStoreMethod and + database.asExpr() = input.getCall().getQualifier() + or + m.getDeclaringType() instanceof TypeSQLiteDatabase and + m.hasName("compileStatement") and + database.asExpr() = input.getCall() + or + m.getDeclaringType() instanceof ContentValues and + m.hasName("put") and + input.getPosition() = 1 and + database.asExpr() = input.getCall().getQualifier() + ) +} + +/** + * Holds if `store` is a method call for storing a previously appended input value, + * either through the use of prepared statements, via the `ContentValues` class, or + * directly executing a raw SQL query. + */ +private predicate localDatabaseStore(DataFlow::Node database, MethodAccess store) { + exists(Method m | store.getMethod() = m | + m instanceof LocalDatabaseInputStoreMethod and + database.asExpr() = store.getQualifier() + or + m.getDeclaringType() instanceof TypeSQLiteDatabase and + m.getName().matches(["insert%", "replace%", "update%"]) and + database.asExpr() = store.getAnArgument() and + database.getType() instanceof ContentValues + or + m.getDeclaringType() instanceof TypeSQLiteStatement and + m.hasName(["executeInsert", "executeUpdateDelete"]) and + database.asExpr() = store.getQualifier() + ) +} + +private class LocalDatabaseFlowConfig extends DataFlow::Configuration { + LocalDatabaseFlowConfig() { this = "LocalDatabaseFlowConfig" } + + override predicate isSource(DataFlow::Node source) { + source.asExpr() instanceof LocalDatabaseOpenMethodAccess + } + + override predicate isSink(DataFlow::Node sink) { + localDatabaseInput(sink, _) or + localDatabaseStore(sink, _) + } + + override predicate isAdditionalFlowStep(DataFlow::Node n1, DataFlow::Node n2) { + // Adds a step for tracking databases through field flow, that is, a database is opened and + // assigned to a field, and then an input or store method is called on that field elsewhere. + exists(Field f | + f.getType() instanceof TypeSQLiteDatabase and + f.getAnAssignedValue() = n1.asExpr() and + f = n2.asExpr().(FieldRead).getField() + ) + } +} diff --git a/java/ql/lib/semmle/code/java/security/SensitiveActions.qll b/java/ql/lib/semmle/code/java/security/SensitiveActions.qll index c0c45975d8b8..ceaae6fb40ee 100644 --- a/java/ql/lib/semmle/code/java/security/SensitiveActions.qll +++ b/java/ql/lib/semmle/code/java/security/SensitiveActions.qll @@ -14,7 +14,11 @@ import java private string suspicious() { - result = ["%password%", "%passwd%", "%account%", "%accnt%", "%trusted%"] + result = + [ + "%password%", "%passwd%", "pwd", "%account%", "%accnt%", "%trusted%", "%refresh%token%", + "%secret%token" + ] } private string nonSuspicious() { diff --git a/java/ql/src/Security/CWE/CWE-312/CleartextStorageAndroidDatabase.java b/java/ql/src/Security/CWE/CWE-312/CleartextStorageAndroidDatabase.java new file mode 100644 index 000000000000..0145125448f7 --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-312/CleartextStorageAndroidDatabase.java @@ -0,0 +1,27 @@ +public void sqliteStorageUnsafe(Context ctx, String name, String password) { + // BAD - sensitive information saved in cleartext. + SQLiteDatabase db = ctx.openOrCreateDatabase("test", Context.MODE_PRIVATE, null); + db.execSQL("INSERT INTO users VALUES (?, ?)", new String[] {name, password}); +} + +public void sqliteStorageSafe(Context ctx, String name, String password) { + // GOOD - sensitive information encrypted with a custom method. + SQLiteDatabase db = ctx.openOrCreateDatabase("test", Context.MODE_PRIVATE, null); + db.execSQL("INSERT INTO users VALUES (?, ?)", new String[] {name, encrypt(password)}); +} + +public void sqlCipherStorageSafe(String name, String password, String databasePassword) { + // GOOD - sensitive information saved using SQLCipher. + net.sqlcipher.database.SQLiteDatabase db = + net.sqlcipher.database.SQLiteDatabase.openOrCreateDatabase("test", databasePassword, null); + db.execSQL("INSERT INTO users VALUES (?, ?)", new String[] {name, password}); +} + +private static String encrypt(String cleartext) { + // Use an encryption or strong hashing algorithm in the real world. + // The example below just returns a SHA-256 hash. + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(cleartext.getBytes(StandardCharsets.UTF_8)); + String encoded = Base64.getEncoder().encodeToString(hash); + return encoded; +} \ No newline at end of file diff --git a/java/ql/src/Security/CWE/CWE-312/CleartextStorageAndroidDatabase.qhelp b/java/ql/src/Security/CWE/CWE-312/CleartextStorageAndroidDatabase.qhelp new file mode 100644 index 000000000000..f8e6183f36f9 --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-312/CleartextStorageAndroidDatabase.qhelp @@ -0,0 +1,36 @@ + + + +

+ 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 columnsPresentInTable, int computedColumnsOffset, String typeDiscriminatorValue, + String selection, String[] selectionArgs, String groupBy, String having); + + public abstract String buildUnionSubQuery(String typeDiscriminatorColumn, String[] unionColumns, + Set columnsPresentInTable, int computedColumnsOffset, String typeDiscriminatorValue, + String selection, String groupBy, String having); + + public static void appendColumns(StringBuilder s, String[] columns) { + } + + public abstract void setProjectionMap(Map columnMap); + + public abstract void setTables(String inTables); + + public abstract void appendWhere(CharSequence inWhere); + + public abstract void appendWhereStandalone(CharSequence inWhere); + +} diff --git a/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebResourceResponse.java b/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebResourceResponse.java index 0df042650462..1a2ff3cc1da9 100644 --- a/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebResourceResponse.java +++ b/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebResourceResponse.java @@ -16,7 +16,6 @@ package android.webkit; import java.io.InputStream; -import java.io.StringBufferInputStream; import java.util.Map; /** diff --git a/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebSettings.java b/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebSettings.java index e6a7d5d70508..33c9a1b8a571 100644 --- a/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebSettings.java +++ b/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebSettings.java @@ -15,11 +15,8 @@ */ package android.webkit; +import java.net.CookieManager; import android.content.Context; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /** * Manages settings state for a WebView. When a WebView is first created, it diff --git a/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebView.java b/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebView.java index d9625b707717..3065a9df966a 100644 --- a/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebView.java +++ b/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebView.java @@ -18,6 +18,7 @@ import android.view.View; public class WebView extends View { + public WebView(Context context) { super(context); } diff --git a/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebViewClient.java b/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebViewClient.java index 4b1bd58498b8..03a98480210e 100644 --- a/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebViewClient.java +++ b/java/ql/test/stubs/google-android-9.0.0/android/webkit/WebViewClient.java @@ -15,9 +15,6 @@ */ package android.webkit; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - public class WebViewClient { /** * Give the host application a chance to take over the control when a new url is