Skip to content

Commit fd4357e

Browse files
committed
fix(extension): detect login/OTP inputs without forms
1 parent d637431 commit fd4357e

File tree

3 files changed

+140
-1
lines changed

3 files changed

+140
-1
lines changed

browser/chromium-extension/dist/formScanner.js

Lines changed: 67 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

browser/chromium-extension/dist/formScanner.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

browser/chromium-extension/src/formScanner.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ const USERNAME_HINTS = ['user', 'login', 'identifier'];
1717
const EMAIL_HINTS = ['email', 'mail'];
1818
const TOTP_HINTS = ['otp', 'totp', '2fa', 'mfa', 'token', 'one-time', 'onetime', 'verification', 'auth', 'security code'];
1919

20+
function isVisibleInput(input: HTMLInputElement): boolean {
21+
if (input.disabled) return false;
22+
const style = window.getComputedStyle(input);
23+
if (style.display === 'none' || style.visibility === 'hidden') return false;
24+
return true;
25+
}
26+
2027
function isLikelyTotp(input: HTMLInputElement): boolean {
2128
if (input.autocomplete === 'one-time-code') return true;
2229

@@ -95,6 +102,48 @@ function scoreForm(fields: DetectedField[]): number {
95102
return score;
96103
}
97104

105+
function findVirtualFormRoot(input: HTMLInputElement): Element {
106+
const MAX_INPUTS = 10;
107+
const MIN_INPUTS = 2;
108+
const candidateTypes = new Set(['text', 'search', 'email', 'tel', 'password', 'number']);
109+
110+
let node: Element | null = input.parentElement;
111+
for (let depth = 0; depth < 6 && node; depth++) {
112+
const inputs = Array.from(node.querySelectorAll('input'))
113+
.filter((el): el is HTMLInputElement => el instanceof HTMLInputElement)
114+
.filter((el) => candidateTypes.has(el.type.toLowerCase()))
115+
.filter(isVisibleInput);
116+
117+
if (inputs.includes(input) && inputs.length >= MIN_INPUTS && inputs.length <= MAX_INPUTS) {
118+
return node;
119+
}
120+
121+
node = node.parentElement;
122+
}
123+
124+
return input.closest('main') ?? input.closest('section') ?? input.closest('div') ?? document.body;
125+
}
126+
127+
function buildDetectedFormFromInputs(inputs: HTMLInputElement[], root: Document): DetectedForm | null {
128+
const fields: DetectedField[] = inputs
129+
.filter((input) => !!input.type)
130+
.map((input) => ({
131+
name: input.name || input.id || input.getAttribute('aria-label') || 'field',
132+
type: classifyField(input),
133+
selector: selectorFor(input)
134+
}));
135+
136+
if (!fields.length) return null;
137+
const score = scoreForm(fields);
138+
if (score === 0) return null;
139+
return {
140+
action: root.location.href,
141+
method: 'POST',
142+
fields,
143+
score
144+
};
145+
}
146+
98147
export function scanForms(root: Document = document): DetectedForm[] {
99148
const forms = Array.from(root.forms);
100149
const detected: DetectedForm[] = [];
@@ -120,6 +169,29 @@ export function scanForms(root: Document = document): DetectedForm[] {
120169
});
121170
}
122171

172+
// Some modern sites don't use <form>. Build "virtual forms" from grouped inputs.
173+
const allInputs = Array.from(root.querySelectorAll('input'))
174+
.filter((el): el is HTMLInputElement => el instanceof HTMLInputElement)
175+
.filter(isVisibleInput);
176+
177+
const seenRoots = new Set<Element>();
178+
for (const input of allInputs) {
179+
if (input.form) continue;
180+
const kind = classifyField(input);
181+
if (kind !== 'password' && kind !== 'totp') continue;
182+
183+
const groupRoot = findVirtualFormRoot(input);
184+
if (seenRoots.has(groupRoot)) continue;
185+
seenRoots.add(groupRoot);
186+
187+
const groupedInputs = Array.from(groupRoot.querySelectorAll('input'))
188+
.filter((el): el is HTMLInputElement => el instanceof HTMLInputElement)
189+
.filter(isVisibleInput);
190+
191+
const virtual = buildDetectedFormFromInputs(groupedInputs, root);
192+
if (virtual) detected.push(virtual);
193+
}
194+
123195
return detected;
124196
}
125197

0 commit comments

Comments
 (0)