Skip to content

Java: Create new query Cleartext storage of sensitive information in Android databases #6492

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
2 changes: 2 additions & 0 deletions java/ql/lib/semmle/code/java/dataflow/ExternalFlow.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions java/ql/lib/semmle/code/java/frameworks/Thrift.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 0 additions & 36 deletions java/ql/lib/semmle/code/java/frameworks/android/Android.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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") }
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
16 changes: 16 additions & 0 deletions java/ql/lib/semmle/code/java/frameworks/android/SQLite.qll
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/** Provides classes and predicates for working with SQLite databases. */

import java
import Android
import semmle.code.java.dataflow.FlowSteps
Expand All @@ -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 =
Expand Down
23 changes: 23 additions & 0 deletions java/ql/lib/semmle/code/java/frameworks/android/Widget.qll
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
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.
</p>
</overview>

<recommendation>
<p>
Use <code>SQLCipher</code> or similar libraries to add encryption capabilities to SQLite. Alternatively, encrypt sensitive data using cryptographically secure algorithms before storing it in the database.
</p>
</recommendation>

<example>
<p>
In the first example, sensitive user information is stored in cleartext.
</p>

<p>
In the second and third examples, the code encrypts sensitive information before saving it to the database.
</p>
<sample src="CleartextStorageAndroidDatabase.java" />
</example>

<references>
<li>
Android Developers:
<a href="https://developer.android.com/topic/security/data">Work with data more securely</a>
</li>
<li>
SQLCipher:
<a href="https://www.zetetic.net/sqlcipher/sqlcipher-for-android/">Android Application Integration</a>
</li>
</references>
</qhelp>
Loading