|
7 | 7 |
|
8 | 8 | ## Resultado |
9 | 9 |
|
10 | | -| Severidad | Encontrados | Resueltos | Aceptados | |
11 | | -|-----------|-------------|-----------|-----------| |
12 | | -| CRÍTICO | 8 | 8 | 0 | |
13 | | -| ALTO | 14 | 12 | 2 | |
14 | | -| MEDIO | 8 | 8 | 0 | |
15 | | -| BAJO | 7 | 6 | 1 | |
| 10 | +| Severidad | Encontrados | Resueltos | Aceptados | Pendientes | |
| 11 | +|-----------|-------------|-----------|-----------|------------| |
| 12 | +| CRÍTICO | 8 | 8 | 0 | 0 | |
| 13 | +| ALTO | 17 | 15 | 2 | 0 | |
| 14 | +| MEDIO | 8 | 8 | 0 | 0 | |
| 15 | +| BAJO | 7 | 6 | 1 | 0 | |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## Hallazgos resueltos — Segunda auditoría 2026-07-29 |
| 20 | + |
| 21 | +### N-1 · `HelmMusicService` exportado sin permiso de bind ✅ resuelto en 9f947a3 |
| 22 | +**Archivos:** `app/src/main/AndroidManifest.xml:84-91` · `audio/src/main/kotlin/dev/helm/audio/HelmMusicService.kt:41-45` |
| 23 | +**Severidad:** Alta |
| 24 | + |
| 25 | +`HelmMusicService` tiene `android:exported="true"` sin `android:permission` en el `<service>`. El guard en `onGetSession` compara `controllerInfo.packageName == packageName`, pero ese valor lo suministra el caller vía `connectionHints` — no está verificado por el sistema. Cualquier app puede hacer bind y controlar la reproducción o inyectar URIs al queue de ExoPlayer. |
| 26 | + |
| 27 | +**Fix:** |
| 28 | +```xml |
| 29 | +<service |
| 30 | + android:name=".HelmMusicService" |
| 31 | + android:permission="android.permission.MEDIA_CONTENT_CONTROL" |
| 32 | + android:exported="true"> |
| 33 | +``` |
| 34 | +En `onGetSession`: confiar solo en `controllerInfo.isTrusted`, eliminar la comparación por `packageName`. |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +### N-2 · OTA — URL de descarga sin validación de host/scheme ✅ resuelto en 36e5b05 |
| 39 | +**Archivo:** `ota/src/main/kotlin/dev/helm/ota/OtaRepository.kt:45-56` |
| 40 | +**Severidad:** Alta |
| 41 | + |
| 42 | +`browser_download_url` de la respuesta JSON de GitHub se pasa verbatim a `URL(apkUrl).openConnection()` sin verificar scheme ni host. `HttpURLConnection` sigue redirects por defecto. Con cuenta de GitHub comprometida o DNS poisoning se puede redirigir la descarga a un servidor arbitrario. |
| 43 | + |
| 44 | +**Fix:** |
| 45 | +```kotlin |
| 46 | +val parsed = URL(apkUrl) |
| 47 | +require(parsed.protocol == "https") { "Scheme inválido" } |
| 48 | +require( |
| 49 | + parsed.host.endsWith(".github.com") || |
| 50 | + parsed.host.endsWith(".githubusercontent.com") |
| 51 | +) { "Host no permitido" } |
| 52 | +conn.instanceFollowRedirects = false |
| 53 | +``` |
| 54 | + |
| 55 | +--- |
| 56 | + |
| 57 | +### N-3 · OTA — Comparación de firma APK con lógica `any/any` ✅ resuelto en 752900b |
| 58 | +**Archivo:** `ota/src/main/kotlin/dev/helm/ota/OtaRepository.kt:102-104` |
| 59 | +**Severidad:** Alta |
| 60 | + |
| 61 | +La verificación usa `apkCerts.any { apk -> installedCerts.any { ... } }` — pasa si el APK descargado comparte **al menos un** certificado con el instalado. Si se rota la clave de firma y la anterior queda comprometida, un APK firmado con esa clave vieja seguiría pasando el check. Agravado por N-2. |
| 62 | + |
| 63 | +**Fix:** |
| 64 | +```kotlin |
| 65 | +// Igualdad estricta de conjuntos |
| 66 | +require( |
| 67 | + apkCerts.map { it.toCharsString() }.toSet() == |
| 68 | + installedCerts.map { it.toCharsString() }.toSet() |
| 69 | +) |
| 70 | +``` |
| 71 | +Opción más robusta: comparar contra un fingerprint SHA-256 hardcodeado del cert de release, eliminando dependencia del runtime. |
16 | 72 |
|
17 | 73 | --- |
18 | 74 |
|
|
0 commit comments