mirror of
https://github.com/github/codeql.git
synced 2025-12-24 04:36:35 +01:00
Second query version
Remove sinks flowing to write operations requirement
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
108
java/ql/test/query-tests/security/CWE-441/Test.java
Normal file
108
java/ql/test/query-tests/security/CWE-441/Test.java
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
1
java/ql/test/query-tests/security/CWE-441/options
Normal file
1
java/ql/test/query-tests/security/CWE-441/options
Normal file
@@ -0,0 +1 @@
|
||||
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/google-android-9.0.0
|
||||
Reference in New Issue
Block a user