Update FormFiller logic to improve browser extension autofill reliability

This commit is contained in:
Leendert de Borst
2025-11-28 18:50:40 +01:00
parent 2a902eeb97
commit d846825b84

View File

@@ -27,18 +27,16 @@ export class FormFiller {
* @param credential The credential to fill the form with.
*/
public async fillFields(credential: Credential): Promise<void> {
// Perform security validation before filling any fields
if (!await this.validateFormSecurity()) {
console.warn('[AliasVault Security] Autofill blocked due to security validation failure');
return;
}
// Perform security validation to identify safe fields
const securityResults = await this.validateFormSecurity();
/*
* Fill fields sequentially to avoid race conditions and conflicts.
* Some websites have event handlers that can interfere with parallel filling.
* Only fill fields that passed security validation.
*/
await this.fillBasicFields(credential);
await this.fillPasswordFields(credential);
await this.fillBasicFields(credential, securityResults);
await this.fillPasswordFields(credential, securityResults);
this.fillBirthdateFields(credential);
this.fillGenderFields(credential);
@@ -51,12 +49,18 @@ export class FormFiller {
* - Form field obstruction via overlays
* - Suspicious element positioning
* - Multiple forms with identical fields (potential decoy attacks)
*
* @returns A map of field elements to their security validation result (true = safe, false = unsafe)
*/
private async validateFormSecurity(): Promise<boolean> {
private async validateFormSecurity(): Promise<Map<HTMLElement, boolean>> {
const results = new Map<HTMLElement, boolean>();
try {
// Skip security validation in test environments where browser APIs may not be available
if (typeof window === 'undefined' || typeof MouseEvent === 'undefined') {
return true;
// In test environments, mark all fields as safe
this.getAllFormFields().forEach(field => results.set(field, true));
return results;
}
// 1. Check page-wide security using ClickValidator (detects body/HTML opacity tricks)
@@ -68,30 +72,40 @@ export class FormFiller {
});
// Note: isTrusted is read-only and set by the browser
if (!await this.clickValidator.validateClick(dummyEvent)) {
console.warn('[AliasVault Security] Form autofill blocked: Page-wide attack detected');
return false;
const pageWideSecure = await this.clickValidator.validateClick(dummyEvent);
if (!pageWideSecure) {
console.warn('[AliasVault Security] Page-wide attack detected - blocking all autofill');
// Mark all fields as unsafe
this.getAllFormFields().forEach(field => results.set(field, false));
return results;
}
// 2. Check form field obstruction and positioning
// 2. Check for suspicious form duplication (decoy attack)
const hasDecoyForms = this.detectDecoyForms();
if (hasDecoyForms) {
console.warn('[AliasVault Security] Multiple suspicious forms detected - blocking all autofill');
// Mark all fields as unsafe
this.getAllFormFields().forEach(field => results.set(field, false));
return results;
}
// 3. Check individual form field obstruction and positioning
const formFields = this.getAllFormFields();
for (const field of formFields) {
if (!this.validateFieldSecurity(field)) {
console.warn('[AliasVault Security] Form autofill blocked: Field obstruction detected', field);
return false;
const isFieldSecure = this.validateFieldSecurity(field);
results.set(field, isFieldSecure);
if (!isFieldSecure) {
console.warn('[AliasVault Security] Field failed security check (will be skipped):', field);
}
}
// 3. Check for suspicious form duplication (decoy attack)
if (this.detectDecoyForms()) {
console.warn('[AliasVault Security] Form autofill blocked: Multiple suspicious forms detected');
return false;
}
return true;
return results;
} catch (error) {
console.error('[AliasVault Security] Form security validation error:', error);
return false; // Fail safely - block autofill if validation fails
// Fail safely - mark all fields as unsafe if validation fails
this.getAllFormFields().forEach(field => results.set(field, false));
return results;
}
}
@@ -291,13 +305,14 @@ export class FormFiller {
/**
* Fill the basic fields of the form.
* @param credential The credential to fill the form with.
* @param securityResults Security validation results for each field.
*/
private async fillBasicFields(credential: Credential): Promise<void> {
if (this.form.usernameField && credential.Username) {
private async fillBasicFields(credential: Credential, securityResults: Map<HTMLElement, boolean>): Promise<void> {
if (this.form.usernameField && credential.Username && securityResults.get(this.form.usernameField) !== false) {
await this.fillTextFieldWithTyping(this.form.usernameField, credential.Username);
}
if (this.form.emailField && (credential.Alias?.Email !== undefined || credential.Username !== undefined)) {
if (this.form.emailField && (credential.Alias?.Email !== undefined || credential.Username !== undefined) && securityResults.get(this.form.emailField) !== false) {
if (credential.Alias?.Email) {
this.setElementValue(this.form.emailField, credential.Alias.Email);
this.triggerInputEvents(this.form.emailField);
@@ -317,7 +332,7 @@ export class FormFiller {
}
}
if (this.form.emailConfirmField && credential.Alias?.Email) {
if (this.form.emailConfirmField && credential.Alias?.Email && securityResults.get(this.form.emailConfirmField) !== false) {
this.setElementValue(this.form.emailConfirmField, credential.Alias.Email);
this.triggerInputEvents(this.form.emailConfirmField);
}
@@ -388,19 +403,20 @@ export class FormFiller {
* Fill password fields sequentially to avoid visual conflicts.
* First fills the main password field, then the confirm field if present.
* @param credential The credential containing the password.
* @param securityResults Security validation results for each field.
*/
private async fillPasswordFields(credential: Credential): Promise<void> {
private async fillPasswordFields(credential: Credential, securityResults: Map<HTMLElement, boolean>): Promise<void> {
if (!credential.Password) {
return;
}
// Fill main password field first
if (this.form.passwordField) {
// Fill main password field first (only if it passed security check)
if (this.form.passwordField && securityResults.get(this.form.passwordField) !== false) {
await this.fillPasswordField(this.form.passwordField, credential.Password);
}
// Then fill password confirm field after main field is complete
if (this.form.passwordConfirmField) {
// Then fill password confirm field after main field is complete (only if it passed security check)
if (this.form.passwordConfirmField && securityResults.get(this.form.passwordConfirmField) !== false) {
await this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
}
}