Resolvedores de conteúdo

Categoria do OWASP: MASVS-PLATFORM - Interação com plataformas

Visão geral

De acordo com a documentação, ContentResolver é uma "classe que fornece aos aplicativos acesso ao modelo de conteúdo". Os resolvedores de conteúdo expõem métodos para interagir, buscar ou modificar conteúdo fornecido de:

  • Apps instalados (esquema de URI content://)
  • Sistemas de arquivos (esquema de URI file://)
  • APIs de suporte fornecidas pelo Android (esquema de URI android.resource://)

Resumindo, as vulnerabilidades relacionadas a ContentResolver pertencem à classe confused deputy (link em inglês), porque o invasor pode usar os privilégios de um aplicativo vulnerável para acessar conteúdo protegido.

Risco: abuso baseado em um URI file:// não confiável

O abuso de ContentResolver com a vulnerabilidade de URI file:// aproveita a capacidade de ContentResolver retornar descritores de arquivos descritos pelo URI. Essa vulnerabilidade afeta funções como openFile(), openFileDescriptor(), openInputStream(), openOutputStream() ou openAssetFileDescriptor() da API ContentResolver. A vulnerabilidade pode ser explorada com um controle total ou parcial do URI file:// para forçar o aplicativo a acessar arquivos que não deveriam ser acessíveis, por exemplo, bancos de dados internos ou preferências compartilhadas.

Um dos possíveis cenários de ataque seria criar uma galeria ou um seletor de arquivos maliciosos que, quando usada por um app vulnerável, retornaria um URI malicioso.

Existem algumas variantes desse ataque:

  • Um URI file:// totalmente controlado pelo invasor que aponta para os arquivos internos de um app.
  • Uma parte do URI file:// é controlada pelo invasor, tornando-o propenso a travessias de caminho.
  • Um URI file:// que leva a um link simbólico controlado por um invasor que aponta para os arquivos internos do app.
  • Semelhante à variante anterior, mas aqui o invasor troca repetidamente o destino do link simbólico de um destino legítimo por arquivos internos de um app. O objetivo é explorar uma disputa entre uma possível verificação de segurança e o uso do caminho do arquivo.

Impacto

O impacto da exploração dessa vulnerabilidade varia de acordo com o uso do ContentResolver. Em muitos casos, isso pode resultar na exfiltração de dados protegidos de um app ou em modificações desses dados feitas por partes não autorizadas.

Mitigações

Para atenuar essa vulnerabilidade, use o algoritmo abaixo para validar o descritor de arquivos. Após a validação, o descritor de arquivos pode ser usado com segurança.

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.
}


Risco: abuso baseado em um URI content:// não confiável

O abuso de um ContentResolver usando uma vulnerabilidade de URI content:// ocorre quando um URI controlado total ou parcialmente por um invasor é transmitido a APIs ContentResolver para operar em conteúdo que não deveria ser acessado.

Há dois cenários principais para esse ataque:

  • O app opera no próprio conteúdo interno. Por exemplo: depois de receber um URI de um invasor, o app de e-mail anexa dados do próprio provedor de conteúdo interno em vez de uma foto externa.
  • O app atua como um proxy e acessa os dados de outro app do invasor. Por exemplo: o aplicativo de e-mail anexa dados do app X que é protegido por uma permissão que normalmente impede que o invasor acesse esse anexo específico. Ele está disponível para o aplicativo que está fazendo o anexo, mas não inicialmente, redirecionando esse conteúdo ao invasor.

Um possível cenário de ataque é criar uma galeria ou um seletor de arquivos maliciosos que, quando usados por um app vulnerável, retornariam um URI malicioso.

Impacto

O impacto da exploração dessa vulnerabilidade varia de acordo com o contexto associado ao ContentResolver. Isso pode resultar na exfiltração de dados protegidos de um app ou em modificações feitas por partes não autorizadas em dados protegidos.

Mitigações

Geral

Valide os URIs recebidos. Por exemplo, usar uma lista de permissões das autoridades esperadas é considerado uma prática recomendada.

O URI é destinado ao provedor de conteúdo não exportado ou protegido por permissões que pertence ao app vulnerável

Verifique se o URI está direcionado ao seu app:

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);
}

Ou se o provedor de destino for exportado:

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;
}

Ou, se a permissão foi concedida ao URI, essa verificação se baseia no pressuposto de que, se essa permissão é concedida para acessar os dados, o URI não é malicioso:

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;
}

O URI é destinado a um ContentProvider protegido por permissão que pertence a outro app que confia no app vulnerável

Esse ataque é relevante nestas situações:

  • Ecossistemas de aplicativos em que os apps definem e usam permissões personalizadas ou outros mecanismos de autenticação.
  • Ataques de proxy de permissão, em que um invasor explora um app vulnerável que tem uma permissão de execução, como READ_CONTACTS, para extrair dados de um provedor de sistema.

Teste se a permissão do URI foi concedida:

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;
}

Se o uso de outros provedores de conteúdo não exigir uma concessão de permissão (como quando o app permite que todos os apps do ecossistema acessem todos os dados), proíba explicitamente o uso dessas autorizações.