Merge pull request #10684 from joefarebrother/android-keyboard-cache

Java: Add query for Sensitive Keyboard Cache
This commit is contained in:
Joe Farebrother
2022-11-16 15:27:44 +00:00
committed by GitHub
13 changed files with 296 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
/** Definitions for the keyboard cache query */
import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.security.SensitiveActions
import semmle.code.xml.AndroidManifest
/** An Android Layout XML file. */
private class AndroidLayoutXmlFile extends XmlFile {
AndroidLayoutXmlFile() { this.getRelativePath().matches("%/res/layout/%.xml") }
}
/** A component declared in an Android layout file. */
class AndroidLayoutXmlElement extends XmlElement {
AndroidXmlAttribute id;
AndroidLayoutXmlElement() {
this.getFile() instanceof AndroidLayoutXmlFile and
id = this.getAttribute("id")
}
/** Gets the ID of this component. */
string getId() { result = id.getValue() }
/** Gets the class of this component. */
Class getClass() {
this.getName() = "view" and
this.getAttribute("class").getValue() = result.getQualifiedName()
or
this.getName() = result.getQualifiedName()
or
result.hasQualifiedName(["android.widget", "android.view"], this.getName())
}
}
/** An XML element that represents an editable text field. */
class AndroidEditableXmlElement extends AndroidLayoutXmlElement {
AndroidEditableXmlElement() {
this.getClass().getASourceSupertype*().hasQualifiedName("android.widget", "EditText")
}
/** Gets the input type of this field, if any. */
string getInputType() { result = this.getAttribute("inputType").(AndroidXmlAttribute).getValue() }
}
/** A `findViewById` or `requireViewById` method on `Activity` or `View`. */
private class FindViewMethod extends Method {
FindViewMethod() {
this.hasQualifiedName("android.view", "View", ["findViewById", "requireViewById"])
or
exists(Method m |
m.hasQualifiedName("android.app", "Activity", ["findViewById", "requireViewById"]) and
this = m.getAnOverride*()
)
}
}
/** Gets a use of the view that has the given id. */
private MethodAccess getAUseOfViewWithId(string id) {
exists(string name, NestedClass r_id, Field id_field |
id = "@+id/" + name and
result.getMethod() instanceof FindViewMethod and
r_id.getEnclosingType().hasName("R") and
r_id.hasName("id") and
id_field.getDeclaringType() = r_id and
id_field.hasName(name)
|
DataFlow::localExprFlow(id_field.getAnAccess(), result.getArgument(0))
)
}
/** Gets the argument of a use of `setInputType` called on the view with the given id. */
private Argument setInputTypeForId(string id) {
exists(MethodAccess setInputType |
setInputType.getMethod().hasQualifiedName("android.widget", "TextView", "setInputType") and
DataFlow::localExprFlow(getAUseOfViewWithId(id), setInputType.getQualifier()) and
result = setInputType.getArgument(0)
)
}
/** Holds if the given field is a constant flag indicating that an input with this type will not be cached. */
private predicate inputTypeFieldNotCached(Field f) {
f.getDeclaringType().hasQualifiedName("android.text", "InputType") and
(
not f.getName().matches("%TEXT%")
or
f.getName().matches("%PASSWORD%")
or
f.hasName("TYPE_TEXT_FLAG_NO_SUGGESTIONS")
)
}
/** Configuration that finds uses of `setInputType` for non cached fields. */
private class GoodInputTypeConf extends DataFlow::Configuration {
GoodInputTypeConf() { this = "GoodInputTypeConf" }
override predicate isSource(DataFlow::Node node) {
inputTypeFieldNotCached(node.asExpr().(FieldAccess).getField())
}
override predicate isSink(DataFlow::Node node) { node.asExpr() = setInputTypeForId(_) }
override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
exists(OrBitwiseExpr bitOr |
node1.asExpr() = bitOr.getAChildExpr() and
node2.asExpr() = bitOr
)
}
}
/** Gets a regex indicating that an input field may contain sensitive data. */
private string getInputSensitiveInfoRegex() {
result =
[
getCommonSensitiveInfoRegex(),
"(?i).*(bank|credit|debit|(pass(wd|word|code|phrase))|security).*"
]
}
/** Holds if input using the given input type (as written in XML) is not stored in the keyboard cache. */
bindingset[ty]
private predicate inputTypeNotCached(string ty) {
not ty.matches("%text%")
or
ty.regexpMatch("(?i).*(nosuggestions|password).*")
}
/** Gets an input field whose contents may be sensitive and may be stored in the keyboard cache. */
AndroidEditableXmlElement getASensitiveCachedInput() {
result.getId().regexpMatch(getInputSensitiveInfoRegex()) and
(
not inputTypeNotCached(result.getInputType()) and
not exists(GoodInputTypeConf conf, DataFlow::Node src, DataFlow::Node sink |
conf.hasFlow(src, sink) and
sink.asExpr() = setInputTypeForId(result.getId())
)
)
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- BAD: This password field uses the `text` input type, which allows the input to be saved to the keyboard cache. -->
<EditText
android:id="@+id/password_bad"
android:inputType="text"/>
<!-- GOOD: This password field uses the `textPassword` input type, which ensures that the input is not saved to the keyboard cache. -->
<EditText
android:id="@+id/password_good"
android:inputType="textPassword"/>
</LinearLayout>

View File

@@ -0,0 +1,33 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>When a user enters information in a text input field on an Android application, their input is saved to a keyboard cache which provides autocomplete suggestions and predictions. There is a risk that sensitive user data, such as passwords or banking information, may be leaked to other applications via the keyboard cache.</p>
</overview>
<recommendation>
<p>For input fields expected to accept sensitive information, use input types such as <code>"textNoSuggestions"</code> (or <code>"textPassword"</code> for a password) to ensure the input does not get stored in the keyboard cache.</p>
<p>Optionally, instead of declaring an input type through XML, you can set the input type in your code using <code>TextView.setInputType()</code>.</p>
</recommendation>
<example>
<p>In the following example, the field labeled BAD allows the password to be saved to the keyboard cache,
whereas the field labeled GOOD uses the <code>"textPassword"</code> input type to ensure the password is not cached.</p>
<sample src="Example.xml" />
</example>
<references>
<li>
OWASP Mobile Application Security Testing Guide: <a href="https://github.com/OWASP/owasp-mastg/blob/b7a93a2e5e0557cc9a12e55fc3f6675f6986bb86/Document/0x05d-Testing-Data-Storage.md#determining-whether-the-keyboard-cache-is-disabled-for-text-input-fields-mstg-storage-5">Determining Whether the Keyboard Cache Is Disabled for Text Input Fields</a>.
</li>
<li>
Android Developers: <a href="https://developer.android.com/reference/android/widget/TextView#attr_android:inputType">android:inputType attribute documentation.</a>
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,18 @@
/**
* @name Android sensitive keyboard cache
* @description Allowing the keyboard to cache sensitive information may result in information leaks to other applications.
* @kind problem
* @problem.severity warning
* @security-severity 8.1
* @id java/android/sensitive-keyboard-cache
* @tags security
* external/cwe/cwe-524
* @precision medium
*/
import java
import semmle.code.java.security.SensitiveKeyboardCacheQuery
from AndroidEditableXmlElement el
where el = getASensitiveCachedInput()
select el, "This input field may contain sensitive information that is saved to the keyboard cache."

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new query, `java/android/sensitive-keyboard-cache`, to detect instances of sensitive information possibly being saved to the keyboard cache.

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.test">
</manifest>

View File

@@ -0,0 +1,8 @@
package com.example.test;
public final class R {
public static final class id {
public static final int test7_password = 1;
public static final int test8_password = 2;
}
}

View File

@@ -0,0 +1,19 @@
import java
import semmle.code.java.security.SensitiveKeyboardCacheQuery
import TestUtilities.InlineExpectationsTest
class SensitiveKeyboardCacheTest extends InlineExpectationsTest {
SensitiveKeyboardCacheTest() { this = "SensitiveKeyboardCacheTest" }
override string getARelevantTag() { result = "hasResult" }
override predicate hasActualResult(Location loc, string element, string tag, string value) {
exists(AndroidEditableXmlElement el |
el = getASensitiveCachedInput() and
loc = el.getLocation() and
element = el.toString() and
tag = "hasResult" and
value = ""
)
}
}

View File

@@ -0,0 +1,16 @@
package com.example.test;
import android.app.Activity;
import android.os.Bundle;
import android.widget.EditText;
import android.view.View;
import android.text.InputType;
class Test extends Activity {
public void onCreate(Bundle b) {
EditText test7pw = findViewById(R.id.test7_password);
test7pw.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
EditText test8pw = requireViewById(R.id.test8_password);
test8pw.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
}

View File

@@ -0,0 +1 @@
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/google-android-9.0.0

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- $hasResult --> <EditText
android:id="@+id/test1_password"
android:inputType="text"/>
<EditText
android:id="@+id/test2_safe"
android:inputType="text"/>
<EditText
android:id="@+id/test3_password"
android:inputType="textNoSuggestions"/>
<EditText
android:id="@+id/test4_password"
android:inputType="textPassword"/>
<!-- $hasResult --> <EditText
android:id="@+id/test5_bank_account_name"
android:inputType="textMultiLine"/>
<!-- $hasResult --> <EditText
android:id="@+id/test6_password"/>
<EditText
android:id="@+id/test7_password"/>
<EditText
android:id="@+id/test8_password"/>
</LinearLayout>

View File

@@ -488,4 +488,8 @@ public class Activity extends ContextWrapper {
public <T extends View> T findViewById(int id) {
return null;
}
public <T extends View> T requireViewById(int id) {
return null;
}
}