mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-02-19 07:28:47 -05:00
Add TOTP field detection to FormDetector (#1634)
This commit is contained in:
committed by
Leendert de Borst
parent
0735e257ec
commit
0cdfe5f4d9
@@ -14,6 +14,7 @@ export type FieldPatterns = {
|
||||
birthDateDay: string[];
|
||||
birthDateMonth: string[];
|
||||
birthDateYear: string[];
|
||||
totp: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,7 +49,8 @@ export const EnglishFieldPatterns: FieldPatterns = {
|
||||
gender: ['gender', 'sex'],
|
||||
birthDateDay: ['-day', 'birthdate_d', 'birthdayday', '_day', 'day'],
|
||||
birthDateMonth: ['-month', 'birthdate_m', 'birthdaymonth', '_month', 'month'],
|
||||
birthDateYear: ['-year', 'birthdate_y', 'birthdayyear', '_year', 'year']
|
||||
birthDateYear: ['-year', 'birthdate_y', 'birthdayyear', '_year', 'year'],
|
||||
totp: ['totp', 'otp', 'one-time', 'onetime', 'token', 'authenticator', '2fa', 'twofa', 'two-factor', 'mfa', 'security-code', 'auth-code', 'passcode', 'pin-code', 'pincode']
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -123,7 +125,8 @@ export const DutchFieldPatterns: FieldPatterns = {
|
||||
gender: ['geslacht', 'aanhef'],
|
||||
birthDateDay: ['dag'],
|
||||
birthDateMonth: ['maand'],
|
||||
birthDateYear: ['jaar']
|
||||
birthDateYear: ['jaar'],
|
||||
totp: ['verificatiecode', 'eenmalig', 'authenticatie', 'tweefactor', 'beveiligingscode']
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -204,7 +207,8 @@ export const CombinedFieldPatterns: FieldPatterns = {
|
||||
gender: [...new Set([...EnglishFieldPatterns.gender, ...DutchFieldPatterns.gender])],
|
||||
birthDateDay: [...new Set([...EnglishFieldPatterns.birthDateDay, ...DutchFieldPatterns.birthDateDay])],
|
||||
birthDateMonth: [...new Set([...EnglishFieldPatterns.birthDateMonth, ...DutchFieldPatterns.birthDateMonth])],
|
||||
birthDateYear: [...new Set([...EnglishFieldPatterns.birthDateYear, ...DutchFieldPatterns.birthDateYear])]
|
||||
birthDateYear: [...new Set([...EnglishFieldPatterns.birthDateYear, ...DutchFieldPatterns.birthDateYear])],
|
||||
totp: [...new Set([...EnglishFieldPatterns.totp, ...DutchFieldPatterns.totp])]
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,8 +42,8 @@ export class FormDetector {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the wrapper contains a password or likely username field before processing.
|
||||
if (this.containsPasswordField(formWrapper) || this.containsLikelyUsernameOrEmailField(formWrapper)) {
|
||||
// Check if the wrapper contains a password, likely username field, or TOTP field before processing.
|
||||
if (this.containsPasswordField(formWrapper) || this.containsLikelyUsernameOrEmailField(formWrapper) || this.containsTotpField(formWrapper)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -871,6 +871,66 @@ export class FormDetector {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a form contains a TOTP/2FA field.
|
||||
*/
|
||||
private containsTotpField(wrapper: HTMLElement): boolean {
|
||||
const totpField = this.findTotpField(wrapper as HTMLFormElement | null);
|
||||
return totpField !== null && this.isElementVisible(totpField);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// First try pattern-based detection
|
||||
const candidates = this.findAllInputFields(
|
||||
form,
|
||||
CombinedFieldPatterns.totp,
|
||||
['text', 'number']
|
||||
);
|
||||
|
||||
// Filter out parent-child duplicates
|
||||
const filteredCandidates = this.filterOutNestedDuplicates(candidates);
|
||||
|
||||
if (filteredCandidates.length > 0) {
|
||||
return filteredCandidates[0];
|
||||
}
|
||||
|
||||
// Additional heuristics for TOTP fields that may not match patterns
|
||||
const allInputs = form
|
||||
? Array.from(form.querySelectorAll<HTMLInputElement>('input'))
|
||||
: Array.from(this.document.querySelectorAll<HTMLInputElement>('input'));
|
||||
|
||||
for (const input of allInputs) {
|
||||
if (!this.isElementVisible(input)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for autocomplete="one-time-code"
|
||||
const autocomplete = input.getAttribute('autocomplete')?.toLowerCase() ?? '';
|
||||
if (autocomplete === 'one-time-code') {
|
||||
return input;
|
||||
}
|
||||
|
||||
// Check for maxLength=6 combined with inputmode="numeric"
|
||||
const maxLength = input.maxLength;
|
||||
const inputMode = input.getAttribute('inputmode');
|
||||
if (maxLength === 6 && inputMode === 'numeric') {
|
||||
return input;
|
||||
}
|
||||
|
||||
// Check for numeric pattern attribute with length constraint
|
||||
const pattern = input.getAttribute('pattern');
|
||||
if (pattern && /^\[0-9\]/.test(pattern) && maxLength === 6) {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a form contains a likely username or email field.
|
||||
*/
|
||||
@@ -964,6 +1024,12 @@ export class FormDetector {
|
||||
return 'email';
|
||||
}
|
||||
|
||||
// Check if any of the elements is a TOTP field
|
||||
const totpField = this.findTotpField(formWrapper as HTMLFormElement | null);
|
||||
if (totpField && elementsToCheck.includes(totpField)) {
|
||||
return 'totp';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1030,6 +1096,11 @@ 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,
|
||||
@@ -1041,7 +1112,8 @@ export class FormDetector {
|
||||
firstNameField,
|
||||
lastNameField,
|
||||
birthdateField,
|
||||
genderField
|
||||
genderField,
|
||||
totpField
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { FormField, testField } from './TestUtils';
|
||||
|
||||
describe('FormDetector TOTP tests', () => {
|
||||
it('contains tests for TOTP field detection', () => {
|
||||
/**
|
||||
* This test suite uses testField() helper function
|
||||
* to test TOTP/2FA field detection for various forms.
|
||||
* The actual test implementations are in the helper functions.
|
||||
* This test is just to ensure the test suite is working and to satisfy the linter.
|
||||
*/
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
describe('English TOTP form 1 detection', () => {
|
||||
const htmlFile = 'en-totp-form1.html';
|
||||
|
||||
testField(FormField.Totp, 'otp', htmlFile);
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,8 @@ export enum FormField {
|
||||
Gender = 'gender',
|
||||
GenderMale = 'genderMale',
|
||||
GenderFemale = 'genderFemale',
|
||||
GenderOther = 'genderOther'
|
||||
GenderOther = 'genderOther',
|
||||
Totp = 'totp'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,7 +200,8 @@ export const createMockFormFields = (document: Document): FormFields => ({
|
||||
genderField: {
|
||||
type: 'select',
|
||||
field: document.createElement('select')
|
||||
}
|
||||
},
|
||||
totpField: null
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>TOTP Form 1</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-v-f878e761="" class="form"><form data-v-f878e761=""><h1 data-v-f878e761="" class="h3 mb-3 fw-normal"></h1><!----><!----><div data-v-f878e761=""><div data-v-f878e761="" class="form-floating mt-3"><input data-v-f878e761="" id="otp" type="text" maxlength="6" class="form-control" placeholder="123456" autocomplete="off" required="" data-av-autocomplete="one-time-code" data-av-field-type="totp"><label data-v-f878e761="" for="otp">Token</label></div></div><div data-v-f878e761="" class="form-check mb-3 mt-3 d-flex justify-content-center pe-4"><div data-v-f878e761="" class="form-check"><input data-v-f878e761="" id="remember" type="checkbox" class="form-check-input" value="remember-me"><label data-v-f878e761="" class="form-check-label" for="remember">Remember me</label></div></div><button data-v-f878e761="" class="w-100 btn btn-primary" type="submit">Login</button><!----></form></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -30,4 +30,7 @@ export type FormFields = {
|
||||
other: HTMLInputElement | null;
|
||||
};
|
||||
};
|
||||
|
||||
// TOTP/2FA field for one-time codes
|
||||
totpField: HTMLInputElement | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user