The Insecure Session Trap: How a Simple Android Flaw Exposes Millions of Users

Listen to this Post

Featured Image

Introduction:

A recent bug bounty discovery in an Android application highlights a critical yet often overlooked vulnerability: insecure session management. This flaw, where user sessions remain active after a password change, exposes a fundamental weakness in authentication logic that can lead to full account compromise.

Learning Objectives:

  • Understand the mechanics and risks of session management vulnerabilities.
  • Learn to identify and test for session validation flaws in mobile and web applications.
  • Implement secure coding practices and server-side controls to mitigate this risk.

You Should Know:

1. The Core of the Vulnerability: Session Persistence

At its heart, this vulnerability occurs because the application’s server does not invalidate existing authentication tokens (like session cookies or JWT tokens) upon a critical security event—in this case, a password change. An attacker who has previously stolen a session token can maintain access to the victim’s account even after the password has been changed, completely negating the security benefits of the password reset.

  1. Testing for Session Validation Flaws with Burp Suite
    The primary method for testing this vulnerability involves using a proxy tool like Burp Suite to intercept and replay requests.

Step-by-Step Guide:

  1. Login: Log in to the target application and capture the login request and response in Burp Proxy. Note the session cookie or authorization token in the `Cookie` or `Authorization` header.
  2. Change Password: While your session is active, use the application’s functionality to change your account password. This should generate a successful password change response.
  3. Replay Request: Without logging out, take a previous authenticated request (e.g., GET /api/v1/user/profile) and replay it in Burp Repeater using the old session token obtained in step 1.
  4. Analyze Result: If the request is successful and returns 200 OK with sensitive user data, the application is vulnerable. The server failed to terminate the active session after the password change.

3. Server-Side Mitigation: Invalidating Sessions in Node.js

The fix must be implemented server-side. Upon a password change request, the application must actively destroy all existing sessions for that user.

Verified Code Snippet (Node.js with Express session):

app.post('/api/change-password', async (req, res) => {
// 1. Authenticate user & validate current password
// 2. Hash new password and update in database
// 3. INVALIDATE ALL EXISTING SESSIONS FOR THIS USER
const userId = req.session.userId; // Get current user's ID

// Query the session store and destroy all sessions for this user
req.sessionStore.all((error, sessions) => {
if (error) { / handle error / }
for (const [sessionId, sessionData] of Object.entries(sessions)) {
if (sessionData.userId === userId) {
req.sessionStore.destroy(sessionId, (destroyErr) => {
if (destroyErr) { / log error / }
});
}
}
});

// 4. Send success response and force client-side logout
res.status(200).json({ message: "Password changed successfully. Please log in again." });
});

What this does: This code iterates through all active sessions in the server’s session store and destroys every session associated with the current user’s ID, effectively logging them out on all devices.

4. Advanced Testing: Manipulating JWT Tokens

Many modern apps use JSON Web Tokens (JWT) instead of traditional sessions. A JWT is often stateless, meaning the server does not store it. To invalidate them, developers must implement a token blacklist or use token versioning.

Testing Command to Decode a JWT:

echo -n "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" | base64 -d

Step-by-Step Guide: This command decodes the Base64Url-encoded payload of a JWT to reveal its contents (e.g., {"sub":"1234567890","name":"John Doe","iat":1516239022}). Check if the token contains a field like `passwordHash` or version. If it doesn’t, changing the password likely won’t affect the token’s validity, making it vulnerable.

5. Secure Implementation: JWT Token Versioning

The secure solution is to embed a version number in the JWT that is tied to the user’s password.

Verified Code Snippet (JWT Creation & Validation):

// Upon login or password change, increment a 'tokenVersion' for the user in the database
const jwt = require('jsonwebtoken');

// Generate a token after successful login
const token = jwt.sign(
{
userId: user.id,
tokenVersion: user.tokenVersion // This value is stored in the DB
},
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);

// Middleware to verify JWT on every request
const verifyToken = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[bash];
if (!token) return res.sendStatus(401);

jwt.verify(token, process.env.JWT_SECRET, async (err, decoded) => {
if (err) return res.sendStatus(403);
const user = await User.findById(decoded.userId);
// Check if the token version matches the current version in the database
if (user.tokenVersion !== decoded.tokenVersion) {
return res.sendStatus(401); // Token is invalidated
}
req.user = user;
next();
});
};

What this does: When a password is changed, the server increments the user’s `tokenVersion` in the database. Any subsequent request with an old JWT (containing the outdated tokenVersion) will fail validation, forcing the user to log in again and obtain a new token.

6. Android Client-Side Handling: Forcing Re-authentication

The client application must also handle the server’s invalidation command gracefully.

Verified Android Code Snippet (Kotlin):

// After a password change API call, handle a 401 Unauthorized response
interface ApiService {
@POST("change-password")
suspend fun changePassword(@Body request: ChangePasswordRequest): Response<Unit>
}

// In your ViewModel or Repository
try {
val response = apiService.changePassword(request)
if (response.isSuccessful) {
// Clear local tokens and navigate to login screen
authTokenManager.clearToken()
_uiState.value = UiState.Success("Password changed. Please log in.")
}
} catch (e: HttpException) {
if (e.code() == 401) {
// Server invalidated our session, force logout
authTokenManager.clearToken()
_uiState.value = UiState.Error("Session expired. Please log in again.")
}
}

What this does: This Kotlin code ensures the Android app reacts correctly to a successful password change or a session invalidation (401 error) by immediately clearing any locally stored tokens and redirecting the user to the login screen, providing a secure user experience.

7. Automated Testing with OWASP ZAP

Continuous security testing is crucial. The OWASP ZAP tool can be automated to test for this vulnerability.

Verified ZAP Baseline Scan Command:

docker run -v $(pwd):/zap/wrk/:rw -t owasp/zap2docker-stable zap-baseline.py \
-t https://your-test-app.com \
-c "auth_login_url=https://your-test-app.com/login \
auth_username="[email protected]" \
auth_password="oldpassword" \
auth_username_field="email" \
auth_password_field="password" \
auth_exclude_urls=".logout." \
post_change_password_url=https://your-test-app.com/change-password \
post_change_password_data='{"newPassword":"NewPass123!"}'"

Step-by-Step Guide: This command runs a baseline scan with authentication context. A skilled tester can write scripts for ZAP to automate the specific test flow: authenticate, change password, and then attempt to access a protected resource with the pre-change session to check for validity.

What Undercode Say:

  • This vulnerability is a classic example of a broken authentication mechanism, squarely placing it in the OWASP Top 10 category. Its prevalence is high because developers often focus on the core functionality of changing a password (updating the database) but neglect the crucial secondary security step of session termination.
  • The impact is severe: account takeover. If an attacker phishes a user’s session cookie or steals it via malware, the victim’s attempt to secure their account by changing their password is futile. The attacker retains access for the entire duration of the stolen session’s lifespan, which could be days or weeks.

The discovery underscores a critical gap in the secure development lifecycle (SDL). Security testing must include explicit checks for logic flaws around state-changing security events. Relying solely on automated DAST tools that check for common vulnerabilities like SQLi or XSS is insufficient; manual, logical testing is paramount. This bug is not about fancy exploitation chains; it’s about a fundamental misunderstanding of state management, making it both dangerous and embarrassingly common.

Prediction:

The persistence of such logical flaws will increasingly become a primary attack vector as automated tools and platform hardening make traditional vulnerabilities like SQL injection harder to exploit. We predict a rise in automated scripts specifically designed to hunt for these session management flaws at scale, scanning for applications that fail to invalidate sessions after password changes, 2FA enrollment, or email updates. Furthermore, as regulatory frameworks like GDPR and CCPA emphasize the “right to be forgotten” and secure data handling, companies failing to address these flaws will face not only reputational damage from breaches but also significant regulatory fines for inadequate security controls. This will push the adoption of secure-by-design frameworks and libraries that handle session invalidation automatically, moving the responsibility away from individual developers.

🎯Let’s Practice For Free:

IT/Security Reporter URL:

Reported By: Patilssahil Bugbounty – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow UndercodeTesting & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin | 🦋BlueSky