コンテンツ リゾルバ

OWASP カテゴリ: MASVS-PLATFORM: プラットフォームのインタラクション

概要

こちらのドキュメントによると、ContentResolver は「アプリがコンテンツ モデルにアクセスできるようにするクラス」です。ContentResolver は、以下から提供されるコンテンツを操作、取得、変更するメソッドを公開しています。

  • インストール済みのアプリ(content:// URI スキーム)
  • ファイル システム(file:// URI スキーム)
  • Android で規定されているサポート API(android.resource:// URI スキーム)。

要約すると、ContentResolver に関連する脆弱性は、攻撃者が脆弱なアプリの権限を利用して保護されたコンテンツにアクセスできることから、混乱した使節(confused deputy)に分類されます。

リスク: 信頼できない file:// URI に基づく悪用

file:// URI の脆弱性を使った ContentResolver の悪用では、ContentResolver の機能を利用して、URI で記述されたファイル記述子を返します。この脆弱性は、ContentResolver APIopenFile()openFileDescriptor()openInputStream()openOutputStream()、または openAssetFileDescriptor() のような関数に影響を与えます。この脆弱性を、完全にまたは部分的に攻撃者の管理下にある file:// URI を使って悪用すると、アプリに、アクセス可能となるとは意図されていなかったファイル(内部データベースや共有設定など)にアクセスさせることができます。

攻撃シナリオの一つとして、悪意のあるギャラリーやファイル選択ツールを作成し、脆弱性のあるアプリで使用すると悪意のある URI を返すようにしておくというものがあります。

この攻撃には次のような亜種があります。

  • アプリの内部ファイルを指す、完全に攻撃者の管理下にある file:// URI
  • file:// URI の一部が攻撃者の管理下にあるため、簡単にパス トラバーサルができる
  • file://: 攻撃者の管理下にあり、かつアプリの内部ファイルを指しているシンボリック リンク(symlink)をターゲットとする URI
  • 上の亜種と同様だが、攻撃者がシンボリック リンクのターゲットを正規のターゲットからアプリの内部ファイルに繰り返し交換するというもの。その目的は、セキュリティ チェックとファイルパスの使用との間の競合状態を利用することです。

影響

この脆弱性が利用された場合の影響は、ContentResolver の用途によって異なります。多くの場合、アプリの保護対象データの流出、または不正ユーザーによる保護対象データの改ざんが発生する可能性があります。

リスクの軽減

この脆弱性を軽減するには、以下のアルゴリズムを使用してファイル記述子を検証します。検証に合格すると、そのファイル記述子を安全に使用できるようになります。

Kotlin

fun isValidFile(ctx: Context, pfd: ParcelFileDescriptor, fileUri: Uri): Boolean {
    // Canonicalize to resolve symlinks and path traversals.
    val fdCanonical = File(fileUri.path!!).canonicalPath

    val pfdStat: StructStat = Os.fstat(pfd.fileDescriptor)

    // Lstat doesn't follow the symlink.
    val canonicalFileStat: StructStat = Os.lstat(fdCanonical)

    // Since we canonicalized (followed the links) the path already,
    // the path shouldn't point to symlink unless it was changed in the
    // meantime.
    if (OsConstants.S_ISLNK(canonicalFileStat.st_mode)) {
        return false
    }

    val sameFile =
        pfdStat.st_dev == canonicalFileStat.st_dev &&
        pfdStat.st_ino == canonicalFileStat.st_ino

    if (!sameFile) {
        return false
    }

    return !isBlockedPath(ctx, fdCanonical)
}

fun isBlockedPath(ctx: Context, fdCanonical: String): Boolean {
    // Paths that should rarely be exposed
    if (fdCanonical.startsWith("/proc/") ||
        fdCanonical.startsWith("/data/misc/")) {
        return true
    }

    // Implement logic to block desired directories. For example, specify
    // the entire app data/ directory to block all access.
}

Java

boolean isValidFile(Context ctx, ParcelFileDescriptor pfd, Uri fileUri) {
    // Canonicalize to resolve symlinks and path traversals
    String fdCanonical = new File(fileUri.getPath()).getCanonicalPath();

    StructStat pfdStat = Os.fstat(pfd.getFileDescriptor());

    // Lstat doesn't follow the symlink.
    StructStat canonicalFileStat = Os.lstat(fdCanonical);

    // Since we canonicalized (followed the links) the path already,
    // the path shouldn't point to symlink unless it was changed in the meantime
    if (OsConstants.S_ISLNK(canonicalFileStat.st_mode)) {
        return false;
    }

    boolean sameFile =
        pfdStat.stDev == canonicalFileStat.stDev && pfdStat.stIno == canonicalFileStat.stIno;

    if (!sameFile) {
        return false;
    }

    return !isBlockedPath(ctx, fdCanonical);
}

boolean isBlockedPath(Context ctx, String fdCanonical) {
       
        // Paths that should rarely be exposed
        if (fdCanonical.startsWith("/proc/") || fdCanonical.startsWith("/data/misc/")) {
            return true;
        }

        // Implement logic to block desired directories. For example, specify
        // the entire app data/ directory to block all access.
}


リスク: 信頼できない content:// URI に基づく悪用

content:// URI の脆弱性を使用した ContentResolver の悪用は、完全にまたは部分的に攻撃者の管理下にある URI が ContentResolver API に渡され、アクセス可能となるとは意図されていなかったコンテンツが操作された場合に発生します。

この攻撃には、主に次の 2 つのシナリオがあります。

  • アプリが自身の内部コンテンツを操作するというシナリオ。たとえば、メールアプリが、攻撃者から URI を得てから、外部の写真ではなく自身の内部コンテンツ プロバイダから取得したデータを添付するというシナリオです。
  • アプリがプロキシとなり、攻撃者に代わって別のアプリのデータにアクセスするというシナリオ。たとえば、アプリ X のデータを、通常は攻撃者による閲覧を許さない権限で保護されている状況で、メールアプリが添付するというシナリオです。添付を行うアプリはアクセスできますが、初めからではないため、このようにしてこのコンテンツを攻撃者に中継します。

攻撃シナリオの一つとして、悪意のあるギャラリーやファイル選択ツールを作成し、脆弱性のあるアプリで使用すると悪意のある URI を返すようにしておくというものがあります。

影響

この脆弱性が悪用された場合の影響は、ContentResolver に関連付けられているコンテキストによって異なります。これによって、アプリの保護対象データの流出、または不正ユーザーによる保護対象データの改ざんが発生する可能性があります。

リスクの軽減

全般

受け取る URI を検証します。たとえば、所定の機関の許可リストを使用することもおすすめです。

URI が、脆弱性があるアプリに属する、エクスポートされていない、または権限で保護されているコンテンツ プロバイダをターゲットにしている

次のようにして、URI がアプリをターゲットにしているかどうかをチェックします。

Kotlin

fun belongsToCurrentApplication(ctx: Context, uri: Uri): Boolean {
    val authority: String = uri.authority.toString()
    val info: ProviderInfo =
        ctx.packageManager.resolveContentProvider(authority, 0)!!

    return ctx.packageName.equals(info.packageName)
}

Java

boolean belongsToCurrentApplication(Context ctx, Uri uri){
    String authority = uri.getAuthority();
    ProviderInfo info = ctx.getPackageManager().resolveContentProvider(authority, 0);

    return ctx.getPackageName().equals(info.packageName);
}

または、次のようにして、ターゲットにしているプロバイダがエクスポートされているかどうかをチェックします。

Kotlin

fun isExported(ctx: Context, uri: Uri): Boolean {
    val authority = uri.authority.toString()
    val info: ProviderInfo =
            ctx.packageManager.resolveContentProvider(authority, 0)!!

    return info.exported
}

Java

boolean isExported(Context ctx, Uri uri){
    String authority = uri.getAuthority();
    ProviderInfo info = ctx.getPackageManager().resolveContentProvider(authority, 0);      

    return info.exported;
}

あるいは、次のようにして、URI への明示的な権限が付与されているかどうかを確認します。このチェックは、データにアクセスするための明示的な権限が付与されていれば、URI に悪意がないということを前提としています。

Kotlin

// grantFlag is one of: FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
fun wasGrantedPermission(ctx: Context, uri: Uri?, grantFlag: Int): Boolean {
    val pid: Int = Process.myPid()
    val uid: Int = Process.myUid()
    return ctx.checkUriPermission(uri, pid, uid, grantFlag) ==
            PackageManager.PERMISSION_GRANTED
}

Java

// grantFlag is one of: FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
boolean wasGrantedPermission(Context ctx, Uri uri, int grantFlag){
    int pid = Process.myPid();
    int uid = Process.myUid();

    return ctx.checkUriPermission(uri, pid, uid, grantFlag) == PackageManager.PERMISSION_GRANTED;
}

URI が、別のアプリに属する権限で保護された ContentProvider をターゲットにしているが、その別のアプリが脆弱性のあるアプリを信頼している

この攻撃は次のような状況で問題になります。

  • カスタム権限やその他の認証メカニズムをアプリが定義して使用するようなアプリのエコシステム。
  • 攻撃者が、実行時の権限(READ_CONTACTS など)を保持している脆弱なアプリを悪用して、システム プロバイダからデータを取得する、権限プロキシ攻撃。

次のようにして、URI 権限が付与されているかどうかをテストします。

Kotlin

// grantFlag is one of: FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
fun wasGrantedPermission(ctx: Context, uri: Uri?, grantFlag: Int): Boolean {
    val pid: Int = Process.myPid()
    val uid: Int = Process.myUid()
    return ctx.checkUriPermission(uri, pid, uid, grantFlag) ==
            PackageManager.PERMISSION_GRANTED
}

Java

// grantFlag is one of: FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
boolean wasGrantedPermission(Context ctx, Uri uri, int grantFlag){
    int pid = Process.myPid();
    int uid = Process.myUid();

    return ctx.checkUriPermission(uri, pid, uid, grantFlag) == PackageManager.PERMISSION_GRANTED;
}

他のコンテンツ プロバイダの使用に権限の付与が不要な場合(エコシステムのすべてのアプリがすべてのデータにアクセスできる場合など)は、こうした権限の使用を明示的に禁止してください。