Skip to content

MASTG-TEST-0001: Testando Local Storage para Dados Sensíveis

Deprecated Test

This test is deprecated and should not be used anymore. Reason: Nova versão disponível no MASTG V2

Please check the following MASTG v2 tests that cover this v1 test:

Visão Geral

Este caso de teste foca na identificação de dados potencialmente sensíveis armazenados por um aplicativo e na verificação se eles estão armazenados de forma segura. As seguintes verificações devem ser realizadas:

  • Analisar o armazenamento de dados no código-fonte.
  • Certificar-se de acionar todas as funcionalidades possíveis no aplicativo (por exemplo, clicando em todos os lugares possíveis) para garantir a geração de dados.
  • Verificar todos os arquivos gerados e modificados pelo aplicativo e assegurar que o método de armazenamento seja suficientemente seguro.
    • Isso inclui SharedPreferences, bancos de dados, Armazenamento Interno, Armazenamento Externo, etc.

NOTA: Para conformidade com MASVS L1, é suficiente armazenar dados não criptografados no diretório de armazenamento interno do aplicativo (sandbox). Para conformidade L2, é necessária criptografia adicional usando chaves criptográficas gerenciadas com segurança no Android KeyStore. Isso inclui o uso de criptografia de envelope (DEK+KEK) ou métodos equivalentes, ou o uso de EncryptedFile/EncryptedSharedPreferences da Biblioteca de Segurança do Android.

Aviso

A biblioteca de criptografia de segurança do Jetpack, incluindo as classes EncryptedFile e EncryptedSharedPreferences, foi descontinuada. No entanto, como um substituto oficial ainda não foi lançado, recomendamos o uso dessas classes até que uma alternativa esteja disponível.

Análise Estática

Primeiramente, tente determinar o tipo de armazenamento usado pelo aplicativo Android e descobrir se o aplicativo processa dados sensíveis de forma insegura.

  • Verifique o AndroidManifest.xml em busca de permissões de leitura/gravação em armazenamento externo, por exemplo, uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE".
  • Verifique o código-fonte em busca de palavras-chave e chamadas de API usadas para armazenar dados:
    • Permissões de arquivo, como:
      • MODE_WORLD_READABLE ou MODE_WORLD_WRITABLE: Você deve evitar o uso de MODE_WORLD_WRITEABLE e MODE_WORLD_READABLE para arquivos, pois qualquer aplicativo poderá ler ou gravar neles, mesmo que estejam armazenados no diretório de dados privados do aplicativo. Se os dados precisarem ser compartilhados com outros aplicativos, considere usar um content provider. Um content provider oferece permissões de leitura e gravação para outros aplicativos e pode conceder permissões dinâmicas caso a caso.
    • Classes e funções, como:
      • a classe SharedPreferences (armazena pares chave-valor)
      • a classe FileOutPutStream (usa armazenamento interno ou externo)
      • as funções getExternal* (usam armazenamento externo)
      • a função getWritableDatabase (retorna um SQLiteDatabase para gravação)
      • a função getReadableDatabase (retorna um SQLiteDatabase para leitura)
      • as funções getCacheDir e getExternalCacheDirs (usam arquivos em cache)

A criptografia deve ser implementada usando funções comprovadas do SDK. A seguir, descrevemos más práticas a serem procuradas no código-fonte:

  • Informações sensíveis armazenadas localmente "criptografadas" por meio de operações simples de bits como XOR ou inversão de bits. Essas operações devem ser evitadas porque os dados criptografados podem ser recuperados facilmente.
  • Chaves usadas ou criadas sem os recursos integrados do Android, como o Android KeyStore
  • Chaves divulgadas por hard-coding

Um uso inadequado típico são chaves criptográficas hard-coded. Chaves criptográficas hard-coded e legíveis globalmente aumentam significativamente a possibilidade de recuperação de dados criptografados. Uma vez que um invasor obtém os dados, descriptografá-los é trivial. Chaves de criptografia simétrica devem ser armazenadas no dispositivo, portanto, identificá-las é apenas uma questão de tempo e esforço. Considere o seguinte código:

this.db = localUserSecretStore.getWritableDatabase("SuperPassword123");

Obter a chave é trivial porque ela está contida no código-fonte e é idêntica para todas as instalações do aplicativo. Criptografar dados dessa forma não é benéfico. Procure por chaves de API hard-coded/chaves privadas e outros dados valiosos; eles representam um risco similar. Chaves codificadas/criptografadas representam outra tentativa de dificultar, mas não impossibilitar, o acesso aos ativos mais valiosos.

Considere o seguinte código:

Exemplo em Java:

//Um esforço mais complicado para armazenar as metades XOR de uma chave (em vez da própria chave)
private static final String[] myCompositeKey = new String[]{
  "oNQavjbaNNSgEqoCkT9Em4imeQQ=","3o8eFOX4ri/F8fgHgiy/BS47"
};

Exemplo em Kotlin:

private val myCompositeKey = arrayOf<String>("oNQavjbaNNSgEqoCkT9Em4imeQQ=", "3o8eFOX4ri/F8fgHgiy/BS47")

O algoritmo para decodificar a chave original pode ser algo assim:

Exemplo em Java:

public void useXorStringHiding(String myHiddenMessage) {
  byte[] xorParts0 = Base64.decode(myCompositeKey[0],0);
  byte[] xorParts1 = Base64.decode(myCompositeKey[1],0);

  byte[] xorKey = new byte[xorParts0.length];
  for(int i = 0; i < xorParts1.length; i++){
    xorKey[i] = (byte) (xorParts0[i] ^ xorParts1[i]);
  }
  HidingUtil.doHiding(myHiddenMessage.getBytes(), xorKey, false);
}

Exemplo em Kotlin:

fun useXorStringHiding(myHiddenMessage:String) {
  val xorParts0 = Base64.decode(myCompositeKey[0], 0)
  val xorParts1 = Base64.decode(myCompositeKey[1], 0)
  val xorKey = ByteArray(xorParts0.size)
  for (i in xorParts1.indices)
  {
    xorKey[i] = (xorParts0[i] xor xorParts1[i]).toByte()
  }
  HidingUtil.doHiding(myHiddenMessage.toByteArray(), xorKey, false)
}

Verifique locais comuns de segredos:

  • recursos (normalmente em res/values/strings.xml) Exemplo:
<resources>
    <string name="app_name">SuperApp</string>
    <string name="hello_world">Hello world!</string>
    <string name="action_settings">Settings</string>
    <string name="secret_key">My_Secret_Key</string>
  </resources>
  • configurações de build, como em local.properties ou gradle.properties Exemplo:
buildTypes {
  debug {
    minifyEnabled true
    buildConfigField "String", "hiddenPassword", "\"${hiddenPassword}\""
  }
}

Análise Dinâmica

Instale e use o aplicativo, executando todas as funções pelo menos uma vez. Os dados podem ser gerados quando inseridos pelo usuário, enviados pelo endpoint ou incluídos no aplicativo. Em seguida, complete o seguinte:

  • Verifique tanto o armazenamento local interno quanto externo em busca de qualquer arquivo criado pelo aplicativo que contenha dados sensíveis.
  • Identifique arquivos de desenvolvimento, arquivos de backup e arquivos antigos que não deveriam estar incluídos em uma versão de produção.
  • Determine se bancos de dados SQLite estão disponíveis e se contêm informações sensíveis. Bancos de dados SQLite são armazenados em /data/data/<package-name>/databases.
  • Identifique se os bancos de dados SQLite estão criptografados. Se estiverem, determine como a senha do banco de dados é gerada e armazenada e se isso está suficientemente protegido conforme descrito na seção "Armazenando uma Chave)" da visão geral do Keystore.
  • Verifique Shared Preferences armazenados como arquivos XML (em /data/data/<package-name>/shared_prefs) em busca de informações sensíveis. Shared Preferences são inseguros e não criptografados por padrão. Alguns aplicativos podem optar por usar secure-preferences para criptografar os valores armazenados em Shared Preferences.
  • Verifique as permissões dos arquivos em /data/data/<package-name>. Apenas o usuário e grupo criados quando você instalou o aplicativo (por exemplo, u0_a82) devem ter permissões de leitura, gravação e execução (rwx) do usuário. Outros usuários não devem ter permissão para acessar arquivos, mas podem ter permissões de execução para diretórios.
  • Verifique o uso de qualquer Firebase Real-time databases e tente identificar se eles estão configurados incorretamente fazendo a seguinte chamada de rede:
    • https://_firebaseProjectName_.firebaseio.com/.json
  • Determine se um banco de dados Realm está disponível em /data/data/<package-name>/files/, se está unencrypted e se contém informações sensíveis. Por padrão, a extensão do arquivo é realm e o nome do arquivo é default. Inspecione o banco de dados Realm com o Realm Browser.