Second query version

Remove sinks flowing to write operations requirement
This commit is contained in:
Tony Torralba
2022-08-29 10:36:53 +02:00
parent 153ec5368e
commit 4a18892da9
12 changed files with 266 additions and 88 deletions

View File

@@ -1,14 +1,12 @@
/** Provides classes to reason about vulnerabilites related to content URIs. */
import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.frameworks.android.Android
private import semmle.code.java.dataflow.TaintTracking
private import semmle.code.java.frameworks.android.Android
private import semmle.code.java.security.PathSanitizer
/** A URI that gets resolved by a `ContentResolver`. */
abstract class ContentUriResolutionSink extends DataFlow::Node {
/** Gets the call node that resolves this URI. */
abstract DataFlow::Node getCallNode();
}
abstract class ContentUriResolutionSink extends DataFlow::Node { }
/** A sanitizer for content URIs. */
abstract class ContentUriResolutionSanitizer extends DataFlow::Node { }
@@ -31,10 +29,16 @@ private class DefaultContentUriResolutionSink extends ContentUriResolutionSink {
this.getType().(RefType).hasQualifiedName("android.net", "Uri")
)
}
}
/** Gets the call node of this argument. */
override DataFlow::Node getCallNode() {
result = DataFlow::exprNode(this.asExpr().(Argument).getCall())
/** A `ContentResolver` method that resolves a URI. */
private class UriOpeningContentResolverMethod extends Method {
UriOpeningContentResolverMethod() {
this.hasName([
"openInputStream", "openOutputStream", "openAssetFile", "openAssetFileDescriptor",
"openFile", "openFileDescriptor", "openTypedAssetFile", "openTypedAssetFileDescriptor",
]) and
this.getDeclaringType() instanceof AndroidContentResolver
}
}
@@ -46,6 +50,9 @@ private class UninterestingTypeSanitizer extends ContentUriResolutionSanitizer {
}
}
private class PathSanitizer extends ContentUriResolutionSanitizer instanceof PathInjectionSanitizer {
}
private class FilenameOnlySanitizer extends ContentUriResolutionSanitizer {
FilenameOnlySanitizer() {
exists(Method m | this.asExpr().(MethodAccess).getMethod() = m |
@@ -73,19 +80,8 @@ private class DecodedAsAnImageSanitizer extends ContentUriResolutionSanitizer {
"decodeStream"
])
|
DataFlow::localFlow(this.(ContentUriResolutionSink).getCallNode(),
DataFlow::exprNode(decodeArg))
TaintTracking::localExprTaint(this.(ContentUriResolutionSink).asExpr().(Argument).getCall(),
decodeArg)
)
}
}
/** A `ContentResolver` method that resolves a URI. */
private class UriOpeningContentResolverMethod extends Method {
UriOpeningContentResolverMethod() {
this.hasName([
"openInputStream", "openOutputStream", "openAssetFile", "openAssetFileDescriptor",
"openFile", "openFileDescriptor", "openTypedAssetFile", "openTypedAssetFileDescriptor",
]) and
this.getDeclaringType() instanceof AndroidContentResolver
}
}

View File

@@ -0,0 +1,24 @@
/** Provides taint tracking configurations to be used in unsafe content URI resolution queries. */
import java
import semmle.code.java.dataflow.ExternalFlow
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.security.UnsafeContentUriResolution
/** A taint-tracking configuration to find paths from remote sources to content URI resolutions. */
class UnsafeContentResolutionConf extends TaintTracking::Configuration {
UnsafeContentResolutionConf() { this = "UnsafeContentResolutionConf" }
override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) { sink instanceof ContentUriResolutionSink }
override predicate isSanitizer(DataFlow::Node sanitizer) {
sanitizer instanceof ContentUriResolutionSanitizer
}
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
any(ContentUriResolutionAdditionalTaintStep s).step(node1, node2)
}
}

View File

@@ -0,0 +1,41 @@
import android.content.ContentResolver;
import android.net.Uri;
public class Example extends Activity {
public void onCreate() {
// BAD: Externally-provided URI directly used in content resolution
{
ContentResolver contentResolver = getContentResolver();
Uri uri = (Uri) getIntent().getParcelableExtra("URI_EXTRA");
InputStream is = contentResolver.openInputStream(uri);
copyToExternalCache(is);
}
// BAD: input Uri is not normalized, and check can be bypassed with ".." characters
{
ContentResolver contentResolver = getContentResolver();
Uri uri = (Uri) getIntent().getParcelableExtra("URI_EXTRA");
String path = uri.getPath();
if (path.startsWith("/data"))
throw new SecurityException();
InputStream is = contentResolver.openInputStream(uri);
copyToExternalCache(is);
}
// GOOD: URI gets properly validated to avoid access to internal files
{
ContentResolver contentResolver = getContentResolver();
Uri uri = (Uri) getIntent().getParcelableExtra("URI_EXTRA");
String path = uri.getPath();
java.nio.file.Path normalized =
java.nio.file.FileSystems.getDefault().getPath(path).normalize();
if (normalized.startsWith("/data"))
throw new SecurityException();
InputStream is = contentResolver.openInputStream(uri);
copyToExternalCache(is);
}
}
private void copyToExternalCache(InputStream is) {
// Reads the contents of is and writes a file in the app's external
// cache directory, which can be read publicly by applications in the same device.
}
}

View File

@@ -0,0 +1,49 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
When an Android application wants to access data in a content provider, it uses the <code>ContentResolver</code>
object. <code>ContentResolver</code>s communicate with an instance of a class that implements the
<code>ContentProvider</code> interface via URIs with the <code>content://</code> scheme.
The authority part (the first path segment) of the URI passed as parameter to the <code>ContentResolver</code>
determines which content provider is contacted for the operation. Specific operations that act on files also
support the <code>file://</code> scheme, in which case the local filesystem is queried instead.
If an external component, like a malicious or compromised application, controls the URI that is used in a
<code>ContentResolver</code> operation, it can trick the vulnerable application into accessing its own private
files or non-exported content providers. Depending on what the vulnerable application does after accessing the file,
the attacking application might get access to the file by forcing it to be copied to a public directory like the
external storage, or tamper with it by making the application overwrite it with unexpected data.
</p>
</overview>
<recommendation>
<p>
If possible, avoid using externally-provided data to determine URIs used by a <code>ContentResolver</code>.
If that is not an option, validate that the incoming URI can only reference trusted components, like an allow list
of content providers and/or applications, or alternatively make sure that the URI does not reference private
directories like <code>/data/</code>.
</p>
</recommendation>
<example>
This example shows two ways of opening a file using a <code>ContentResolver</code>. In the first case, externally-provided
data coming from an intent is directly used in the file-reading operation, allowing an attacker to provide a URI
of the form <code>/data/data/(vulnerable app package)/(private file)</code> to trick the application into reading it and
copying it to the external storage. In the second case, the URI is validated before being used, making sure it does not reference
any internal application files.
<p>
</p>
<sample src="UnsafeContentUriResolution.java" />
</example>
<references>
<li>
Android developers:
<a href="https://developer.android.com/guide/topics/providers/content-provider-basics">Content provider basics</a>
</li>
<li>
<a href="https://developer.android.com/reference/android/content/ContentResolver">The ContentResolver class</a>
</li>
</references>
</qhelp>

View File

@@ -1,9 +1,9 @@
/**
* @name Uncontrolled data used in path expression
* @name Uncontrolled data used in content resolution
* @description Resolving externally-provided content URIs without validation can allow an attacker
* to access unexpected resources.
* @kind path-problem
* @problem.severity error
* @problem.severity warning
* @precision high
* @id java/android/unsafe-content-uri-resolution
* @tags security
@@ -12,11 +12,10 @@
*/
import java
import UnsafeContentUriResolutionQuery
import semmle.code.java.security.UnsafeContentUriResolutionQuery
import DataFlow::PathGraph
from DataFlow::PathNode src, DataFlow::PathNode sink
where any(UnsafeContentResolutionConf c).hasFlowPath(src, sink)
select sink.getNode(), src, sink,
"This $@ flows to a ContentResolver method that resolves a URI. The result is then used in a write operation.",
select sink.getNode(), src, sink, "This $@ flows to a ContentResolver method that resolves a URI.",
src.getNode(), "user input"

View File

@@ -1,61 +0,0 @@
/** Provides taint tracking configurations to be used in unsafe content URI resolution queries. */
import java
import semmle.code.java.dataflow.ExternalFlow
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.dataflow.TaintTracking2
import UnsafeContentUriResolution
/** A taint-tracking configuration to find paths from remote sources to content URI resolutions. */
class UnsafeContentResolutionConf extends TaintTracking::Configuration {
UnsafeContentResolutionConf() { this = "UnsafeContentResolutionConf" }
override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) {
flowsToWrite(sink.(ContentUriResolutionSink).getCallNode())
}
override predicate isSanitizer(DataFlow::Node sanitizer) {
sanitizer instanceof ContentUriResolutionSanitizer
}
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
any(ContentUriResolutionAdditionalTaintStep s).step(node1, node2)
}
}
/** Holds if `node` flows to a write operation. */
private predicate flowsToWrite(DataFlow::Node node) { any(FlowsToWriteConfig c).hasFlow(node, _) }
/** A taint-tracking configuration to find paths to write operations. */
private class FlowsToWriteConfig extends TaintTracking2::Configuration {
FlowsToWriteConfig() { this = "FlowsToWriteConfig" }
override predicate isSource(DataFlow::Node src) {
src = any(ContentUriResolutionSink s).getCallNode()
}
override predicate isSink(DataFlow::Node sink) {
sinkNode(sink, "create-file")
or
sinkNode(sink, "write-file")
or
exists(MethodAccess ma | sink.asExpr() = ma.getArgument(0) |
ma.getMethod() instanceof WriteStreamMethod
)
}
}
private class WriteStreamMethod extends Method {
WriteStreamMethod() {
this.getAnOverride*().hasQualifiedName("java.io", "OutputStream", "write")
or
this.hasQualifiedName("org.apache.commons.io", "IOUtils", "copy")
or
this.hasQualifiedName("org.springframework.util", ["StreamUtils", "CopyUtils"], "copy")
or
this.hasQualifiedName("com.google.common.io", ["ByteStreams", "CharStreams"], "copy")
}
}

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* A new query "Uncontrolled data used in content resolution" (`java/androd/unsafe-content-uri-resolution`) has been added. This query finds paths from user-provided data to URI resolution operations in Android's `ContentResolver` without previous validation or sanitization.

View File

@@ -0,0 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="test">
<application>
<activity android:exported="true" android:name=".Test">
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,108 @@
package test;
import android.content.ContentResolver;
import android.net.Uri;
import android.app.Activity;
public class Test extends Activity {
private void validateWithEquals(Uri uri) {
if (!uri.equals(Uri.parse("content://safe/uri")))
throw new SecurityException();
}
private void validateWithAllowList(Uri uri) throws SecurityException {
String path = uri.getPath();
java.nio.file.Path normalized =
java.nio.file.FileSystems.getDefault().getPath(path).normalize();
if (!normalized.startsWith("/safe/path"))
throw new SecurityException();
}
private void validateWithBlockList(Uri uri) throws SecurityException {
String path = uri.getPath();
java.nio.file.Path normalized =
java.nio.file.FileSystems.getDefault().getPath(path).normalize();
if (normalized.startsWith("/data"))
throw new SecurityException();
}
public void onCreate() {
{
ContentResolver contentResolver = getContentResolver();
Uri uri = (Uri) getIntent().getParcelableExtra("URI_EXTRA");
contentResolver.openInputStream(uri); // $ hasTaintFlow
}
{
ContentResolver contentResolver = getContentResolver();
Uri uri = (Uri) getIntent().getParcelableExtra("URI_EXTRA");
String path = uri.getPath();
if (path.startsWith("/data"))
throw new SecurityException();
contentResolver.openInputStream(uri); // $ hasTaintFlow
}
// Equals checks
{
ContentResolver contentResolver = getContentResolver();
Uri uri = (Uri) getIntent().getParcelableExtra("URI_EXTRA");
if (!uri.equals(Uri.parse("content://safe/uri")))
throw new SecurityException();
contentResolver.openInputStream(uri); // Safe
}
{
ContentResolver contentResolver = getContentResolver();
Uri uri = (Uri) getIntent().getParcelableExtra("URI_EXTRA");
validateWithEquals(uri);
contentResolver.openInputStream(uri); // Safe
}
// Allow list checks
{
ContentResolver contentResolver = getContentResolver();
Uri uri = (Uri) getIntent().getParcelableExtra("URI_EXTRA");
String path = uri.getPath();
if (!path.startsWith("/safe/path"))
throw new SecurityException();
contentResolver.openInputStream(uri); // $ hasTaintFlow
}
{
ContentResolver contentResolver = getContentResolver();
Uri uri = (Uri) getIntent().getParcelableExtra("URI_EXTRA");
String path = uri.getPath();
java.nio.file.Path normalized =
java.nio.file.FileSystems.getDefault().getPath(path).normalize();
if (!normalized.startsWith("/safe/path"))
throw new SecurityException();
contentResolver.openInputStream(uri); // Safe
}
{
ContentResolver contentResolver = getContentResolver();
Uri uri = (Uri) getIntent().getParcelableExtra("URI_EXTRA");
validateWithAllowList(uri);
contentResolver.openInputStream(uri); // Safe
}
// Block list checks
{
ContentResolver contentResolver = getContentResolver();
Uri uri = (Uri) getIntent().getParcelableExtra("URI_EXTRA");
String path = uri.getPath();
if (path.startsWith("/data"))
throw new SecurityException();
contentResolver.openInputStream(uri); // $ hasTaintFlow
}
{
ContentResolver contentResolver = getContentResolver();
Uri uri = (Uri) getIntent().getParcelableExtra("URI_EXTRA");
String path = uri.getPath();
java.nio.file.Path normalized =
java.nio.file.FileSystems.getDefault().getPath(path).normalize();
if (normalized.startsWith("/data"))
throw new SecurityException();
contentResolver.openInputStream(uri); // Safe
}
{
ContentResolver contentResolver = getContentResolver();
Uri uri = (Uri) getIntent().getParcelableExtra("URI_EXTRA");
validateWithBlockList(uri);
contentResolver.openInputStream(uri); // Safe
}
}
}

View File

@@ -0,0 +1,11 @@
import java
import TestUtilities.InlineFlowTest
import semmle.code.java.security.UnsafeContentUriResolutionQuery
class Test extends InlineFlowTest {
override DataFlow::Configuration getValueFlowConfig() { none() }
override TaintTracking::Configuration getTaintFlowConfig() {
result instanceof UnsafeContentResolutionConf
}
}

View File

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