Skip to content

Commit 42b4e11

Browse files
authored
[SDK-165] Fix user-controlled bypass of sensitive method (GSRR-709) (#957)
* Fix. * Fixes * Revert * Fix. * Revert * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Add tests * Add tests * Add tests * Add tests * Add tests * Add tests * Add tests
1 parent 5ca0f6b commit 42b4e11

File tree

4 files changed

+432
-6
lines changed

4 files changed

+432
-6
lines changed

iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,10 +420,34 @@ private void onLogin(
420420
}
421421

422422
private void completeUserLogin() {
423+
completeUserLogin(_email, _userId, _authToken);
424+
}
425+
426+
/**
427+
* Completes user login with validated credentials.
428+
* This method ensures sensitive operations (syncInApp, syncMessages, registerForPush) only execute
429+
* with server-validated user data, preventing user-controlled bypass attacks.
430+
*
431+
* @param email Server-validated email (can be null)
432+
* @param userId Server-validated userId (can be null)
433+
* @param authToken Server-validated authToken (must not be null for sensitive operations when JWT auth is enabled)
434+
*/
435+
private void completeUserLogin(@Nullable String email, @Nullable String userId, @Nullable String authToken) {
423436
if (!isInitialized()) {
424437
return;
425438
}
426439

440+
// Only enforce authToken requirement when JWT auth is enabled
441+
// This prevents user-controlled bypass where unvalidated userId/email from keychain
442+
// could be used to access another user's data in JWT auth scenarios
443+
if (config.authHandler != null && authToken == null) {
444+
IterableLogger.w(TAG, "Cannot complete user login - JWT auth enabled but no validated authToken present");
445+
if (_setUserFailureCallbackHandler != null) {
446+
_setUserFailureCallbackHandler.onFailure("JWT authentication is enabled but no valid authToken is available", null);
447+
}
448+
return;
449+
}
450+
427451
if (config.autoPushRegistration) {
428452
registerForPush();
429453
} else if (_setUserSuccessCallbackHandler != null) {
@@ -502,10 +526,45 @@ private String getDeviceId() {
502526
return _deviceId;
503527
}
504528

529+
/**
530+
* Completion handler interface for storeAuthData operations.
531+
* Receives the exact credentials that were stored to keychain.
532+
*/
533+
private interface AuthDataStorageHandler {
534+
void onAuthDataStored(String email, String userId, String authToken);
535+
}
536+
505537
private void storeAuthData() {
538+
storeAuthData(null);
539+
}
540+
541+
/**
542+
* Stores auth data and optionally invokes completion handler with the stored credentials.
543+
*
544+
* SECURITY - TOCTOU Protection:
545+
* When a completion handler is provided, it receives the exact credentials that were stored
546+
* to keychain. This prevents TOCTOU (Time-Of-Check-Time-Of-Use) attacks where:
547+
* 1. Credentials are stored to keychain
548+
* 2. Attacker modifies keychain (between store and read)
549+
* 3. Sensitive operations use tampered credentials
550+
*
551+
* By capturing credentials BEFORE storage and passing them directly via completion handler,
552+
* we ensure completeUserLogin uses exactly what was stored, not what's currently in keychain.
553+
*
554+
* @param completionHandler Optional handler invoked synchronously after storage with the stored credentials
555+
*/
556+
private void storeAuthData(AuthDataStorageHandler completionHandler) {
506557
if (_applicationContext == null) {
507558
return;
508559
}
560+
561+
// SECURITY: Capture current instance field values BEFORE storing to keychain.
562+
// These captured values will be passed to completion handler, ensuring the caller
563+
// receives exactly what was stored, not potentially modified keychain data.
564+
final String storedEmail = _email;
565+
final String storedUserId = _userId;
566+
final String storedAuthToken = _authToken;
567+
509568
IterableKeychain iterableKeychain = getKeychain();
510569
if (iterableKeychain != null) {
511570
iterableKeychain.saveEmail(_email);
@@ -515,6 +574,11 @@ private void storeAuthData() {
515574
} else {
516575
IterableLogger.e(TAG, "Shared preference creation failed. ");
517576
}
577+
578+
// Invoke completion handler with the captured credentials
579+
if (completionHandler != null) {
580+
completionHandler.onAuthDataStored(storedEmail, storedUserId, storedAuthToken);
581+
}
518582
}
519583

520584
private void retrieveEmailAndUserId() {
@@ -595,10 +659,14 @@ void setAuthToken(String authToken, boolean bypassAuth) {
595659
if (isInitialized()) {
596660
if ((authToken != null && !authToken.equalsIgnoreCase(_authToken)) || (_authToken != null && !_authToken.equalsIgnoreCase(authToken))) {
597661
_authToken = authToken;
598-
storeAuthData();
599-
completeUserLogin();
662+
// SECURITY: Use completion handler to atomically store and pass validated credentials.
663+
// The completion handler receives exact values stored to keychain, preventing TOCTOU
664+
// attacks where keychain could be modified between storage and completeUserLogin execution.
665+
storeAuthData((email, userId, token) -> completeUserLogin(email, userId, token));
600666
} else if (bypassAuth) {
601-
completeUserLogin();
667+
// SECURITY: Pass current credentials directly to completeUserLogin.
668+
// completeUserLogin will validate authToken presence when JWT auth is enabled.
669+
completeUserLogin(_email, _userId, _authToken);
602670
}
603671
}
604672
}

0 commit comments

Comments
 (0)