From f48647e2f112dd64eb3961324cf94e5150dc32da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Mon, 16 Mar 2026 11:23:29 -0400 Subject: [PATCH] [PM-31646] stop label collection from crossing into sibling form-field containers (#19452) Adds a containsChildFormElement guard to left-label, right-label, and recursive sibling traversal so that text inside a sibling DOM container that holds its own input/select/textarea is not absorbed as label text for the target field. This prevents Yahoo's country-code dropdown container ("Enter Country Code") from contaminating the username field's label-left, which caused a false TOTP classification via the word "code". --- .../autofill/services/autofill-constants.ts | 3 + .../collect-autofill-content.service.spec.ts | 111 ++++++++++++++++++ .../collect-autofill-content.service.ts | 29 ++++- 3 files changed, 141 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/autofill/services/autofill-constants.ts b/apps/browser/src/autofill/services/autofill-constants.ts index 0fbe2e67ae3..d2c92fc73ca 100644 --- a/apps/browser/src/autofill/services/autofill-constants.ts +++ b/apps/browser/src/autofill/services/autofill-constants.ts @@ -98,6 +98,9 @@ export class AutoFillConstants { ...AutoFillConstants.ExcludedAutofillTypes, ]; + /** HTML elements for form fields */ + static readonly FieldElements: string[] = ["input", "select", "textarea"]; + static readonly ExcludedIdentityAutocompleteTypes: Set = new Set([ "current-password", "new-password", diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts index 7a1099e199b..b47092ec27a 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts @@ -1503,6 +1503,34 @@ describe("CollectAutofillContentService", () => { expect(labelTag).toEqual("Username"); }); + + it("does not collect text from a sibling that contains a form field", () => { + document.body.innerHTML = ` +
+ +
Enter Country Code
+
+ `; + const element = document.querySelector("#username-id") as FillableFormFieldElement; + + const labelTag = collectAutofillContentService["createAutofillFieldRightLabel"](element); + + expect(labelTag).toEqual(""); + }); + + it("does not stop traversal at a sibling div that has no form field descendant", () => { + document.body.innerHTML = ` +
+ +
Helper text
+
+ `; + const element = document.querySelector("#username-id") as FillableFormFieldElement; + + const labelTag = collectAutofillContentService["createAutofillFieldRightLabel"](element); + + expect(labelTag).toEqual("Helper text"); + }); }); describe("createAutofillFieldLeftLabel", () => { @@ -1520,6 +1548,53 @@ describe("CollectAutofillContentService", () => { expect(labelTag).toEqual("Text ContentUsername"); }); + + it("does not collect text from a direct sibling that contains a form field", () => { + document.body.innerHTML = ` +
+
Enter Country Code
+ +
+ `; + const element = document.querySelector("#username-id") as FillableFormFieldElement; + + const labelTag = collectAutofillContentService["createAutofillFieldLeftLabel"](element); + + expect(labelTag).toEqual(""); + }); + + it("does not collect text from a parent sibling that contains a form field", () => { + // Exercises the parent-walk code path: the input has no direct previous + // siblings, so the traversal walks up to the parent and checks its previous + // sibling — which should be blocked because it contains a form field. + document.body.innerHTML = ` +
+
Enter Country Code
+
+ +
+
+ `; + const element = document.querySelector("#username-id") as FillableFormFieldElement; + + const labelTag = collectAutofillContentService["createAutofillFieldLeftLabel"](element); + + expect(labelTag).toEqual(""); + }); + + it("does not stop traversal at a sibling div that has no form field descendant", () => { + document.body.innerHTML = ` +
+
Helpful label
+ +
+ `; + const element = document.querySelector("#username-id") as FillableFormFieldElement; + + const labelTag = collectAutofillContentService["createAutofillFieldLeftLabel"](element); + + expect(labelTag).toEqual("Helpful label"); + }); }); describe("createAutofillFieldTopLabel", () => { @@ -1653,6 +1728,42 @@ describe("CollectAutofillContentService", () => { }); }); + describe("containsChildField", () => { + it("returns true when the element contains an input descendant", () => { + const div = document.createElement("div"); + div.innerHTML = `Enter Country Code`; + + expect(collectAutofillContentService["containsChildField"](div)).toBe(true); + }); + + it("returns true when the element contains a select descendant", () => { + const div = document.createElement("div"); + div.innerHTML = ``; + + expect(collectAutofillContentService["containsChildField"](div)).toBe(true); + }); + + it("returns true when the element contains a textarea descendant", () => { + const div = document.createElement("div"); + div.innerHTML = ``; + + expect(collectAutofillContentService["containsChildField"](div)).toBe(true); + }); + + it("returns false when the element contains no form field descendants", () => { + const div = document.createElement("div"); + div.innerHTML = `Helper text`; + + expect(collectAutofillContentService["containsChildField"](div)).toBe(false); + }); + + it("returns false when the node is a text node", () => { + const textNode = document.createTextNode("Enter Country Code"); + + expect(collectAutofillContentService["containsChildField"](textNode)).toBe(false); + }); + }); + describe("isNewSectionElement", () => { const validElementTags = [ "html", diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index edf76d3f456..e3d565a1ef1 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -36,6 +36,7 @@ import { } from "./abstractions/collect-autofill-content.service"; import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service"; import { DomQueryService } from "./abstractions/dom-query.service"; +import { AutoFillConstants } from "./autofill-constants"; export class CollectAutofillContentService implements CollectAutofillContentServiceInterface { private readonly sendExtensionMessage = sendExtensionMessage; @@ -662,6 +663,10 @@ export class CollectAutofillContentService implements CollectAutofillContentServ break; } + if (this.containsChildField(currentElement)) { + break; + } + const textContent = this.getTextContentFromElement(currentElement); if (textContent) { labelTextContent.push(textContent); @@ -712,6 +717,18 @@ export class CollectAutofillContentService implements CollectAutofillContentServ : null; } + /** + * Checks whether any of an element's descendants are form fields. + */ + private containsChildField(element: Node): boolean { + if (nodeIsElement(element)) { + const fields = AutoFillConstants.FieldElements.join(", "); + return !!element.querySelector(fields); + } else { + return false; + } + } + /** * Check if the element's tag indicates that a transition to a new section of the * page is occurring. If so, we should not use the element or its children in order @@ -794,6 +811,10 @@ export class CollectAutofillContentService implements CollectAutofillContentServ return textContentItems; } + if (this.containsChildField(currentElement)) { + return textContentItems; + } + const textContent = this.getTextContentFromElement(currentElement); if (textContent) { textContentItems.push(textContent); @@ -813,11 +834,15 @@ export class CollectAutofillContentService implements CollectAutofillContentServ let siblingElement = nodeIsElement(currentElement) ? currentElement.previousElementSibling : currentElement.previousSibling; - while (siblingElement?.lastChild && !this.isNewSectionElement(siblingElement)) { + while ( + siblingElement?.lastChild && + !this.isNewSectionElement(siblingElement) && + !this.containsChildField(siblingElement) + ) { siblingElement = siblingElement.lastChild; } - if (this.isNewSectionElement(siblingElement)) { + if (this.isNewSectionElement(siblingElement) || this.containsChildField(siblingElement)) { return textContentItems; }