OWASP Top 10 para Aplicaciones Móviles

Vulnerabilidades críticas y mejores prácticas para el desarrollo de aplicaciones móviles seguras

¿Qué es OWASP?

El Open Web Application Security Project (OWASP) es una fundación sin fines de lucro que trabaja para mejorar la seguridad del software. Su lista Top 10 identifica los riesgos de seguridad más críticos para las aplicaciones web y APIs y similares.

Riesgos específicos en entornos móviles

Las aplicaciones móviles enfrentan desafíos únicos: almacenamiento de datos en dispositivos que pueden perderse o robarse, comunicación a través de redes no confiables, y acceso a sensores como cámara, micrófono y geolocalización que pueden comprometer la privacidad del usuario.

Los 10 Riesgos Principales

1

Uso Incorrecto de Credenciales

Vector de ataque

Acceso a datos de autenticación mal almacenados o expuestos en el dispositivo.

Debilidad

Credenciales almacenadas sin cifrado o dentro del código fuente.

Impacto

Puede permitir que un atacante acceda a cuentas o funcionalidades restringidas.

Estrategias de mitigación

  • Almacenar credenciales en lugares seguros como Keystore o Keychain.
  • Evitar insertar claves en el código fuente.
  • Usar autenticación basada en tokens temporales.

Ejemplos de implementación

// Código vulnerable (Android)

// ❌ Guardar usuario y contraseña sin cifrar en SharedPreferences
val prefs = getSharedPreferences("creds", MODE_PRIVATE)
prefs.edit().putString("username", "admin").apply()
prefs.edit().putString("password", "123456").apply()

// Código seguro (Android)

// ✅ Usar EncryptedSharedPreferences para cifrar valores almacenados
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
val encryptedPrefs = EncryptedSharedPreferences.create(
    "secure_prefs",
    masterKeyAlias,
    context,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
encryptedPrefs.edit().putString("password", "123456").apply()

// Código vulnerable (Android)

// ❌ Contraseña codificada directamente en la app
fun login() {
  val credentials = mapOf("user" to "admin", "pass" to "123456")
  sendToServer(credentials)
}

// Código seguro (Android)

// ✅ Recupera la contraseña desde almacenamiento seguro
fun loginSecure() {
  val password = getPasswordFromSecureStorage()
  val credentials = mapOf("user" to "admin", "pass" to password)
  sendToServer(credentials)
}

// Código vulnerable (Android)

// ❌ Codifica credenciales directamente, sin cifrado ni token
val creds = "admin:123456"
val base64 = Base64.encodeToString(creds.toByteArray(), Base64.NO_WRAP)
request.setHeader("Authorization", "Basic $base64")

// Código seguro (Android)

// ✅ Utiliza token de autenticación seguro
val token = secureTokenProvider.getToken()
request.setHeader("Authorization", "Bearer $token")
2

Seguridad Inadecuada en la Cadena de Suministro

Vector de ataque

Inclusión de librerías o SDKs de terceros vulnerables o maliciosos.

Debilidad

Dependencias con código no verificado o con permisos excesivos.

Impacto

Puede introducir puertas traseras o facilitar ataques indirectos.

Estrategias de mitigación

  • Revisar la seguridad de cada librería o SDK antes de incluirlos.
  • Actualizar dependencias con regularidad.
  • Usar herramientas de análisis de composición de software (SCA).

Ejemplos de implementación

// Código vulnerable (Android)

// Agregando dependencia directa desde un repositorio privado no verificado
implementation("com.untrusted.sdk:tracking-lib:1.0.0")

// Código seguro (Android)

// ✅ Uso de librería de repositorio oficial y verificación de checksum
implementation("com.google.firebase:firebase-analytics:21.3.0")
// Además, usar Gradle Dependency Verification con archivo checksum

// Código vulnerable (Android)

// ❌ Dependencia con código ofuscado sin auditoría previa
implementation("org.unknown:lib-obfuscated:1.1.0")

// Código seguro (Android)

// ✅ Uso de librería auditada de código abierto con versión mantenida
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")

// Código vulnerable (Android)

// ❌ No se usa verificación de firma en el artefacto descargado
implementation("com.malicious.analytics:core:3.2.0")

// Código seguro (Android)

// ✅ Aplicación de firma PGP o checksum para verificar la integridad
dependencies {
  implementation("com.segment.analytics.android:analytics:4.10.4")
}
// Verificado desde Maven Central
3

Autenticación/Autorización Insegura

Vector de ataque

Manipulación del flujo de autenticación o reutilización de tokens.

Debilidad

Mecanismos de control de acceso mal implementados o débiles.

Impacto

Permite a usuarios no autorizados realizar acciones restringidas.

Estrategias de mitigación

  • Implementar autenticación multifactor.
  • Verificar tokens del lado del servidor en cada solicitud.
  • Evitar confiar únicamente en validaciones del lado cliente.

Ejemplos de implementación

// Código vulnerable (Android)

// ❌ Guardar token en preferencias sin cifrado
val prefs = getSharedPreferences("prefs", MODE_PRIVATE)
prefs.edit().putString("authToken", token).apply()

// Código seguro (Android)

// ✅ Guardar token usando EncryptedSharedPreferences
val securePrefs = EncryptedSharedPreferences.create(
  context,
  "secure_prefs",
  masterKey,
  EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
  EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("authToken", token).apply()

// Código vulnerable (Android)

// ❌ Validación local insegura
if (username == "admin" && password == "1234") {
  login()
}

// Código seguro (Android)

// ✅ Validación a través del backend con token JWT y posible MFA
api.login(username, password).enqueue {
  if (response.isSuccessful) {
    prompt2FA()
  }
}

// Código vulnerable (Android)

// ❌ Acceso a actividad sin comprobar autenticación
startActivity(Intent(this, ProfileActivity::class.java))

// Código seguro (Android)

// ✅ Comprobar autenticación antes de abrir actividad
if (sessionManager.isAuthenticated()) {
  startActivity(Intent(this, ProfileActivity::class.java))
} else {
  showLoginPrompt()
}
4

Validación Insuficiente de Entrada/Salida

Vector de ataque

Inyección de datos maliciosos a través de formularios o parámetros.

Debilidad

Falta de sanitización o validación de datos de entrada.

Impacto

Puede dar lugar a inyecciones SQL, XSS o corrupción de datos.

Estrategias de mitigación

  • Validar y sanear todos los datos del usuario.
  • Utilizar listas blancas de entrada permitida.
  • Aplicar encoding de salida al presentar datos.

Ejemplos de implementación

// Código vulnerable (Android)

// ❌ Uso del email sin validarlo
val email = emailInput.text.toString()
sendToServer(email)

// Código seguro (Android)

// ✅ Validación con expresión regular
val email = emailInput.text.toString()
val regex = Regex("[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+")
if (regex.matches(email)) {
  sendToServer(email)
} else {
  showToast("Email inválido")
}

// Código vulnerable (Android)

// ❌ SQL sin validación de entrada
val query = "INSERT INTO users (name) VALUES ('$userInput')"
database.execSQL(query)

// Código seguro (Android)

// ✅ Uso de SQLiteStatement preparado
val stmt = database.compileStatement("INSERT INTO users (name) VALUES (?)")
stmt.bindString(1, userInput)
stmt.execute()

// Código vulnerable (Android)

// ❌ Parseo de JSON sin validación
val user = gson.fromJson(response, User::class.java)
showUser(user)

// Código seguro (Android)

// ✅ Verificar campos requeridos después del parsing
val user = gson.fromJson(response, User::class.java)
if (!user.username.isNullOrEmpty() && user.id > 0) {
  showUser(user)
} else {
  showError()
}
5

Comunicación Insegura

Vector de ataque

Intercepción de tráfico entre la app y el servidor a través de redes públicas.

Debilidad

Transmisión de datos sin cifrado o sin validación de certificados.

Impacto

Permite a atacantes espiar o modificar información sensible.

Estrategias de mitigación

  • Usar HTTPS con certificados válidos y verificados.
  • Implementar pinning de certificados.
  • Evitar el uso de redes públicas para operaciones críticas.

Ejemplos de implementación

// Código vulnerable (Android)

// ❌ Permitir tráfico HTTP en AndroidManifest.xml
<application
  android:usesCleartextTraffic="true"
  ...>
</application>

// ❌ Cliente HTTP sin validación de certificados
val client = OkHttpClient.Builder()
  .hostnameVerifier { hostname, session -> true } // ❌ Acepta cualquier certificado
  .build()

// Código seguro (Android)

// ✅ Bloquear tráfico HTTP en AndroidManifest.xml
<application
  android:usesCleartextTraffic="false"
  ...>
</application>

// ✅ Implementación de Certificate Pinning con OkHttp
val certificatePinner = CertificatePinner.Builder()
  .add("api.example.com", "sha256/HASH_DEL_CERTIFICADO=")
  .build()

val client = OkHttpClient.Builder()
  .certificatePinner(certificatePinner)
  .build()

// Código vulnerable (Android)

// ❌ Envío de datos sensibles sin cifrado adicional
fun sendPaymentInfo(cardNumber: String, cvv: String, expiry: String) {
  val paymentData = JSONObject().apply {
    put("cardNumber", cardNumber) // ❌ Datos sensibles en texto plano
    put("cvv", cvv)
    put("expiry", expiry)
  }
  
  // ❌ Aunque use HTTPS, los datos van en texto plano en el cuerpo
  apiService.processPayment(paymentData)
}

// Código seguro (Android)

// ✅ Cifrado adicional de datos sensibles (end-to-end encryption)
fun sendPaymentInfo(cardNumber: String, cvv: String, expiry: String) {
  // ✅ Obtener clave pública del servidor
  val serverPublicKey = getServerPublicKey()
  
  // ✅ Añadir timestamp y nonce para prevenir ataques de replay
  val paymentData = JSONObject().apply {
    put("cardNumber", cardNumber)
    put("cvv", cvv)
    put("expiry", expiry)
    put("timestamp", System.currentTimeMillis())
    put("nonce", UUID.randomUUID().toString())
  }
  
  // ✅ Cifrar datos con la clave pública del servidor
  val encryptedData = encryptWithRsa(paymentData.toString(), serverPublicKey)
  
  // ✅ Enviar solo datos cifrados
  apiService.processEncryptedPayment(encryptedData)
}

// Código vulnerable (Android)

// ❌ Falta de validación de respuestas del servidor
suspend fun getUserProfile(userId: String): UserProfile {
  val response = apiService.getUserProfile(userId)
  
  // ❌ No se valida la respuesta ni se manejan errores adecuadamente
  val userProfile = response.body()
  
  // ❌ Se asume que la respuesta es válida y contiene datos
  return userProfile!!
}

// Código seguro (Android)

// ✅ Validación adecuada de respuestas del servidor
suspend fun getUserProfile(userId: String): Result<UserProfile> {
  return try {
    val response = apiService.getUserProfile(userId)
    
    // ✅ Verificar código de respuesta HTTP
    if (!response.isSuccessful) {
      return Result.failure(HttpException(response.code(), response.message()))
    }
    
    // ✅ Verificar que el cuerpo de la respuesta no sea nulo
    val userProfile = response.body() ?: return Result.failure(
      ApiException("Respuesta vacía del servidor")
    )
    
    // ✅ Verificar firma digital de la respuesta (si está disponible)
    val signature = response.headers()["X-Signature"]
    if (signature != null && !verifySignature(userProfile, signature)) {
      return Result.failure(SecurityException("Firma inválida"))
    }
    
    // ✅ Validar estructura y contenido de la respuesta
    validateUserProfile(userProfile)
    
    Result.success(userProfile)
  } catch (e: Exception) {
    Result.failure(e)
  }
}
6

Controles de Privacidad Inadecuados

Vector de ataque

Acceso o compartición de datos personales sin consentimiento explícito.

Debilidad

Recolección excesiva de información o sin justificación técnica.

Impacto

Compromete la privacidad del usuario y viola regulaciones legales.

Estrategias de mitigación

  • Solicitar permisos solo cuando sean necesarios.
  • Aplicar el principio de minimización de datos.
  • Incluir avisos de privacidad claros y comprensibles.

Ejemplos de implementación

// Código vulnerable (Android)

// ❌ Solicita permiso de ubicación sin justificación al iniciar la app
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1)

// Código seguro (Android)

// ✅ Solicita el permiso en el momento de uso y con aviso previo
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
  // Mostrar mensaje explicativo al usuario
}
requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1)

// Código vulnerable (Android)

// ❌ Registro de información sensible
Log.d("DEBUG", "Access Token: $token")

// Código seguro (Android)

// ✅ Evitar mostrar información sensible en logs
Log.d("DEBUG", "Token recibido correctamente")

// Código vulnerable (Android)

// ❌ Guardar datos sensibles sin cifrado en SharedPreferences
val prefs = getSharedPreferences("prefs", Context.MODE_PRIVATE)
prefs.edit().putString("password", "123456").apply()

// Código seguro (Android)

// ✅ Uso de EncryptedSharedPreferences
val masterKey = MasterKey.Builder(this)
  .setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
val securePrefs = EncryptedSharedPreferences.create(
  this, "secure_prefs", masterKey,
  EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
  EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("password", "123456").apply()
7

Protecciones Binarias Insuficientes

Vector de ataque

Análisis estático del binario o ingeniería inversa por parte del atacante.

Debilidad

Código fácilmente descompilable o sin protección contra modificaciones.

Impacto

Permite la creación de versiones maliciosas o piratería de la app.

Estrategias de mitigación

  • Ofuscar el código antes de su compilación.
  • Usar detección de debugging y hooking.
  • Aplicar firmas digitales a los binarios.

Ejemplos de implementación

// Código vulnerable (Android)

// ❌ Logging en producción
Log.d("DEBUG", "Token: $token")

// Código seguro (Android)

// ✅ Evitar logs en producción
if (BuildConfig.DEBUG) {
  Log.d("DEBUG", "Token presente")
}

// Código vulnerable (Android)

// ❌ APK no validado
fun startApp() {
  println("App iniciada")
}

// Código seguro (Android)

// ✅ Validación de firma del APK
val info = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES)
val signatures = info.signingInfo.apkContentsSigners
val valid = signatures.any { 
  it.toCharsString().contains("MIIBIjAN...") // Parte del certificado esperado 
}

// Código vulnerable (Android)

// ❌ Sin detección de root
fun isSecure() = true

// Código seguro (Android)

// ✅ Detección básica de root
fun isDeviceRooted(): Boolean {
  val rootPaths = arrayOf("/system/app/Superuser.apk", "/system/xbin/su")
  return rootPaths.any { File(it).exists() }
}
8

Configuración de Seguridad Incorrecta

Vector de ataque

Explotación de configuraciones por defecto o mal aplicadas en el entorno de la app.

Debilidad

Permisos mal definidos, errores de despliegue o configuración por defecto.

Impacto

Puede abrir puertas a accesos no autorizados o filtraciones de información.

Estrategias de mitigación

  • Revisar y limitar permisos solicitados.
  • Eliminar configuraciones y archivos de prueba en producción.
  • Desactivar funcionalidades de debugging en versiones finales.

Ejemplos de implementación

// Código vulnerable (Android)

/* ❌ AndroidManifest.xml:
<uses-permission android:name="android.permission.READ_SMS" />
No se usa este permiso en la app */
fun main() {
    println("App iniciada con permisos innecesarios")
}

// Código seguro (Android)

/* ✅ AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
Solo se solicita lo que realmente se necesita */
fun main() {
    println("App iniciada con permisos mínimos")
}

// Código vulnerable (Android)

val baseUrl = "https://dev.api.miapp.com" // ❌ entorno de desarrollo
println("❌ Usando entorno de prueba")

// Código seguro (Android)

val baseUrl = "https://api.miapp.com" // ✅ entorno de producción
println("✅ Entorno correcto en producción")

// Código vulnerable (Android)

println("❌ Log de debugging activo en producción")

// Código seguro (Android)

if (!BuildConfig.DEBUG) {
    println("✅ Producción sin logs de debug")
}
9

Almacenamiento de Datos Inseguro

Vector de ataque

Acceso físico al dispositivo o uso de malware para leer datos locales.

Debilidad

Guardar datos sensibles sin cifrado o en ubicaciones accesibles.

Impacto

Permite la extracción de información privada en caso de pérdida o robo del equipo.

Estrategias de mitigación

  • Cifrar todos los datos sensibles almacenados localmente.
  • Utilizar mecanismos de almacenamiento seguros del sistema operativo.
  • Evitar almacenar información sensible innecesaria.

Ejemplos de implementación

// Código vulnerable (Android)

val prefs = context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
prefs.edit().putString("user_password", "usuario123").apply() // ❌ Guardado sin cifrado

// Código seguro (Android)

val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
val securePrefs = EncryptedSharedPreferences.create(
  context, "secure_prefs", masterKey,
  EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
  EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("user_password", "usuario123").apply() // ✅ Almacenamiento cifrado

// Código vulnerable (Android)

val token = "jwt-token-aqui"
val prefs = context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
prefs.edit().putString("auth_token", token).apply() // ❌ Persistencia innecesaria

// Código seguro (Android)

val token = "jwt-token-aqui"
// ✅ Mantener en memoria mientras dure la sesión
val sessionManager = SessionManager()
sessionManager.setToken(token)

// Código vulnerable (Android)

val file = File(context.filesDir, "datos.txt")
file.writeText("nombre: Juan, DNI: 12345678") // ❌ Datos sensibles sin cifrar

// Código seguro (Android)

val plainText = "nombre: Juan, DNI: 12345678"
val secretKey = SecretKeySpec(keyBytes, "AES")
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val encrypted = cipher.doFinal(plainText.toByteArray())
File(context.filesDir, "datos.enc").writeBytes(encrypted) // ✅ Cifrado antes de guardar
10

Criptografía Insuficiente

Vector de ataque

Uso de algoritmos débiles o implementación incorrecta de funciones criptográficas.

Debilidad

Uso de claves mal generadas, algoritmos obsoletos o bibliotecas caseras.

Impacto

Permite a un atacante descifrar información o falsificar datos.

Estrategias de mitigación

  • Utilizar algoritmos criptográficos aprobados por estándares actuales.
  • Nunca implementar criptografía propia.
  • Actualizar regularmente las bibliotecas criptográficas utilizadas.

Ejemplos de implementación

// Código vulnerable (Android)

import java.security.MessageDigest

// ❌ Uso de MD5
val input = "password"
val md = MessageDigest.getInstance("MD5")
val digest = md.digest(input.toByteArray())
println("❌ Hash MD5: ${digest.joinToString("") { "%02x".format(it) }}")

// Código seguro (Android)

import java.security.MessageDigest

// ✅ Uso de SHA-256
val input = "password"
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(input.toByteArray())
println("✅ Hash SHA256: ${digest.joinToString("") { "%02x".format(it) }}")

// Código vulnerable (Android)

val secretKey = "HARDCODED_SECRET" // ❌ Clave embebida
println("❌ Usando clave embebida: $secretKey")

// Código seguro (Android)

val secretKey = BuildConfig.API_SECRET // ✅ Clave configurada por buildConfigField
println("✅ Clave segura desde configuración")

// Código vulnerable (Android)

val sharedPref = context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
sharedPref.edit().putString("auth_token", "token123").apply() // ❌ Guardado inseguro
println("❌ Token en SharedPreferences")

// Código seguro (Android)

val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
val encPrefs = EncryptedSharedPreferences.create(
    context, "secure_prefs", masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
encPrefs.edit().putString("auth_token", "token123").apply()
println("✅ Token cifrado en almacenamiento seguro")

Estadísticas de Seguridad en Aplicaciones

70%

de las 250 aplicaciones Android más populares filtran datos sensibles.

83%

de las aplicaciones tienen al menos una vulnerabilidad de seguridad.

70%

de las 100 principales aplicaciones financieras presentan al menos una vulnerabilidad crítica.