內容解析器

OWASP 類別:MASVS-PLATFORM:平台互動

總覽

根據說明文件定義,ContentResolver 是一種「可讓應用程式存取內容模型的類別」。ContentResolver 可以顯示多種方法,讓您擷取及修改以下項目提供的內容,並與這些內容互動:

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

簡單來說,與 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 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,這個供應器所屬的應用程式會信任具有安全漏洞的應用程式。

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

  • 應用程式的生態系統,其中應用程式定義及使用自訂權限或其他驗證機制。
  • 權限 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;
}

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