mirror of
https://github.com/github/codeql.git
synced 2026-04-30 19:26:02 +02:00
Merge pull request #10684 from joefarebrother/android-keyboard-cache
Java: Add query for Sensitive Keyboard Cache
This commit is contained in:
@@ -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())
|
||||
)
|
||||
)
|
||||
}
|
||||
15
java/ql/src/Security/CWE/CWE-524/Example.xml
Normal file
15
java/ql/src/Security/CWE/CWE-524/Example.xml
Normal 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>
|
||||
@@ -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>
|
||||
18
java/ql/src/Security/CWE/CWE-524/SensitiveKeyboardCache.ql
Normal file
18
java/ql/src/Security/CWE/CWE-524/SensitiveKeyboardCache.ql
Normal 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."
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
8
java/ql/test/query-tests/security/CWE-524/R.java
Normal file
8
java/ql/test/query-tests/security/CWE-524/R.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
16
java/ql/test/query-tests/security/CWE-524/Test.java
Normal file
16
java/ql/test/query-tests/security/CWE-524/Test.java
Normal 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);
|
||||
}
|
||||
}
|
||||
1
java/ql/test/query-tests/security/CWE-524/options
Normal file
1
java/ql/test/query-tests/security/CWE-524/options
Normal file
@@ -0,0 +1 @@
|
||||
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/google-android-9.0.0
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user