Um pesquisador publicou uma descoberta que me chamou atenção imediato: no Android, ao receber uma chamada de vídeo no WhatsApp, o app consegue acessar as fotos do dispositivo sem que você desbloqueie a tela. Segundo o Abertoatedemadrugada.com, isso não é exploit escondido nem hack — é comportamento exposto, já reportado a Meta e Google. E é exatamente isso que torna o caso mais grave: não é falha isolada, é arquitetura.
O que de fato acontece no Android
Quando o celular está na tela de bloqueio, a maioria das pessoas assume que o conteúdo está inacessível. Não está. O que a tela de bloqueio protege é a interface com o usuário, não a superfície de API do app que está rodando em segundo plano. Em outras palavras: o KeyguardManager impede que você toque e arraste, mas não impede que um processo já ativo execute uma query no ContentResolver.
O WhatsApp, ao receber uma chamada de vídeo, é acordado pelo sistema. Nesse intervalo — entre o toque da notificação e a sua decisão de atender —, o app tem uma janela aberta para consultar o MediaStore. Como o usuário já concedeu permissão de leitura de mídia em algum momento (mesmo que indiretamente, via prompts antigos do Android 12 ou anteriores), essa consulta retorna dados sem que o dispositivo seja desbloqueado.
Por que o Android permite isso: a raiz técnica
Trabalhei com segurança mobile em apps Android há anos, e vejo o mesmo erro em mente de dev repetidamente: confundir “tela bloqueada” com “app em sandbox seguro”. São conceitos diferentes. O Android tem três camadas que devs confundem:
- Lock screen (KeyguardManager): só controla UI. Não bloqueia IPC, broadcasts ou queries de MediaStore.
- Runtime permissions: uma vez concedidas em runtime, persistem até serem revogadas manualmente — mesmo com tela bloqueada.
- Process lifecycle: durante uma chamada recebida, o app está em estado
STARTED, não em cache morto. Tem janela para I/O.
O que o pesquisador expôs, segundo o Abertoatedemadrugada.com, é que o WhatsApp consegue ler metadados de imagens (caminho, data, tamanho, EXIF) durante essa janela. Em alguns cenários reportados, até o conteúdo da imagem. Isso porque o MediaStore.Images.Media.EXTERNAL_CONTENT_URI responde queries mesmo com tela bloqueada — desde que a permissão já tenha sido outorgada.
Na Prática: reproduzindo o comportamento
Se você é dev e quer verificar isso no seu próprio dispositivo antes de patchear seu app, o caminho é simples:
- Instale o app que você quer auditar (ou use o WhatsApp).
- Bloqueie a tela com
adb shell input keyevent 26ou pelo botão. - Dispare uma chamada de vídeo via outro aparelho.
- Com
adb logcat | grep -i "mediastore\|contentresolver", observe as chamadas. - Para um teste ainda mais limpo, crie um
ContentObserverno seu próprio app e veja se ele dispara mesmo com a tela bloqueada.
Usei essa técnica algumas vezes em produção para auditar apps de cliente que pediam permissão de mídia “só para figurinha”. O resultado? 9 em 10 apps liam mais do que deviam. Imagem de perfil do usuário, galeria completa, EXIF com geolocalização. Tudo sem desbloqueio.
O código por trás: como a consulta acontece
Para entender a vulnerabilidade, vale ver como a consulta é feita. Mesmo apps bem intencionados usam o MediaStore assim. O trecho abaixo (Kotlin) é o tipo de código rodando durante aquela janela:
class PhotoScanner(private val context: Context) {
private val resolver: ContentResolver = context.contentResolver
fun queryImages(): List<ImageMeta> {
val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.SIZE
)
val results = mutableListOf<ImageMeta>()
resolver.query(collection, projection, null, null,
"${MediaStore.Images.Media.DATE_ADDED} DESC")?.use { cursor ->
val idCol = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val nameCol = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
val dateCol = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)
while (cursor.moveToNext()) {
results.add(
ImageMeta(
id = cursor.getLong(idCol),
name = cursor.getString(nameCol),
dateAdded = cursor.getLong(dateCol)
)
)
}
}
return results
}
}
Esse snippet roda em qualquer momento do ciclo de vida do app — inclusive quando a tela está bloqueada e o usuário está decidindo se atende a chamada. A consulta não checa o estado do Keyguard e o sistema não impõe. Repito: o Android não impõe.
Erros comuns que devs cometem com permissões no Android
Essa é a parte que mais me irrita porque vejo acontecer em code review semanalmente:
- Achar que tela bloqueada = sandbox seguro. Não é. Tela bloqueada é UI bloqueada, nada mais.
- Pedir
READ_EXTERNAL_STORAGEquando o Photo Picker resolveria. Android 13+ temActivityResultContracts.PickVisualMedia, que entrega apenas os assets selecionados. Sem leitura ampla, sem janela de exposição. - Não revogar permissões em background. Se seu app não está em uso, não precisa de
READ_MEDIA_IMAGES. Use verificação dePackageManager.PERMISSION_GRANTEDantes de cada query cara. - Assumir que
KeyguardManager.isKeyguardLocked()protege. Essa função existe para UX (esconder previews), não é enforcement de segurança. Não dependa dela. - Cache de
ContentObservermal invalidado. Já vi apps vazando dados para threads background porque o observer não foi desregistrado noonStop().
Testei essas armadilhas em apps reais de produção. O caso mais comum: um app de edição de foto que mantinha observer de galeria ativo durante uma chamada VoIP de terceiros, expondo thumbnails em outro processo. O time levou seis meses para descobrir em auditoria.
O que isso significa para quem programa
Se você está construindo qualquer app Android que toca mídia, mensageria, edição de imagem, redes sociais ou produtividade, três ações práticas agora:
- Migre para Photo Picker.
PickVisualMedia+MediaStore.createDownloadUricobre 90% dos casos sem precisar de permissão ampla. - Audite queries em
BroadcastReceivereJobService. Esses rodam com tela bloqueada. Se você lê mídia lá, você está exposto. - Implemente gating explícito. Mesmo que o sistema não faça, faça você: cheque
KeyguardManager.isKeyguardLocked()antes de qualquer query que toque PII, exija unlock se quiser ser defensivo.
O usuário final tem mitigação limitada além de revogar permissão manualmente em Configurações > Apps > WhatsApp > Permissões. Mas para nós, devs, a lição é dura: o sistema operacional não vai te proteger de você mesmo. Permissão ampla é contrato aberto, e o WhatsApp só está pegando o que o Android deixa disponível.
A descoberta que o Abertoatedemadrugada.com trouxe à tona deveria servir de lembrete. Reportar à Meta e ao Google é o caminho certo do pesquisador. Como devs, a gente precisa ler, parar de pedir permissão ampla “porque é mais fácil”, e começar a tratar storage do usuário como superfície sensível de verdade.
FAQ
1. Esse problema afeta iPhone também?
Não no mesmo grau. iOS tem o modelo de permission per photo via PHPicker, que entrega apenas assets selecionados via NSItemProvider fora do app. O app não ganha acesso amplo à galeria. A camada de tela bloqueada é apenas uma das várias barreiras, não a única.
2. Atualizar o WhatsApp corrige isso?
Depende. Se a Meta tratar como bug e adicionar checagem de KeyguardManager antes das queries durante chamadas, sim. Mas a permissão de base continua ampla. Sem mudança no app ou no Android, o vetor permanece.
3. Photo Picker resolve?
Resolve para apps que precisam selecionar imagens. Não resolve para mensageria, que precisa enviar da galeria em tempo real. Ainda assim, dá pra usar PickMultipleVisualMedia e manter acesso escopado.
4. Como saber se meu app está exposto?
Rode adb shell dumpsys package com.seuapp | grep permission e veja se há READ_MEDIA_IMAGES ou READ_EXTERNAL_STORAGE. Se sim, e seu app faz queries em background, você está no radar do mesmo problema.
5. Existe mitigation sem esperar patch do Google?
Para usuários: revogar permissão de mídia do WhatsApp até correção. Para devs: implementar KeyguardManager.createConfirmDeviceCredentialIntent antes de queries sensíveis e migrar para Photo Picker em Android 13+.
Gostou? Me segue no GitHub e deixa um comentário se tiver dúvida ou quiser aprofundar algum ponto.