內容解析器

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

總覽

根據說明文件ContentResolver「應用程式可存取內容模型的類別」。ContentResolver 會公開互動、擷取或修改下列內容的方法:

  • 已安裝的應用程式 (content:// URI Scheme)
  • 檔案系統 (file:// URI Scheme)
  • 支援 Android 提供的 API (android.resource:// URI Scheme)。

簡單來說,與 ContentResolver 相關的安全漏洞屬於錯亂代理類別,因為攻擊者可以使用安全漏洞應用程式權限存取受保護的內容。

風險:基於不受信任的 file:// URI 濫用

濫用使用 file:// URI 安全漏洞的 ContentResolver 會剝削 ContentResolver 功能傳回 URI 描述的檔案描述元。這項安全漏洞會影響下列函式:openFile()openFileDescriptor()openInputStream()openOutputStream()openAssetFileDescriptor()(來自ContentResolver API )。如果安全漏洞遭到完全或部分攻擊者控制的 file:// URI 濫用,可能會強制要求應用程式存取不應開放存取的檔案,例如內部資料庫或共用偏好設定。

其中一種可能的攻擊行為,是建立惡意的圖片庫或檔案選擇器,並在安全漏洞應用程式使用時傳回惡意 URI。

這個攻擊只有少數幾個變化版本:

  • 指向應用程式內部檔案的完全受攻擊者控制的 file:// URI
  • 部分 file:// URI 是由攻擊者控管,因此容易造成路徑週遊
  • file:// 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 的濫用

將完全或部分由攻擊者控制的 URI 傳遞至 ContentResolver API,以對無意存取的內容進行操作時,將會濫用使用 content:// URI 安全漏洞的 ContentResolver

這類攻擊有兩種主要情況:

  • 應用程式會在自己的內部內容上運作。例如,在向攻擊者取得 URI 後,郵件應用程式會附加來自其內部內容供應器的資料,而非外部相片。
  • 應用程式會以 Proxy 的形式運作,然後存取另一個應用程式的資料,供攻擊者使用。例如:郵件應用程式會附加應用程式 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 applicationContext.packageName.equals(info.packageName)
}

Java

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

    return getApplicationContext().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,其屬於另一個應用程式,並信任安全漏洞應用程式。

這項攻擊與下列情境相關:

  • 應用程式的生態系統,其中應用程式定義及使用自訂權限或其他驗證機制。
  • 權限 Proxy 攻擊:攻擊者濫用持有執行階段權限的安全漏洞應用程式 (例如 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;
}

如果使用其他內容供應器不需要權限授權 (例如應用程式允許生態系統中的所有應用程式存取所有資料),請明確禁止使用這些授權。