Query for detecting Local Android DoS caused by NFE

This commit is contained in:
luchua-bc
2020-11-05 04:25:21 +00:00
parent 31ec79819e
commit 76a0db84ee
8 changed files with 262 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
public class NFEAndroidDoS extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view);
// BAD: Uncaught NumberFormatException due to remote user inputs
{
String minPriceStr = getIntent().getStringExtra("priceMin");
double minPrice = Double.parseDouble(minPriceStr);
}
// GOOD: Use the proper Android method to get number extra
{
int width = getIntent().getIntExtra("width", 0);
int height = getIntent().getIntExtra("height", 0);
}
}
}

View File

@@ -0,0 +1,40 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>NumberFormatException (NFE) thrown but not caught by an Android application will crash the application. If the application allows external inputs, an attacker can send an invalid number as intent extra to trigger NFE, which introduces local Denial of Service (Dos) attack.</p>
<p>
This is a common problem in Android development since Android components don't have
<code>throw Exception(...)</code>
in their class and method definitions.
</p>
</overview>
<recommendation>
<p>
Use the Android methods intended to get number extras e.g.
<code>Intent.getFloatExtra(String name, float defaultValue)</code>
since they have the built-in try/catch processing, or explicitly do try/catch in the application.
</p>
</recommendation>
<example>
<p>The following example shows both 'BAD' and 'GOOD' configurations. In the 'BAD' configuration, number value is retrieved as string extra then parsed to double. In the 'GOOD' configuration, number value is retrieved as integer extra.</p>
<sample src="NFEAndroidDoS.java" />
</example>
<references>
<li>
CWE:
<a href="https://cwe.mitre.org/data/definitions/749.html">CWE-755: Improper Handling of Exceptional Conditions</a>
</li>
<li>
Android Developers:
<a href="https://developer.android.com/topic/performance/vitals/crash">Android Crashes</a>
</li>
<li>
Google Analytics:
<a href="https://developers.google.com/analytics/devguides/collection/android/v4/exceptions">Crash and Exception Measurement Using the Google Analytics SDK</a>
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,107 @@
/**
* @name Local Android DoS Caused By NumberFormatException
* @id java/android/nfe-local-android-dos
* @description NumberFormatException thrown but not caught by an Android application that allows external inputs can crash the application, which is a local Denial of Service (Dos) attack.
* @kind path-problem
* @tags security
* external/cwe/cwe-755
*/
import java
import semmle.code.java.frameworks.android.Intent
import semmle.code.java.frameworks.android.WebView
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph
/** Code from java/ql/src/Violations of Best Practice/Exception Handling/NumberFormatException.ql */
private class SpecialMethodAccess extends MethodAccess {
predicate isValueOfMethod(string klass) {
this.getMethod().getName() = "valueOf" and
this.getQualifier().getType().(RefType).hasQualifiedName("java.lang", klass) and
this.getAnArgument().getType().(RefType).hasQualifiedName("java.lang", "String")
}
predicate isParseMethod(string klass, string name) {
this.getMethod().getName() = name and
this.getQualifier().getType().(RefType).hasQualifiedName("java.lang", klass)
}
predicate throwsNFE() {
this.isParseMethod("Byte", "parseByte") or
this.isParseMethod("Short", "parseShort") or
this.isParseMethod("Integer", "parseInt") or
this.isParseMethod("Long", "parseLong") or
this.isParseMethod("Float", "parseFloat") or
this.isParseMethod("Double", "parseDouble") or
this.isParseMethod("Byte", "decode") or
this.isParseMethod("Short", "decode") or
this.isParseMethod("Integer", "decode") or
this.isParseMethod("Long", "decode") or
this.isValueOfMethod("Byte") or
this.isValueOfMethod("Short") or
this.isValueOfMethod("Integer") or
this.isValueOfMethod("Long") or
this.isValueOfMethod("Float") or
this.isValueOfMethod("Double")
}
}
private class SpecialClassInstanceExpr extends ClassInstanceExpr {
predicate isStringConstructor(string klass) {
this.getType().(RefType).hasQualifiedName("java.lang", klass) and
this.getAnArgument().getType().(RefType).hasQualifiedName("java.lang", "String") and
this.getNumArgument() = 1
}
predicate throwsNFE() {
this.isStringConstructor("Byte") or
this.isStringConstructor("Short") or
this.isStringConstructor("Integer") or
this.isStringConstructor("Long") or
this.isStringConstructor("Float") or
this.isStringConstructor("Double")
}
}
class NumberFormatException extends RefType {
NumberFormatException() { this.hasQualifiedName("java.lang", "NumberFormatException") }
}
private predicate catchesNFE(TryStmt t) {
exists(CatchClause cc, LocalVariableDeclExpr v |
t.getACatchClause() = cc and
cc.getVariable() = v and
v.getType().(RefType).getASubtype*() instanceof NumberFormatException
)
}
private predicate throwsNFE(Expr e) {
e.(SpecialClassInstanceExpr).throwsNFE() or e.(SpecialMethodAccess).throwsNFE()
}
/**
* Taint configuration tracking flow from untrusted inputs to number conversion calls.
*/
class NFELocalDoSConfiguration extends TaintTracking::Configuration {
NFELocalDoSConfiguration() { this = "NFELocalDoSConfiguration" }
/** Holds if source is a remote flow source */
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
/** Holds if NFE is thrown but not caught */
override predicate isSink(DataFlow::Node sink) {
exists(Expr e |
throwsNFE(e) and
not exists(TryStmt t |
t.getBlock() = e.getEnclosingStmt().getEnclosingStmt*() and
catchesNFE(t)
) and
sink.asExpr() = e
)
}
}
from DataFlow::PathNode source, DataFlow::PathNode sink, NFELocalDoSConfiguration conf
where conf.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "Local Android Denial of Service due to $@.", source.getNode(),
"user-provided value"

View File

@@ -0,0 +1,24 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app"
android:installLocation="auto"
android:versionCode="1"
android:versionName="0.1" >
<uses-permission android:name="android.permission.INTERNET" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".NFEAndroidDoS"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,22 @@
edges
| NFEAndroidDoS.java:13:24:13:34 | getIntent(...) : Intent | NFEAndroidDoS.java:14:21:14:51 | parseDouble(...) |
| NFEAndroidDoS.java:22:21:22:31 | getIntent(...) : Intent | NFEAndroidDoS.java:23:15:23:40 | parseInt(...) |
| NFEAndroidDoS.java:25:22:25:32 | getIntent(...) : Intent | NFEAndroidDoS.java:26:16:26:42 | parseInt(...) |
| NFEAndroidDoS.java:43:24:43:34 | getIntent(...) : Intent | NFEAndroidDoS.java:44:21:44:43 | new Double(...) |
| NFEAndroidDoS.java:43:24:43:34 | getIntent(...) : Intent | NFEAndroidDoS.java:47:21:47:47 | valueOf(...) |
nodes
| NFEAndroidDoS.java:13:24:13:34 | getIntent(...) : Intent | semmle.label | getIntent(...) : Intent |
| NFEAndroidDoS.java:14:21:14:51 | parseDouble(...) | semmle.label | parseDouble(...) |
| NFEAndroidDoS.java:22:21:22:31 | getIntent(...) : Intent | semmle.label | getIntent(...) : Intent |
| NFEAndroidDoS.java:23:15:23:40 | parseInt(...) | semmle.label | parseInt(...) |
| NFEAndroidDoS.java:25:22:25:32 | getIntent(...) : Intent | semmle.label | getIntent(...) : Intent |
| NFEAndroidDoS.java:26:16:26:42 | parseInt(...) | semmle.label | parseInt(...) |
| NFEAndroidDoS.java:43:24:43:34 | getIntent(...) : Intent | semmle.label | getIntent(...) : Intent |
| NFEAndroidDoS.java:44:21:44:43 | new Double(...) | semmle.label | new Double(...) |
| NFEAndroidDoS.java:47:21:47:47 | valueOf(...) | semmle.label | valueOf(...) |
#select
| NFEAndroidDoS.java:14:21:14:51 | parseDouble(...) | NFEAndroidDoS.java:13:24:13:34 | getIntent(...) : Intent | NFEAndroidDoS.java:14:21:14:51 | parseDouble(...) | Local Android Denial of Service due to $@. | NFEAndroidDoS.java:13:24:13:34 | getIntent(...) | user-provided value |
| NFEAndroidDoS.java:23:15:23:40 | parseInt(...) | NFEAndroidDoS.java:22:21:22:31 | getIntent(...) : Intent | NFEAndroidDoS.java:23:15:23:40 | parseInt(...) | Local Android Denial of Service due to $@. | NFEAndroidDoS.java:22:21:22:31 | getIntent(...) | user-provided value |
| NFEAndroidDoS.java:26:16:26:42 | parseInt(...) | NFEAndroidDoS.java:25:22:25:32 | getIntent(...) : Intent | NFEAndroidDoS.java:26:16:26:42 | parseInt(...) | Local Android Denial of Service due to $@. | NFEAndroidDoS.java:25:22:25:32 | getIntent(...) | user-provided value |
| NFEAndroidDoS.java:44:21:44:43 | new Double(...) | NFEAndroidDoS.java:43:24:43:34 | getIntent(...) : Intent | NFEAndroidDoS.java:44:21:44:43 | new Double(...) | Local Android Denial of Service due to $@. | NFEAndroidDoS.java:43:24:43:34 | getIntent(...) | user-provided value |
| NFEAndroidDoS.java:47:21:47:47 | valueOf(...) | NFEAndroidDoS.java:43:24:43:34 | getIntent(...) : Intent | NFEAndroidDoS.java:47:21:47:47 | valueOf(...) | Local Android Denial of Service due to $@. | NFEAndroidDoS.java:43:24:43:34 | getIntent(...) | user-provided value |

View File

@@ -0,0 +1,49 @@
package com.example.app;
import android.app.Activity;
import android.os.Bundle;
/** Android activity that tests app crash by NumberFormatException */
public class NFEAndroidDoS extends Activity {
// BAD - parse string extra to double
public void testOnCreate1(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(-1);
String minPriceStr = getIntent().getStringExtra("priceMin");
double minPrice = Double.parseDouble(minPriceStr);
}
// BAD - parse string extra to integer
public void testOnCreate2(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(-1);
String widthStr = getIntent().getStringExtra("width");
int width = Integer.parseInt(widthStr);
String heightStr = getIntent().getStringExtra("height");
int height = Integer.parseInt(heightStr);
}
// GOOD - parse int extra to integer
public void testOnCreate3(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(-1);
int width = getIntent().getIntExtra("width", 0);
int height = getIntent().getIntExtra("height", 0);
}
// BAD - convert string extra to double
public void testOnCreate4(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(-1);
String minPriceStr = getIntent().getStringExtra("priceMin");
double minPrice = new Double(minPriceStr);
String maxPriceStr = getIntent().getStringExtra("priceMax");
double maxPrice = Double.valueOf(minPriceStr);
}
}

View File

@@ -0,0 +1 @@
experimental/Security/CWE/CWE-755/NFEAndroidDoS.ql

View File

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