Skip to content

Login form: first keystroke after autofill is replaced by 0 in the password field #2989

Description

@davifabiano

Describe the bug

On the login form rendered by Tutor (#tutor-login-form), when the browser has autofilled the password field, the first keystroke the user types is replaced by the character 0 — regardless of which key is pressed.

Typing 123 over an autofilled password produces 023. Typing abc produces 0bc. It is always the first keystroke, and always 0.

The password field is not visible by default, so users cannot see this happening. They only discover it when the login fails.

Steps to reproduce

  1. Visit any page that renders the Tutor login form (e.g. a lesson page while logged out, or /dashboard/).
  2. Let Chrome autofill saved credentials for the site (native Chrome password manager, no extensions involved).
  3. Click into the password field and select all.
  4. Type any single character.
  5. Observe the field now contains 0 instead of the character typed.

Expected behavior

The typed character appears in the field.

Actual behavior

The first character is replaced by 0.

Root cause evidence

I instrumented the field's value setter and captured a stack trace on the offending write. Exactly one write occurs on that keystroke, and it comes from Alpine's bind path inside tutor-core.js:

at HTMLInputElement.set (instrumented setter)
at rv (https://<site>/wp-content/plugins/tutor/assets/js/tutor-core.js?ver=4.0.7:166:741)
at rp (https://<site>/wp-content/plugins/tutor/assets/js/tutor-core.js?ver=4.0.7:166:135)

The captured write had length: 1 and value 0.

In the minified bundle, rv contains this branch:

function rv(e, t) {
  if (rI(e)) { ... }
  else if (rR(e)) {
    if (Number.isInteger(t)) { e.value = t }        // <-- this branch
    else if (!Array.isArray(t) && typeof t !== "boolean" && ![null, void 0].includes(t)) { e.value = String(t) }
    else { ... e.checked ... }
  }
  ...
}

The password input reaches a branch intended for checkbox/radio inputs, and a numeric coercion writes an integer into a text/password field. No other code on the page writes to this input — I verified that by spying on every write to .value during the keystroke.

The markup involved

Rendered by Tutor's login form template:

<div class="tutor-input-field tutor-mb-8" x-data="{ show: false, value: '' }">
  <input :type="show ? 'text' : 'password'"
         class="tutor-form-control tutor-input"
         placeholder="Password"
         name="pwd"
         x-model="value"
         size="20" required
         autocomplete="off" />
</div>

Two things about this markup look relevant:

  1. x-model="value" on a field the browser fills means Alpine's internal state and the DOM value can diverge — Chrome's autofill does not always go through the events Alpine listens to.
  2. The eye toggle is gated on x-show="value.length > 0", i.e. on Alpine's state rather than the field's actual value. When the state is out of sync, the show-password control never appears, which is what hides this bug from users.

Environment

  • Tutor LMS 4.0.7 (latest at time of writing; wp plugin list reports no update available)
  • Tutor LMS Pro 4.0.7
  • Alpine 3.15.2 (as bundled in tutor-core.js)
  • WordPress 6.9.4, PHP 8.2.30
  • Chrome, native password manager, no password-manager extensions
  • Theme: Avada + Fusion Builder

Caveat, stated honestly: this was diagnosed on a production site that also runs WPML, Yoast, Autoptimize and SiteGround's optimizer. I have not reproduced it on a clean install. The stack trace points inside tutor-core.js with no other writer to the field, so I do not believe those plugins are involved — but I can't rule it out, and I'm happy to test a patch or gather more data if that helps.

Suggested direction

Whatever the exact branch, a text or password input should never receive a numeric coercion of its bound value. Guarding the Number.isInteger branch on the input actually being a checkbox/radio would prevent this class of bug.

Separately, gating the show-password control on value.length makes the field silently unusable whenever Alpine's state and the DOM diverge — reading the input's own value there would be more robust.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions