Recibir contenido: aceptar lo que otras aplicaciones te entregan
Aparecer en el selector de compartir de todo el dispositivo es la forma más barata de distribución que existe, y también la de convertirse en receptor de datos fabricados por cualquiera. Esta lección cubre el lado receptor completo: declarar los filtros que hacen visible a la aplicación en el selector, publicar destinos directos, atender la entrega tanto en el arranque como en una instancia ya viva, y leer el contenido por el único camino legítimo, que es el resolutor y nunca una ruta. La segunda mitad es una disciplina de desconfianza aplicada: el nombre es un dato del atacante, el tipo declarado es una afirmación sin respaldo, el tamaño puede faltar o mentir, la referencia puede apuntar de vuelta a tu propio proveedor, y la lectura debe estar acotada antes de empezar.
Todo el nivel ha mirado hacia fuera: cómo entregar contenido, cómo exponer un fichero, cómo pedir permiso al usuario para tocar lo ajeno, cómo publicar datos. Queda el movimiento contrario, que es a la vez el más rentable y el más peligroso. Declarar unas pocas líneas en el manifiesto hace que tu aplicación aparezca en el selector de compartir de todos los dispositivos donde esté instalada, junto a las aplicaciones de mensajería, de correo y de almacenamiento que llevan años ahí. Es distribución gratuita y es un canal de entrada permanente. Y a diferencia de casi cualquier otra superficie del sistema, no hay permiso que conceder, no hay usuario que autorice y no hay origen que verificar: llega un intent con una referencia dentro, y la única información fiable sobre él es que alguien pulsó algo en alguna parte. Todo lo demás, el nombre del fichero, su tipo, su tamaño, su contenido y hasta a quién pertenece realmente, son afirmaciones sin respaldo.
- Declarar los filtros que hacen aparecer la aplicación en el selector y publicar destinos directos.
- Atender la entrega tanto en la creación del componente como en una instancia ya existente.
- Leer contenido ajeno por el resolutor, copiándolo antes de que la concesión caduque.
- Aplicar una disciplina de validación al nombre, al tipo declarado, al tamaño y a la autoridad de la referencia.
Declarar que sabes recibir
Aparecer en el selector es cuestión de un filtro con la acción de compartir y los tipos que realmente sabes tratar. La tentación de declarar un comodín amplio es enorme y es un error de producto antes que de seguridad: la aplicación aparece en todos los envíos del sistema, incluidos aquellos con los que no puede hacer nada, y el usuario aprende rápido a ignorarla. Se declara lo que se sabe abrir, y para varios elementos hace falta el filtro de la acción múltiple, que es distinta.
<activity android:name=".ReceptorActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="application/pdf" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
Para subir a la fila superior del selector, la de los destinos directos, hace falta declarar un destino de compartición en el recurso de accesos directos y publicar accesos dinámicos de larga duración que representen conversaciones, proyectos o carpetas concretas. El sistema decide el orden con su propio modelo de uso, y lo único que está en tu mano es mantener esos accesos actualizados y con etiquetas que signifiquen algo para el usuario.
La entrega llega por dos caminos distintos según el estado de la aplicación, y atender solo el primero produce el fallo clásico: compartir funciona con la aplicación cerrada y no hace nada cuando ya estaba abierta. Con el modo de lanzamiento que evita duplicar instancias, el segundo intent llega a la instancia viva y hay que recogerlo explícitamente.
class ReceptorActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
atender(intent)
setContent { Pantalla() }
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
atender(intent) // la app ya estaba viva: nadie lo hace por ti
}
private fun atender(intent: Intent) = when (intent.action) {
Intent.ACTION_SEND -> importarUno(IntentCompat.getParcelableExtra(
intent, Intent.EXTRA_STREAM, Uri::class.java))
Intent.ACTION_SEND_MULTIPLE -> importarVarios(IntentCompat
.getParcelableArrayListExtra(intent, Intent.EXTRA_STREAM, Uri::class.java))
else -> Unit
}
}
Leer lo que llega, y hacerlo a tiempo
La referencia recibida se abre con el resolutor de contenidos y solo con él. No hay ruta que extraer, no hay fichero que construir y no hay atajo: intentar deducir una ruta a partir de la referencia funciona en algunos dispositivos, falla en la mayoría y no funciona nunca cuando el origen es un servicio remoto o un perfil de trabajo distinto.
El permiso que acompaña a la referencia es temporal y no está bajo tu control. Vive mientras viva la tarea que lo recibió, lo que significa que guardar la referencia en una base de datos para usarla mañana produce un fallo diferido que ninguna prueba corta detecta. Lo correcto es copiar el contenido a almacenamiento propio en el momento de recibirlo, y a partir de ahí trabajar con la copia.
private fun importar(uri: Uri): File? {
val destino = File(cacheDir, "importados/${UUID.randomUUID()}")
destino.parentFile?.mkdirs()
return runCatching {
contentResolver.openInputStream(uri)?.use { entrada ->
destino.outputStream().use { salida ->
// limite duro antes de empezar, no despues
entrada.copiarComoMucho(salida, limite = 50L * 1024 * 1024)
}
}
destino
}.getOrNull()
}
Los metadatos se consultan por las columnas estándar del resolutor, que devuelven el nombre visible y el tamaño. Ambos son opcionales y ambos vienen del proveedor de origen, de modo que son datos de entrada y no hechos comprobados.
flowchart TD
A[Otra app comparte contigo] --> B{La actividad ya existe}
B -->|no| C[onCreate con el intent inicial]
B -->|si| D[onNewIntent]
C --> E[Extraer la referencia del extra]
D --> E
E --> F{La autoridad es tuya}
F -->|si| G[Rechazar o tratar como caso especial]
F -->|no| H[Abrir con el resolutor]
H --> I[Copiar con limite de tamano]
I --> J[Comprobar el contenido real]
J --> K{Coincide con lo declarado}
K -->|no| L[Descartar e informar]
K -->|si| M[Guardar con nombre generado por ti]No creerte nada de lo que llega
El nombre visible es el vector más antiguo y el que más sobrevive. Viene del emisor, puede contener separadores de ruta, secuencias de subida de directorio, caracteres de control o miles de caracteres, y usarlo para construir la ruta de destino permite escribir fuera del directorio previsto. La defensa no es filtrar caracteres sino no usarlo como ruta: el nombre del fichero en tu almacenamiento lo generas tú, y el nombre recibido se guarda como una etiqueta más para mostrar, escapada al pintarla.
El tipo MIME declarado es una afirmación del proveedor de origen y no está verificada por nadie. Un envío puede anunciar una imagen y entregar un ejecutable, un documento con una carga útil o un archivo comprimido diseñado para agotar la memoria al descomprimirse. La comprobación se hace sobre el contenido real después de copiarlo, decodificando de forma controlada y descartando lo que no encaje.
El tamaño puede venir vacío, puede ser negativo y puede mentir. El flujo puede no terminar nunca, porque al otro lado hay un proveedor que genera contenido bajo demanda. Toda lectura de contenido ajeno debe llevar un límite duro impuesto antes de abrir el flujo y aplicado durante la copia, no una comprobación posterior sobre un fichero que ya llenó el disco.
Queda el caso más sutil, que es la referencia que apunta de vuelta a tu propio proveedor. Si tu actividad receptora copia ciegamente lo que le den, alguien puede enviarle una referencia a un fichero interno tuyo y conseguir que tú mismo lo leas con tus privilegios y lo dejes en un sitio del que él sí puede recuperarlo. La defensa es comprobar la autoridad de la referencia antes de abrirla y rechazar las que pertenecen a tu propia aplicación, además de nunca reenviar hacia fuera una referencia recibida sin haberla resuelto a contenido propio.
Declara lo que sabes abrir
Un filtro con comodín amplio te pone en todos los envíos y te convierte en ruido para el usuario.
Copia ahora
La concesión muere con la tarea: guardar la referencia para mañana es un fallo diferido garantizado.
El nombre es un dato
Lo genera el atacante: sirve para mostrar, jamás para construir una ruta de escritura.
Cuidado con tu reflejo
Una referencia a tu propio proveedor te convierte en el intermediario que roba tus propios ficheros.
Conviene ver el filtro de recepción por lo que es en términos de exposición. Al declararlo, tu aplicación pasa a ofrecer, a cualquier proceso instalado en el dispositivo y a cualquier contenido que llegue de internet a través de otra aplicación, un servicio de decodificación gratuito ejecutado con tus privilegios. Esa frase suena exagerada hasta que se enumera lo que hay realmente detrás de abrir un fichero recibido: decodificadores de imagen escritos en código nativo, analizadores de documentos, descompresores, bibliotecas de terceros que arrastran otras bibliotecas, y todo ello procesando bytes que un desconocido eligió con precisión. La mayor parte de las vulnerabilidades graves de la plataforma en la última década han vivido exactamente ahí, en el código que interpreta formatos, y la lección que se extrae no es que haya que auditar decodificadores sino que hay que reducir la superficie y contener el daño. Reducirla significa aceptar los formatos que el producto necesita y no los que la biblioteca soporta por casualidad. Contenerlo significa tratar la importación como una operación hostil por diseño: límites de tamaño impuestos antes de leer, tiempos máximos, decodificación con las opciones que permiten conocer las dimensiones antes de reservar memoria, procesamiento fuera del hilo principal y, cuando el formato es complejo de verdad, aislamiento en un proceso separado que pueda morir sin arrastrar a la aplicación. Y hay una asimetría que conviene interiorizar porque cambia las prioridades: el emisor de un envío elige el momento, el contenido, el tipo declarado, el nombre y el tamaño; el receptor no elige nada. En cualquier intercambio donde una parte controla todas las variables y la otra ninguna, la parte sin control solo tiene una estrategia razonable, que es asumir que cada entrada es un intento de romperla y diseñar para que romperla no sirva de nada. Aparecer en el selector de compartir sale gratis; seguir siendo seguro después de aparecer es el trabajo real.
- Declara los dos filtros, comparte un elemento y varios, y verifica que ambos caminos llegan a tu código.
- Comparte con la aplicación ya abierta sin atender la entrega en la instancia viva y reproduce el fallo de que no pasa nada.
- Guarda la referencia recibida en una base de datos, mátalo todo, vuelve a abrirla al día siguiente y anota la excepción exacta.
- Escribe una aplicación de prueba que comparta un fichero cuyo nombre visible contenga secuencias de subida de directorio y comprueba dónde acaba escribiendo tu receptor.
- Envíate una referencia que apunte a tu propio proveedor de ficheros y demuestra que tu receptor la resuelve con tus privilegios; después añade la comprobación de autoridad.