Skip to content

fix: password bypass fix#7019

Open
Yaddalapalli-Charan-Kumar-Naidu wants to merge 1 commit intoRocketChat:developfrom
Yaddalapalli-Charan-Kumar-Naidu:fix/passcode-attempts-bypass
Open

fix: password bypass fix#7019
Yaddalapalli-Charan-Kumar-Naidu wants to merge 1 commit intoRocketChat:developfrom
Yaddalapalli-Charan-Kumar-Naidu:fix/passcode-attempts-bypass

Conversation

@Yaddalapalli-Charan-Kumar-Naidu
Copy link
Contributor

@Yaddalapalli-Charan-Kumar-Naidu Yaddalapalli-Charan-Kumar-Naidu commented Mar 1, 2026

Proposed changes

Fix a security bypass in the passcode lockout mechanism. The failed attempt counter was a plain local variable (let attempts = 0) that reset to 0 on every component remount. Since getItem was never called for ATTEMPTS_KEY, the persisted count in AsyncStorage was ignored on mount. This allowed a user to background/foreground the app repeatedly to reset the counter and gain unlimited passcode attempts.

Changes made:

  • Replace let attempts with useRef so the count survives re-renders within the same lifecycle
  • Destructure getItem from useAsyncStorage and call it in [readStorage()]to restore the persisted count on every mount
  • Move setAttempts() before the MAX_ATTEMPTS threshold check so AsyncStorage always holds the accurate count, even when the lockout-triggering attempt is made

Issue(s)

Closes #7018

How to test or reproduce

  1. Enable Passcode in the app (Profile → Security → Passcode)
  2. Open the passcode entry screen and enter the wrong passcode 5 times
  3. Background the app and reopen it before the 6th attempt
  4. Before fix: attempt counter resets to 0 — user gets 5 more tries. Repeat indefinitely.
  5. After fix: attempt counter restores to 5 from AsyncStorage — the next wrong attempt triggers lockout as expected

Affected screen: Passcode entry screen ([app/containers/Passcode/PasscodeEnter.tsx]

Screenshots

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the [CONTRIBUTING]
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

The root cause was two separate issues compounding each other:

  1. let attempts = 0 — a plain variable that resets every remount (should be a useRef)
  2. useAsyncStorage(ATTEMPTS_KEY) only destructured setItem, never getItem — so the persisted value was write-only and never read back

Summary by CodeRabbit

  • Bug Fixes
    • Fixed passcode lockout mechanism to persist failed attempts across app sessions.
    • Improved failed attempt tracking and threshold enforcement for account security.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

Walkthrough

This change fixes a security vulnerability where the passcode attempt counter reset on component remount, enabling users to indefinitely bypass the lockout threshold. The fix adds AsyncStorage readback on mount and replaces the transient attempts variable with a persistent ref to maintain state across remounts.

Changes

Cohort / File(s) Summary
Passcode Attempt Persistence
app/containers/Passcode/PasscodeEnter.tsx
Replaced local attempts variable with attemptsRef and added AsyncStorage getItem call to load persisted attempts on mount. Updated onEndProcess to increment, persist, and evaluate attempts against lockout threshold, transitioning to LOCKED state when exceeded.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested labels

type: bug, area: authentication

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: password bypass fix' addresses the main security issue (passcode lockout bypass) but uses generic phrasing ('password bypass fix') that could be more descriptive of the specific root cause fix.
Linked Issues check ✅ Passed All three key objectives from issue #7018 are met: replacing the local attempts variable with useRef for persistence, reading AsyncStorage via getItem on mount, and persisting attempt count before lockout threshold check.
Out of Scope Changes check ✅ Passed All code changes in PasscodeEnter.tsx are directly scoped to fixing the identified security bypass and do not introduce unrelated functionality or modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/containers/Passcode/PasscodeEnter.tsx (1)

41-49: ⚠️ Potential issue | 🟠 Major

Reset in-memory attempts after lock expiry and sanitize parsed storage value.

When diff <= 1, storage is reset but attemptsRef.current is not, so the next wrong attempt can immediately re-lock. Also, invalid stored values can become NaN and break threshold checks.

Proposed fix
 const readStorage = async () => {
-  const stored = await getAttempts();
-  attemptsRef.current = parseInt(stored || '0', 10);
+  const stored = await getAttempts();
+  const parsedAttempts = Number.parseInt(stored ?? '0', 10);
+  attemptsRef.current = Number.isFinite(parsedAttempts) && parsedAttempts >= 0 ? parsedAttempts : 0;
 
   lockedUntil = await getLockedUntil();
   if (lockedUntil) {
     const diff = getDiff(lockedUntil);
     if (diff <= 1) {
       await resetAttempts();
+      attemptsRef.current = 0;
       setStatus(TYPE.ENTER);
     } else {
       setStatus(TYPE.LOCKED);
     }
   } else {
     setStatus(TYPE.ENTER);
   }
   biometry();
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/Passcode/PasscodeEnter.tsx` around lines 41 - 49, When
handling stored attempt and lock expiry in PasscodeEnter.tsx, sanitize the
parsed value from getAttempts by using a safe fallback if parseInt returns NaN
(e.g., treat non-numeric stored as 0) and, when getLockedUntil() indicates
expiry (getDiff(lockedUntil) <= 1), also reset the in-memory counter
attemptsRef.current to 0 after calling resetAttempts() and before
setStatus(TYPE.ENTER) so the next wrong attempt doesn't immediately re-lock;
update logic around getAttempts, attemptsRef.current, getLockedUntil, getDiff,
resetAttempts and setStatus accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/containers/Passcode/PasscodeEnter.tsx`:
- Around line 41-49: When handling stored attempt and lock expiry in
PasscodeEnter.tsx, sanitize the parsed value from getAttempts by using a safe
fallback if parseInt returns NaN (e.g., treat non-numeric stored as 0) and, when
getLockedUntil() indicates expiry (getDiff(lockedUntil) <= 1), also reset the
in-memory counter attemptsRef.current to 0 after calling resetAttempts() and
before setStatus(TYPE.ENTER) so the next wrong attempt doesn't immediately
re-lock; update logic around getAttempts, attemptsRef.current, getLockedUntil,
getDiff, resetAttempts and setStatus accordingly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 566e100 and ee6f652.

📒 Files selected for processing (1)
  • app/containers/Passcode/PasscodeEnter.tsx
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (1)
app/containers/Passcode/PasscodeEnter.tsx (1)
app/lib/methods/userPreferences.ts (1)
  • useUserPreferences (63-90)
🔇 Additional comments (2)
app/containers/Passcode/PasscodeEnter.tsx (2)

24-29: Good fix for remount-based attempt reset.

Using attemptsRef plus getItem is the right direction and closes the core bypass described in the issue.


68-75: ⚠️ Potential issue | 🟠 Major

Await AsyncStorage writes before finalizing lockout state.

setAttempts(...) and setLockedUntil(...) are fire-and-forget calls (lines 71, 74). These are async operations that may not complete before the function returns. If either write is delayed or fails, subsequent reads will observe incomplete lockout state.

Make the setTimeout callback async and await both AsyncStorage writes before transitioning to the LOCKED state:

Example fix
 const onEndProcess = (p: string) => {
-  setTimeout(() => {
+  setTimeout(async () => {
     if (sha256(p) === passcode) {
       finishProcess();
     } else {
       attemptsRef.current += 1;
-      // Always persist the updated count BEFORE checking threshold,
-      // so AsyncStorage is accurate even when lockout triggers.
-      setAttempts(attemptsRef.current.toString());
+      await setAttempts(attemptsRef.current.toString());
       if (attemptsRef.current >= MAX_ATTEMPTS) {
+        await setLockedUntil(new Date().toISOString());
         setStatus(TYPE.LOCKED);
-        setLockedUntil(new Date().toISOString());
         Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
       } else {
         ref?.current?.wrongPasscode();
         Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
       }
     }
   }, 200);
 };

Likely an incorrect or invalid review comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Passcode attempt counter resets on remount lockout can be bypassed indefinitely

1 participant