Skip to end of metadata
Go to start of metadata

By using the ContentProvider.openFile() method, you can provide a facility for another application to access your application data (file). Depending on the implementation of ContentProvider, use of the method can lead to a directory traversal vulnerability. Therefore, when exchanging a file through a content provider, the path should be canonicalized before it is used.

This rule is an Android specific instance of IDS01-J. Normalize strings before validating them) and IDS02-J. Canonicalize path names before validating them.

Noncompliant Code Example 1

This noncompliant code example tries to retrieve the last segment from the path paramUri, which is supposed to denote a file name, by calling android.net.Uri.getLastPathSegment(). The file is accessed in the pre-configured parent directory IMAGE_DIRECTORY.

private static String IMAGE_DIRECTORY = localFile.getAbsolutePath();
public ParcelFileDescriptor openFile(Uri paramUri, String paramString)
    throws FileNotFoundException {
  File file = new File(IMAGE_DIRECTORY, paramUri.getLastPathSegment());
  return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}

However, when the path is URL encoded, it may denote a file in an unintended directory which is outside of the pre-configured parent directory.

From Android 4.3.0_r2.2, the method Uri.getLastPathSegment() calls Uri.getPathSegments() internally (see: Cross Reference: Uri.java):

public String getLastPathSegment() {
  // TODO: If we haven't parsed all of the segments already, just
  // grab the last one directly so we only allocate one string.
  List<String> segments = getPathSegments();
  int size = segments.size();
  if (size == 0) {
    return null;
  }
  return segments.get(size - 1);
}

A part of the method Uri.getPathSegments() is as follows:

PathSegments getPathSegments() {
  if (pathSegments != null) {
    return pathSegments;
  }
  String path = getEncoded();
  if (path == null) {
    return pathSegments = PathSegments.EMPTY;
  }
  PathSegmentsBuilder segmentBuilder = new PathSegmentsBuilder();
  int previous = 0;
  int current;
  while ((current = path.indexOf('/', previous)) > -1) {
    // This check keeps us from adding a segment if the path starts
    // '/' and an empty segment for "//".
    if (previous < current) {
      String decodedSegment = decode(path.substring(previous, current));
      segmentBuilder.add(decodedSegment);
    }
    previous = current + 1;
  }
  // Add in the final path segment.
  if (previous < path.length()) {
    segmentBuilder.add(decode(path.substring(previous)));
  }
  return pathSegments = segmentBuilder.build();
}

The method Uri.getPathSegments() first acquires a path by calling getEncoded(), then divides the path into segments using "/" as a separator. Any segment that is encoded will be URL decoded by the decode() method.

If a path is URL encoded, the separator character will be "%2F" instead of "/" and getLastPathSegment() may not properly return the last segment of the path, which will be a surprise to the user of the method. Moreover, the design of this API directly enables a directory traversal vulnerability.

If the Uri.getPathSegments() method decoded a path before making it into segments, the URL encoded path could be properly processed. Unfortunately, this is not the case and users should not pass a path to Uri.getLastPathSegment() before decoding it.

Noncompliant Code Example 2

This noncompliant code example attempts to fix the first noncompliant code example by calling Uri.getLastPathSegment() twice. The first call is intended for URL decoding and the second call is to obtain the string the developer wanted.

private static String IMAGE_DIRECTORY = localFile.getAbsolutePath();
  public ParcelFileDescriptor openFile(Uri paramUri, String paramString)
      throws FileNotFoundException {
    File file = new File(IMAGE_DIRECTORY, Uri.parse(paramUri.getLastPathSegment()).getLastPathSegment());
    return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
  }

For example, consider what happens when the following URL encoded strings is passed to the content provider:

    ..%2F..%2F..%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml

The first call of Uri.getLastPathSegment() will return the following string:

    ../../../data/data/com.example.android.app/shared_prefs/Example.xml

The string is converted to a Uri object by Uri.parse(), which is passed to the second call of Uri.getLastPathSegment(). The resulting string will be:

    Example.xml

The string is used to create a file object. However, if an attacker could supply a string which cannot be decoded by the first call of the Uri.getLastPathSegment(), the last path segment may not be retrieved. An attacker can create such a string by using the technique called double encoding:

Double Encoding

(See [OWASP 2009] Double Encoding for more information.)

For example, the following double encoded string will circumvent the fix.

    %252E%252E%252F%252E%252E%252F%252E%252E%252Fdata%252Fdata%252Fcom.example.android.app%252Fshared_prefs%252FExample.xml

The first call of Uri.getLastPathSegment() will decode "%25" to "%" and return the string:

    %2E%2E%2F%2E%2E%2F%2E%2E%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml

When this string is passed to the second Uri.getLastPathSegment(), "%2E" and "%2F" will be decoded and the result will be:

 

   ../../../data/data/com.example.android.app/shared_prefs/Example.xml

which makes directory traversal possible.

As a mitigation to the directory traversal attack in this example, it is not enough to only decode the strings. The decoded path must be checked to make sure that the path is under the intended directory.

Proof of Concept

The following malicious code can exploit the vulnerable application that contains the first noncompliant code example:

String target = "content://com.example.android.sdk.imageprovider/data/" +
  "..%2F..%2F..%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml";

ContentResolver cr = this.getContentResolver();
FileInputStream fis = (FileInputStream)cr.openInputStream(Uri.parse(target));

byte[] buff = new byte[fis.available()];
in.read(buff);

Proof of Concept (Double Encoding)

The following malicious code can exploit the vulnerable application that contains the second noncompliant code example:

String target = "content://com.example.android.sdk.imageprovider/data/" +
  "%252E%252E%252F%252E%252E%252F%252E%252E%252Fdata%252Fdata%252Fcom.example.android.app%252Fshared_prefs%252FExample.xml";

ContentResolver cr = this.getContentResolver();
FileInputStream fis = (FileInputStream)cr.openInputStream(Uri.parse(target));

byte[] buff = new byte[fis.available()];
in.read(buff);

 

Compliant Solution

In the following compliant solution, a path is decoded by Uri.decode() before use. Also, after the File object is created, the path is canonicalized by calling File.getCanonicalPath() and checked that it is included in IMAGE_DIRECTORY.

By using the canonicalized path, directory traversal will be mitigated even when a doubly-encoded path is supplied.

private static String IMAGE_DIRECTORY = localFile.getAbsolutePath();
  public ParcelFileDescriptor openFile(Uri paramUri, String paramString)
      throws FileNotFoundException {
    String decodedUriString = Uri.decode(paramUri.toString());
    File file = new File(IMAGE_DIRECTORY, Uri.parse(decodedUriString).getLastPathSegment());
    if (file.getCanonicalPath().indexOf(localFile.getCanonicalPath()) != 0) {
      throw new IllegalArgumentException();
    }
    return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
  }

Applicability

Applications should ensure that any URL received by a content provider is canonicalized to avoid a directory traversal attack.

This rule is special case of IDS01-J. Normalize strings before validating them) and IDS02-J. Canonicalize path names before validating them.

Risk Assessment

Failing to canonicalize a path received by a content provider may lead to a directory traversal vulnerability which could result in the release of sensitive data or in the malicious corruption of data.

Rule

Severity

Likelihood

Remediation Cost

Priority

Level

DRD08-J

High

Probable

Low

P18

L1

Automated Detection

Automatic detection of the receipt of a URL is straightforward. It should also be feasible to automatically check whether the path has been canonicalized. However, if it has not, manual intervention would be required.

Related Vulnerabilities

  • JVN#78601526 GREE for Android vulnerable to directory traversal

Bibliography

 


1 Comment

  1. This looks suspiciously like a general Java rule, not an Android rule, right?