Categoria do OWASP: MASVS-CODE - Qualidade do código
Visão geral
O carregamento dinâmico de código em um aplicativo introduz um nível de risco que precisa ser mitigado. Os invasores podem adulterar ou substituir o código para acessar dados sensíveis ou executar ações prejudiciais.
Muitas formas de carregamento dinâmico de código, especialmente aquelas que usam fontes remotas, violam as políticas do Google Play e podem levar à suspensão do seu app no Google Play.
Impacto
Se os invasores conseguirem acessar o código que será carregado no aplicativo, eles poderão modificá-lo para atingir os objetivos deles. Isso pode levar à exfiltração de dados e a exploits de execução de código. Mesmo que os invasores não possam modificar o código para realizar ações arbitrárias de escolha deles, ainda é possível que eles corrompam ou removam o código e, assim, afetem a disponibilidade do aplicativo.
Mitigações
Evitar o uso do carregamento dinâmico de código
A menos que haja uma necessidade comercial, evite o carregamento dinâmico de código. É preferível incluir todas as funcionalidades diretamente no aplicativo, sempre que possível.
Usar fontes confiáveis
O código que será carregado no aplicativo precisa ser armazenado em locais confiáveis. Em relação ao armazenamento local, o armazenamento interno do aplicativo ou o armazenamento com escopo (para o Android 10 e versões mais recentes) são os locais recomendados. Esses locais têm medidas para evitar o acesso direto de outros aplicativos e usuários.
Ao carregar código de locais remotos, como URLs, evite usar terceiros sempre que possível e armazene o código na sua própria infraestrutura, seguindo as práticas recomendadas de segurança. Se você precisar carregar código de terceiros, verifique se o provedor é confiável.
Realizar verificações de integridade
As verificações de integridade são recomendadas para garantir que o código não tenha sido adulterado. Essas verificações precisam ser realizadas antes de carregar o código no aplicativo.
Ao carregar recursos remotos, a integridade de subrecursos pode ser usada para validar a integridade dos recursos acessados.
Ao carregar recursos do armazenamento externo, use verificações de integridade para verificar se nenhum outro aplicativo adulterou esses dados ou códigos. Os hashes dos arquivos precisam ser armazenados de maneira segura, de preferência criptografados e no armazenamento interno.
Kotlin
package com.example.myapplication
import java.io.BufferedInputStream
import java.io.FileInputStream
import java.io.IOException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
object FileIntegrityChecker {
@Throws(IOException::class, NoSuchAlgorithmException::class)
fun getIntegrityHash(filePath: String?): String {
val md = MessageDigest.getInstance("SHA-256") // You can choose other algorithms as needed
val buffer = ByteArray(8192)
var bytesRead: Int
BufferedInputStream(FileInputStream(filePath)).use { fis ->
while (fis.read(buffer).also { bytesRead = it } != -1) {
md.update(buffer, 0, bytesRead)
}
}
private fun bytesToHex(bytes: ByteArray): String {
val sb = StringBuilder(bytes.length * 2)
for (b in bytes) {
sb.append(String.format("%02x", b))
}
return sb.toString()
}
@Throws(IOException::class, NoSuchAlgorithmException::class)
fun verifyIntegrity(filePath: String?, expectedHash: String): Boolean {
val actualHash = getIntegrityHash(filePath)
return actualHash == expectedHash
}
@Throws(Exception::class)
@JvmStatic
fun main(args: Array<String>) {
val filePath = "/path/to/your/file"
val expectedHash = "your_expected_hash_value"
if (verifyIntegrity(filePath, expectedHash)) {
println("File integrity is valid!")
} else {
println("File integrity is compromised!")
}
}
}
Java
package com.example.myapplication;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class FileIntegrityChecker {
public static String getIntegrityHash(String filePath) throws IOException, NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256"); // You can choose other algorithms as needed
byte[] buffer = new byte[8192];
int bytesRead;
try (BufferedInputStream fis = new BufferedInputStream(new FileInputStream(filePath))) {
while ((bytesRead = fis.read(buffer)) != -1) {
md.update(buffer, 0, bytesRead);
}
}
byte[] digest = md.digest();
return bytesToHex(digest);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public static boolean verifyIntegrity(String filePath, String expectedHash) throws IOException, NoSuchAlgorithmException {
String actualHash = getIntegrityHash(filePath);
return actualHash.equals(expectedHash);
}
public static void main(String[] args) throws Exception {
String filePath = "/path/to/your/file";
String expectedHash = "your_expected_hash_value";
if (verifyIntegrity(filePath, expectedHash)) {
System.out.println("File integrity is valid!");
} else {
System.out.println("File integrity is compromised!");
}
}
}
Assinar o código
Outra opção para garantir a integridade dos dados é assinar o código e verificar a assinatura dele antes de carregá-lo. Esse método também garante a integridade do código hash, não apenas do código em si, o que oferece uma proteção extra contra adulteração.
Embora a assinatura de código ofereça camadas de segurança extras, é importante considerar que ela é um processo mais complexo que pode exigir mais esforço e recursos para ser implementada com sucesso.
Alguns exemplos de assinatura de código podem ser encontrados na seção "Recursos" deste documento.
Recursos
- Integridade de subrecursos
- Assinar dados digitalmente
- Assinatura de código
- Dados sensíveis armazenados no armazenamento externo