Resumen
En la app de Basecamp para Android (versiones 5.0.2 y 5.0.3, versionCode 473), la actividad com.basecamp.bc4.app.main.start.StartActivity estaba declarada con exported="true" y sin ningún atributo android:permission que restringiera quién podía abrirla. En Android eso implica que cualquier otra aplicación instalada en el mismo dispositivo —incluso una sin ningún permiso concedido— puede lanzarla mediante un Intent explícito.
El efecto es que, al lanzar esa actividad mientras el usuario tiene una sesión iniciada, la app rehace su secuencia de arranque, descarta el estado de navegación autenticado y devuelve al usuario a la pantalla de inicio de sesión. El investigador lo verificó en un dispositivo real (con capturas y vídeo): el usuario estaba dentro de la app y, tras disparar el intent, Basecamp mostró directamente la pantalla de acceso. Según el reporte, el comportamiento es fiable, se produce sin ningún aviso visible y puede repetirse una y otra vez.
HackerOne lo clasifica como control de acceso incorrecto (genérico), con severidad media. El programa pagó recompensa, aunque el importe no es público. El reporte se envió el 27 de mayo de 2026 y se hizo público el 4 de julio de 2026.
Pasos de reproducción
Requisitos previos: tener la app de Basecamp para Android instalada y con sesión iniciada en una cuenta válida. No hace falta ningún permiso especial en el dispositivo ni en la app que lanza el intent.
Con Basecamp abierto y con sesión activa, el investigador lanzó la actividad exportada con un intent explícito usando adb (que reproduce exactamente lo que podría hacer cualquier app instalada):
adb shell am start \
-n com.basecamp.bc3/com.basecamp.bc4.app.main.start.StartActivity \
--es launchDataUri "https://app.basecamp.com/6217076/projects/47432622"
Resultado observado: Basecamp pasa de inmediato a la pantalla de inicio de sesión; la sesión activa queda terminada.
Repitiendo esa misma llamada de forma periódica (en el reporte, cada 30 segundos en bucle), el usuario no logra mantener la sesión: cada vez que vuelve a autenticarse, la siguiente invocación lo expulsa de nuevo, dejando la app prácticamente inservible. El investigador también describe una app de demostración sin permisos que programa esa llamada con un Handler para que el ataque continúe aunque la app atacante se cierre; aquí no se reproduce su código completo.
Análisis del origen
La declaración en AndroidManifest.xml no incluye intent-filter ni permiso alguno, por lo que no hay un motivo legítimo para que la actividad sea accesible desde fuera:
<activity
android:name="com.basecamp.bc4.app.main.start.StartActivity"
android:exported="true"
android:launchMode="singleInstance"
android:windowSoftInputMode="adjustResize"/>
Cuando la actividad ya existe, el intent entrante llega a onNewIntent(), que lo pasa al gestor de navegación sin comprobar si hay una sesión que preservar:
public final void onNewIntent(Intent intent) {
kotlin.jvm.internal.k.f(intent, "intent");
super.onNewIntent(intent);
if (((m) this.f6857w.getValue()).a() == o.UpToDate) {
r();
}
m().b(intent); // NavigationManager.processIntent, no auth-state preservation
}
El método y() de MainActivity muestra lo que ocurre con el estado de sesión cuando StartActivity arranca de nuevo: destruye la MainActivity autenticada (finish()) y lanza StartActivity con la URI entrante, descartando por completo la pila de navegación de la sesión en curso:
public final void y() {
Intent intent = new Intent(this, (Class<?>) StartActivity.class);
intent.setData(getIntent().getData());
intent.putExtra("launchDataUri", getIntent().getDataString());
startActivity(intent);
finish(); // <- authenticated MainActivity is destroyed
}
El investigador señala que launchMode="singleInstance" agrava el problema: como la actividad ocupa su propia tarea y solo puede existir una instancia, un intent externo la trae al primer plano a través de onNewIntent, que vuelve a ejecutar la secuencia de arranque y fuerza la salida con independencia de que exista una sesión válida.
Impacto
El reporte describe una terminación forzada de sesión y una denegación de servicio sobre usuarios autenticados:
- Cualquier app coinstalada puede cerrar la sesión de cualquier usuario y devolverlo a la pantalla de acceso, tanto si Basecamp está en primer plano como en segundo.
- Al poder repetirse, se convierte en algo persistente: el usuario percibe la app como inestable ("no para de cerrarme la sesión") sin saber que la causa es otra app instalada. El reporte apunta incluso a que un competidor podría incrustar este intent en su propia app para degradar la experiencia de Basecamp en quien tenga ambas instaladas.
- No requiere interacción del usuario ni permisos en la app atacante: basta con llamar a
startActivity()con un componente explícito, algo que cualquier app puede hacer.
El investigador plantea además un posible impacto secundario: como el extra launchDataUri se reenvía a la navegación sin validar, sugiere que podría forzarse la navegación a una URL de descarga concreta y, si la cookie de sesión se reutiliza, intentar recuperar adjuntos privados. Este punto se presenta como hipótesis, no como algo verificado en el reporte.
Remediación
El investigador propone varias opciones, de menor a mayor alcance:
- Corrección inmediata: declarar
android:exported="false"enStartActivity. Al no tenerintent-filter, solo necesita ser accesible internamente. - Si el export fuera realmente necesario (por ejemplo, para comunicación entre procesos dentro del conjunto de apps de 37signals), restringirlo con un permiso a nivel de firma, de modo que solo apps firmadas con la clave de Basecamp puedan lanzarla:
<activity
android:name="com.basecamp.bc4.app.main.start.StartActivity"
android:exported="true"
android:permission="com.basecamp.bc3.INTERNAL_LAUNCH"
.../>
- Preservar la sesión: validar en
onNewIntent()que existe una sesión autenticada antes de destruirla. SiMainActivityya está en ejecución con una sesión válida, ignorar la petición externa o redirigirla aMainActivity.onNewIntent()en lugar de rehacer el arranque.
El reporte figura como resuelto (Resolved) en HackerOne.