mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-22 11:45:59 +01:00
[PM-5531] Improve captcha field autofill disqualification (#7581)
* improve captcha field autofill disqualification * add tests
This commit is contained in:
parent
d85485e5cb
commit
487d17daed
@ -44,11 +44,11 @@ export class AutoFillConstants {
|
|||||||
|
|
||||||
static readonly SearchFieldNames: string[] = ["search", "query", "find", "go"];
|
static readonly SearchFieldNames: string[] = ["search", "query", "find", "go"];
|
||||||
|
|
||||||
static readonly PasswordFieldIgnoreList: string[] = [
|
static readonly FieldIgnoreList: string[] = ["captcha", "findanything", "forgot"];
|
||||||
|
|
||||||
|
static readonly PasswordFieldExcludeList: string[] = [
|
||||||
|
...AutoFillConstants.FieldIgnoreList,
|
||||||
"onetimepassword",
|
"onetimepassword",
|
||||||
"captcha",
|
|
||||||
"findanything",
|
|
||||||
"forgot",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
static readonly ExcludedAutofillLoginTypes: string[] = [
|
static readonly ExcludedAutofillLoginTypes: string[] = [
|
||||||
|
@ -2282,7 +2282,7 @@ describe("AutofillService", () => {
|
|||||||
tagName: "span",
|
tagName: "span",
|
||||||
});
|
});
|
||||||
pageDetails.fields = [spanField];
|
pageDetails.fields = [spanField];
|
||||||
jest.spyOn(AutofillService, "isExcludedField");
|
jest.spyOn(AutofillService, "isExcludedFieldType");
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
@ -2291,7 +2291,7 @@ describe("AutofillService", () => {
|
|||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(AutofillService["isExcludedField"]).toHaveBeenCalled();
|
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
|
||||||
expect(value).toStrictEqual(unmodifiedFillScriptValues);
|
expect(value).toStrictEqual(unmodifiedFillScriptValues);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2305,7 +2305,7 @@ describe("AutofillService", () => {
|
|||||||
type: excludedType,
|
type: excludedType,
|
||||||
});
|
});
|
||||||
pageDetails.fields = [invalidField];
|
pageDetails.fields = [invalidField];
|
||||||
jest.spyOn(AutofillService, "isExcludedField");
|
jest.spyOn(AutofillService, "isExcludedFieldType");
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
@ -2314,7 +2314,7 @@ describe("AutofillService", () => {
|
|||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(AutofillService["isExcludedField"]).toHaveBeenCalledWith(
|
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledWith(
|
||||||
invalidField,
|
invalidField,
|
||||||
AutoFillConstants.ExcludedAutofillTypes,
|
AutoFillConstants.ExcludedAutofillTypes,
|
||||||
);
|
);
|
||||||
@ -2333,7 +2333,7 @@ describe("AutofillService", () => {
|
|||||||
});
|
});
|
||||||
pageDetails.fields = [notViewableField];
|
pageDetails.fields = [notViewableField];
|
||||||
jest.spyOn(AutofillService, "forCustomFieldsOnly");
|
jest.spyOn(AutofillService, "forCustomFieldsOnly");
|
||||||
jest.spyOn(AutofillService, "isExcludedField");
|
jest.spyOn(AutofillService, "isExcludedFieldType");
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
@ -2343,7 +2343,7 @@ describe("AutofillService", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(notViewableField);
|
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(notViewableField);
|
||||||
expect(AutofillService["isExcludedField"]).toHaveBeenCalled();
|
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
|
||||||
expect(value).toStrictEqual(unmodifiedFillScriptValues);
|
expect(value).toStrictEqual(unmodifiedFillScriptValues);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -2428,7 +2428,7 @@ describe("AutofillService", () => {
|
|||||||
options.cipher.card.code = "testCode";
|
options.cipher.card.code = "testCode";
|
||||||
options.cipher.card.brand = "testBrand";
|
options.cipher.card.brand = "testBrand";
|
||||||
jest.spyOn(AutofillService, "forCustomFieldsOnly");
|
jest.spyOn(AutofillService, "forCustomFieldsOnly");
|
||||||
jest.spyOn(AutofillService, "isExcludedField");
|
jest.spyOn(AutofillService, "isExcludedFieldType");
|
||||||
jest.spyOn(AutofillService as any, "isFieldMatch");
|
jest.spyOn(AutofillService as any, "isFieldMatch");
|
||||||
jest.spyOn(autofillService as any, "makeScriptAction");
|
jest.spyOn(autofillService as any, "makeScriptAction");
|
||||||
jest.spyOn(AutofillService, "hasValue");
|
jest.spyOn(AutofillService, "hasValue");
|
||||||
@ -2446,7 +2446,7 @@ describe("AutofillService", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledTimes(6);
|
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledTimes(6);
|
||||||
expect(AutofillService["isExcludedField"]).toHaveBeenCalledTimes(6);
|
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledTimes(6);
|
||||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalled();
|
expect(AutofillService["isFieldMatch"]).toHaveBeenCalled();
|
||||||
expect(autofillService["makeScriptAction"]).toHaveBeenCalledTimes(4);
|
expect(autofillService["makeScriptAction"]).toHaveBeenCalledTimes(4);
|
||||||
expect(AutofillService["hasValue"]).toHaveBeenCalledTimes(6);
|
expect(AutofillService["hasValue"]).toHaveBeenCalledTimes(6);
|
||||||
@ -2895,7 +2895,7 @@ describe("AutofillService", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
pageDetails.fields = [];
|
pageDetails.fields = [];
|
||||||
jest.spyOn(AutofillService, "forCustomFieldsOnly");
|
jest.spyOn(AutofillService, "forCustomFieldsOnly");
|
||||||
jest.spyOn(AutofillService, "isExcludedField");
|
jest.spyOn(AutofillService, "isExcludedFieldType");
|
||||||
jest.spyOn(AutofillService as any, "isFieldMatch");
|
jest.spyOn(AutofillService as any, "isFieldMatch");
|
||||||
jest.spyOn(autofillService as any, "makeScriptAction");
|
jest.spyOn(autofillService as any, "makeScriptAction");
|
||||||
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
|
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
|
||||||
@ -2913,7 +2913,7 @@ describe("AutofillService", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(customField);
|
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(customField);
|
||||||
expect(AutofillService["isExcludedField"]).toHaveBeenCalled();
|
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
|
||||||
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
|
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
|
||||||
expect(value.script).toStrictEqual([]);
|
expect(value.script).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
@ -2930,7 +2930,7 @@ describe("AutofillService", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(excludedField);
|
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(excludedField);
|
||||||
expect(AutofillService["isExcludedField"]).toHaveBeenCalledWith(
|
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledWith(
|
||||||
excludedField,
|
excludedField,
|
||||||
AutoFillConstants.ExcludedAutofillTypes,
|
AutoFillConstants.ExcludedAutofillTypes,
|
||||||
);
|
);
|
||||||
@ -2950,7 +2950,7 @@ describe("AutofillService", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(viewableField);
|
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(viewableField);
|
||||||
expect(AutofillService["isExcludedField"]).toHaveBeenCalled();
|
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
|
||||||
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
|
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
|
||||||
expect(value.script).toStrictEqual([]);
|
expect(value.script).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
@ -3690,6 +3690,15 @@ describe("AutofillService", () => {
|
|||||||
expect(result).toStrictEqual([passwordField]);
|
expect(result).toStrictEqual([passwordField]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns the an empty array if the field's htmlID contains the words `password` and `captcha`", () => {
|
||||||
|
passwordField.htmlID = "inputPasswordCaptcha";
|
||||||
|
pageDetails.fields = [passwordField];
|
||||||
|
|
||||||
|
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
|
||||||
|
|
||||||
|
expect(result).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns the field in an array if the field's htmlName contains the word `password`", () => {
|
it("returns the field in an array if the field's htmlName contains the word `password`", () => {
|
||||||
passwordField.htmlName = "password";
|
passwordField.htmlName = "password";
|
||||||
pageDetails.fields = [passwordField];
|
pageDetails.fields = [passwordField];
|
||||||
@ -3699,6 +3708,15 @@ describe("AutofillService", () => {
|
|||||||
expect(result).toStrictEqual([passwordField]);
|
expect(result).toStrictEqual([passwordField]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns the an empty array if the field's htmlName contains the words `password` and `captcha`", () => {
|
||||||
|
passwordField.htmlName = "inputPasswordCaptcha";
|
||||||
|
pageDetails.fields = [passwordField];
|
||||||
|
|
||||||
|
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
|
||||||
|
|
||||||
|
expect(result).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns the field in an array if the field's placeholder contains the word `password`", () => {
|
it("returns the field in an array if the field's placeholder contains the word `password`", () => {
|
||||||
passwordField.placeholder = "password";
|
passwordField.placeholder = "password";
|
||||||
pageDetails.fields = [passwordField];
|
pageDetails.fields = [passwordField];
|
||||||
@ -3707,6 +3725,26 @@ describe("AutofillService", () => {
|
|||||||
|
|
||||||
expect(result).toStrictEqual([passwordField]);
|
expect(result).toStrictEqual([passwordField]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns the an empty array if the field's placeholder contains the words `password` and `captcha`", () => {
|
||||||
|
passwordField.placeholder = "inputPasswordCaptcha";
|
||||||
|
pageDetails.fields = [passwordField];
|
||||||
|
|
||||||
|
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
|
||||||
|
|
||||||
|
expect(result).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the an empty array if any of the field's checked attributed contain the words `captcha` while any other attribute contains the word `password` and no excluded terms", () => {
|
||||||
|
passwordField.htmlID = "inputPasswordCaptcha";
|
||||||
|
passwordField.htmlName = "captcha";
|
||||||
|
passwordField.placeholder = "Enter password";
|
||||||
|
pageDetails.fields = [passwordField];
|
||||||
|
|
||||||
|
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
|
||||||
|
|
||||||
|
expect(result).toStrictEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("given a field that is not viewable", () => {
|
describe("given a field that is not viewable", () => {
|
||||||
|
@ -736,7 +736,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
const fillFields: { [id: string]: AutofillField } = {};
|
const fillFields: { [id: string]: AutofillField } = {};
|
||||||
|
|
||||||
pageDetails.fields.forEach((f) => {
|
pageDetails.fields.forEach((f) => {
|
||||||
if (AutofillService.isExcludedField(f, AutoFillConstants.ExcludedAutofillTypes)) {
|
if (AutofillService.isExcludedFieldType(f, AutoFillConstants.ExcludedAutofillTypes)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1114,7 +1114,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
const fillFields: { [id: string]: AutofillField } = {};
|
const fillFields: { [id: string]: AutofillField } = {};
|
||||||
|
|
||||||
pageDetails.fields.forEach((f) => {
|
pageDetails.fields.forEach((f) => {
|
||||||
if (AutofillService.isExcludedField(f, AutoFillConstants.ExcludedAutofillTypes)) {
|
if (AutofillService.isExcludedFieldType(f, AutoFillConstants.ExcludedAutofillTypes)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1350,7 +1350,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
return Boolean(matchFieldAttributeValues.join(" ").match(matchPattern));
|
return Boolean(matchFieldAttributeValues.join(" ").match(matchPattern));
|
||||||
}
|
}
|
||||||
|
|
||||||
static isExcludedField(field: AutofillField, excludedTypes: string[]) {
|
static isExcludedFieldType(field: AutofillField, excludedTypes: string[]) {
|
||||||
if (AutofillService.forCustomFieldsOnly(field)) {
|
if (AutofillService.forCustomFieldsOnly(field)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -1477,6 +1477,44 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static valueIsLikePassword(value: string) {
|
||||||
|
if (value == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Removes all whitespace, _ and - characters
|
||||||
|
const cleanedValue = value.toLowerCase().replace(/[\s_-]/g, "");
|
||||||
|
|
||||||
|
if (cleanedValue.indexOf("password") < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AutoFillConstants.PasswordFieldExcludeList.some((i) => cleanedValue.indexOf(i) > -1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fieldHasDisqualifyingAttributeValue(field: AutofillField) {
|
||||||
|
const checkedAttributeValues = [field.htmlID, field.htmlName, field.placeholder];
|
||||||
|
let valueIsOnExclusionList = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < checkedAttributeValues.length; i++) {
|
||||||
|
const checkedAttributeValue = checkedAttributeValues[i];
|
||||||
|
const cleanedValue = checkedAttributeValue?.toLowerCase().replace(/[\s_-]/g, "");
|
||||||
|
|
||||||
|
valueIsOnExclusionList = Boolean(
|
||||||
|
cleanedValue && AutoFillConstants.FieldIgnoreList.some((i) => cleanedValue.indexOf(i) > -1),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (valueIsOnExclusionList) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return valueIsOnExclusionList;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accepts a pageDetails object with a list of fields and returns a list of
|
* Accepts a pageDetails object with a list of fields and returns a list of
|
||||||
* fields that are likely to be password fields.
|
* fields that are likely to be password fields.
|
||||||
@ -1495,45 +1533,39 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
fillNewPassword: boolean,
|
fillNewPassword: boolean,
|
||||||
) {
|
) {
|
||||||
const arr: AutofillField[] = [];
|
const arr: AutofillField[] = [];
|
||||||
|
|
||||||
pageDetails.fields.forEach((f) => {
|
pageDetails.fields.forEach((f) => {
|
||||||
if (AutofillService.isExcludedField(f, AutoFillConstants.ExcludedAutofillLoginTypes)) {
|
if (AutofillService.isExcludedFieldType(f, AutoFillConstants.ExcludedAutofillLoginTypes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any attribute values match disqualifying values, the entire field should not be used
|
||||||
|
if (AutofillService.fieldHasDisqualifyingAttributeValue(f)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPassword = f.type === "password";
|
const isPassword = f.type === "password";
|
||||||
const valueIsLikePassword = (value: string) => {
|
|
||||||
if (value == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Removes all whitespace, _ and - characters
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const cleanedValue = value.toLowerCase().replace(/[\s_\-]/g, "");
|
|
||||||
|
|
||||||
if (cleanedValue.indexOf("password") < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AutoFillConstants.PasswordFieldIgnoreList.some((i) => cleanedValue.indexOf(i) > -1)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
const isLikePassword = () => {
|
const isLikePassword = () => {
|
||||||
if (f.type !== "text") {
|
if (f.type !== "text") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (valueIsLikePassword(f.htmlID)) {
|
|
||||||
|
if (AutofillService.valueIsLikePassword(f.htmlID)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (valueIsLikePassword(f.htmlName)) {
|
|
||||||
|
if (AutofillService.valueIsLikePassword(f.htmlName)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (valueIsLikePassword(f.placeholder)) {
|
|
||||||
|
if (AutofillService.valueIsLikePassword(f.placeholder)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!f.disabled &&
|
!f.disabled &&
|
||||||
(canBeReadOnly || !f.readonly) &&
|
(canBeReadOnly || !f.readonly) &&
|
||||||
@ -1545,6 +1577,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
arr.push(f);
|
arr.push(f);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1621,7 +1654,10 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fieldIsDisqualified = AutofillService.fieldHasDisqualifyingAttributeValue(f);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
!fieldIsDisqualified &&
|
||||||
!f.disabled &&
|
!f.disabled &&
|
||||||
(canBeReadOnly || !f.readonly) &&
|
(canBeReadOnly || !f.readonly) &&
|
||||||
(withoutForm || f.form === passwordField.form) &&
|
(withoutForm || f.form === passwordField.form) &&
|
||||||
|
Loading…
Reference in New Issue
Block a user