mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-18 21:40:41 -04:00
Update browser extension form detector to better detect password fields (#2016)
This commit is contained in:
committed by
Leendert de Borst
parent
ae9f151f14
commit
8ddc2e15e1
@@ -77,7 +77,10 @@ export const EnglishFieldPatterns: FieldPatterns = {
|
||||
lastName: { include: ['lastname', 'last-name', 'last_name', 'lname', 'surname', 'family-name'] },
|
||||
email: { include: ['email', 'mail', 'emailaddress'] },
|
||||
emailConfirm: { include: ['confirm', 'verification', 'repeat', 'retype', 'verify', 'email2'] },
|
||||
password: { include: ['password', 'pwd', 'pass'] },
|
||||
password: {
|
||||
include: ['password', 'pwd', 'pass'],
|
||||
exclude: ['otp', 'totp', 'tfa', '2fa', 'mfa', 'twofa', 'authenticator', 'one-time', 'onetime', 'verification-code', 'verificationcode', 'two-factor', 'second-factor', 'auth-code', 'security-code', 'six-digit']
|
||||
},
|
||||
birthdate: { include: ['birthdate', 'birth-date', 'dob', 'date-of-birth'] },
|
||||
gender: { include: ['gender', 'sex'] },
|
||||
birthDateDay: { include: ['-day', 'birthdate_d', 'birthdayday', '_day', 'day'] },
|
||||
|
||||
@@ -720,11 +720,18 @@ export class FormDetector {
|
||||
}
|
||||
}
|
||||
|
||||
// If email type is explicitly requested, prefer actual <input type="email">
|
||||
if (types.includes('email') && type === 'email') {
|
||||
matches.push({ input: input as HTMLInputElement, score: -1 });
|
||||
continue;
|
||||
}
|
||||
|
||||
// If password type is explicitly requested, prefer actual <input type="password">
|
||||
if (types.includes('password') && type === 'password') {
|
||||
matches.push({ input: input as HTMLInputElement, score: -1 });
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check autocomplete attribute for direct field type matching.
|
||||
* First check our custom data-av-autocomplete attribute (set by AliasVault when disabling
|
||||
@@ -1372,7 +1379,7 @@ export class FormDetector {
|
||||
* Find a TOTP/2FA input field in the form.
|
||||
* Uses pattern matching and heuristics specific to TOTP fields.
|
||||
*/
|
||||
private findTotpField(form: HTMLFormElement | null): HTMLInputElement | null {
|
||||
private findTotpField(form: HTMLFormElement | null, excludeElements: HTMLInputElement[] = []): HTMLInputElement | null {
|
||||
// Check if this is an email verification form (not TOTP/2FA)
|
||||
if (this.isEmailVerificationForm()) {
|
||||
return null;
|
||||
@@ -1382,7 +1389,8 @@ export class FormDetector {
|
||||
const candidates = this.findAllInputFields(
|
||||
form,
|
||||
CombinedFieldPatterns.totp,
|
||||
['text', 'number']
|
||||
['text', 'number', 'password'],
|
||||
excludeElements
|
||||
);
|
||||
|
||||
// Filter out parent-child duplicates
|
||||
@@ -1607,6 +1615,11 @@ export class FormDetector {
|
||||
if (passwordFields.confirm) {
|
||||
detectedFields.push(passwordFields.confirm);
|
||||
}
|
||||
|
||||
const totpField = this.findTotpField(wrapper as HTMLFormElement | null, detectedFields);
|
||||
if (totpField) {
|
||||
detectedFields.push(totpField);
|
||||
}
|
||||
|
||||
const usernameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.username, ['text'], detectedFields, checkVisibility);
|
||||
if (usernameField) {
|
||||
@@ -1657,11 +1670,6 @@ export class FormDetector {
|
||||
detectedFields.push(genderField.field as HTMLInputElement);
|
||||
}
|
||||
|
||||
const totpField = this.findTotpField(wrapper as HTMLFormElement | null);
|
||||
if (totpField) {
|
||||
detectedFields.push(totpField);
|
||||
}
|
||||
|
||||
return {
|
||||
form: wrapper as HTMLFormElement,
|
||||
emailField: emailFields.primary,
|
||||
|
||||
@@ -259,4 +259,96 @@ describe('FormDetector English tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* Regression: a field declared as <input type="password"> must be detected
|
||||
* as a password field even when its id/name/placeholder don't include the
|
||||
* strings "password"/"pwd"/"pass" (e.g. intranet forms using "pin",
|
||||
* "loginPin", etc.). The HTML standard type attribute is an unambiguous
|
||||
* signal that should be respected.
|
||||
*/
|
||||
describe('English login form with type="password" but no password keyword in id/name', () => {
|
||||
const htmlFile = 'en-login-form-typed-password.html';
|
||||
|
||||
testField(FormField.Username, 'loginUser', htmlFile);
|
||||
testField(FormField.Password, 'loginPin', htmlFile);
|
||||
|
||||
it('should detect the form as a login form', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
const formDetector = new FormDetector(document);
|
||||
expect(formDetector.containsLoginForm()).toBe(true);
|
||||
});
|
||||
|
||||
it('should trigger autofill when clicking the type="password" field', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
|
||||
const pinInput = document.getElementById('loginPin');
|
||||
const formDetector = new FormDetector(document, pinInput as HTMLElement);
|
||||
|
||||
expect(formDetector.isAutofillTriggerableField()).toBe(true);
|
||||
});
|
||||
|
||||
it('should expose the type="password" field as the primary password field', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
|
||||
const pinInput = document.getElementById('loginPin');
|
||||
const formDetector = new FormDetector(document, pinInput as HTMLElement);
|
||||
const form = formDetector.getForm();
|
||||
|
||||
expect(form).toBeTruthy();
|
||||
expect(form?.passwordField).toBeTruthy();
|
||||
expect(form?.passwordField?.id).toBe('loginPin');
|
||||
expect(form?.passwordField?.type).toBe('password');
|
||||
expect(form?.passwordConfirmField).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* Counterpart to the test above: when type="password" is (mis)used to mask
|
||||
* an OTP/2FA input, the password detector must NOT classify it as a
|
||||
* credential password (otherwise the master password would be autofilled
|
||||
* into a TOTP field), and the TOTP detector should pick it up so the
|
||||
* user still gets a meaningful autofill experience.
|
||||
*/
|
||||
describe('English form with type="password" masked TOTP input', () => {
|
||||
const htmlFile = 'en-totp-masked-as-password.html';
|
||||
|
||||
it('should NOT classify the type="password" OTP field as a credential password', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
|
||||
const otpInput = document.getElementById('otp');
|
||||
const formDetector = new FormDetector(document, otpInput as HTMLElement);
|
||||
const form = formDetector.getForm();
|
||||
|
||||
expect(form?.passwordField).toBeFalsy();
|
||||
expect(form?.passwordConfirmField).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should classify the type="password" OTP field as a TOTP field', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
|
||||
const otpInput = document.getElementById('otp');
|
||||
const formDetector = new FormDetector(document, otpInput as HTMLElement);
|
||||
const form = formDetector.getForm();
|
||||
|
||||
expect(form?.totpField).toBeTruthy();
|
||||
expect(form?.totpField?.id).toBe('otp');
|
||||
expect(form?.totpField?.type).toBe('password');
|
||||
});
|
||||
|
||||
it('should trigger autofill when clicking the masked OTP field', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
|
||||
const otpInput = document.getElementById('otp');
|
||||
const formDetector = new FormDetector(document, otpInput as HTMLElement);
|
||||
|
||||
expect(formDetector.isAutofillTriggerableField()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
Login form with a password field whose id/name/placeholder do NOT contain
|
||||
the strings "password", "pwd" or "pass". The HTML standard type="password"
|
||||
attribute is the only signal that this is a password input.
|
||||
-->
|
||||
<html>
|
||||
<body>
|
||||
<form id="login-form" action="/login" method="post">
|
||||
<div>
|
||||
<label for="loginUser">Username</label>
|
||||
<input type="text" id="loginUser" name="user" autocomplete="off">
|
||||
</div>
|
||||
<div>
|
||||
<label for="loginPin">Login code</label>
|
||||
<input
|
||||
type="password"
|
||||
name="pin"
|
||||
id="loginPin"
|
||||
class="inputs input-block form-control input-lg ng-pristine ng-valid"
|
||||
maxlength="5"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
<button type="submit">Sign in</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
<!--
|
||||
Some sites mask OTP/2FA inputs with type="password" instead of the more
|
||||
appropriate type="text"/inputmode="numeric". The TOTP-related keywords in
|
||||
the id/name should cause the password detector to skip the field via the
|
||||
password entry's exclude list.
|
||||
-->
|
||||
<html>
|
||||
<body>
|
||||
<form id="2fa-form" action="/verify" method="post">
|
||||
<div>
|
||||
<label for="otp">Enter your 6-digit verification code</label>
|
||||
<input
|
||||
type="password"
|
||||
name="otp_code"
|
||||
id="otp"
|
||||
maxlength="6"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
<button type="submit">Verify</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user