Webviews: carregamento de URI não seguro

Categoria do OWASP: MASVS-CODE - Qualidade do código (link em inglês)

Visão geral

Um carregamento de URI não seguro ocorre quando um app Android não avalia corretamente a validade de um URI antes de carregá-lo em uma WebView.

O motivo desse tipo de vulnerabilidade é que um URI é composto por várias partes (link em inglês). Assim, ao menos o esquema e o host (da parte da autoridade) precisam ser verificados (por exemplo, na lista de permissões) antes de o URI ser carregado em uma WebView ou usado internamente pelo aplicativo.

Os erros mais comuns incluem:

  • Verificar o host, mas não o esquema, permitindo que um invasor use esquemas como http://, content:// ou javascript:// com um host autenticado.
  • Falha ao analisar o URI de forma correta, especialmente nos casos em que ele é recebido como uma string.
  • Validação do esquema, mas não do host (validação de host insuficiente).

Em relação ao último caso, isso geralmente ocorre quando o aplicativo precisa permitir subdomínios arbitrários de um domínio principal. Portanto, mesmo que o nome do host tenha sido extraído corretamente, o app usa métodos como startsWith, endsWith, ou contains da classe java.lang.String para validar a presença de um domínio principal na seção da string extraída. Usados incorretamente, esses métodos podem levar a resultados com falha e forçar o aplicativo a confiar de forma indevida em um host potencialmente mal-intencionado.

Impacto

Dependendo do contexto em que o host é usado, o impacto pode variar. Nos casos em que o carregamento de um URI malicioso (ou seja, que ignorou a filtragem/lista de permissões) em um WebView pode levar à invasão de conta (por exemplo, usando phishing), execução de código (por exemplo, carregamento de JavaScript malicioso) ou comprometendo o dispositivo (exploração do código entregue usando um hiperlink).

Mitigações

Ao processar URIs de string, é importante analisar a string como um URI e validar o esquema e o host:

Kotlin

fun isUriTrusted(incomingUri: String, trustedHostName: String): Boolean {
    try {
        val uri = Uri.parse(incomingUri)
        return uri.scheme == "https" && uri.host == trustedHostName
    } catch (e: NullPointerException) {
        throw NullPointerException("incomingUri is null or not well-formed")
    }
}

Java

public static boolean isUriTrusted(String incomingUri, String trustedHostName)
    throws NullPointerException {
        try {
            Uri uri = Uri.parse(incomingUri);
            return uri.getScheme().equals("https") &&
            uri.getHost().equals(trustedHostName);
        } catch (NullPointerException e) {
            throw new NullPointerException(
                "incomingUri is null or not well-formed");
        }
    }

Para a validação do host, depois de isolar a parte do URI correspondente, é importante fazer uma validação total (e não parcial) para identificar com precisão se o host é confiável ou não. Ao usar métodos como startsWith ou endsWith, é importante usar a sintaxe correta e não ignorar caracteres ou símbolos necessários. Por exemplo, endsWith exige o caractere de ponto "." antes do nome de domínio para uma correspondência precisa. Ignorar esses caracteres pode resultar em correspondências imprecisas e comprometer a segurança. Como os subdomínios podem ser aninhados infinitamente, a correspondência de expressão regular não é uma estratégia recomendada para validar nomes de host.

Colaboradores: Dimitrios Valsamaras e Michael Peck da Microsoft Threat Intelligence

Recursos