Add excluded field definitions for settings related screens, update logic (#1935)

This commit is contained in:
Leendert de Borst
2026-04-19 11:46:04 +02:00
committed by Leendert de Borst
parent e7251775eb
commit 59dde0230d
4 changed files with 126 additions and 21 deletions

View File

@@ -109,12 +109,15 @@ export const EnglishEmailVerificationPatterns: EmailVerificationPatterns = {
/**
* English field exclusion patterns. These patterns identify fields that should NOT trigger autofill,
* such as search boxes and filters. These are commonly found in admin panels,
* data tables, and navigation areas where autofill would be inappropriate.
* such as search boxes, filters, and configuration fields. These are commonly found in admin panels,
* data tables, settings pages, and navigation areas where autofill would be inappropriate.
*/
export const EnglishFieldExclusionPatterns: FieldExclusionPatterns = [
// Search and filter fields
'search', 'find', 'lookup', 'searchbox', 'search-box', 'searchfield', 'search-field', 'searchinput', 'search-input', 'searchquery', 'search-query',
'filter', 'filterable', 'filterinput', 'filter-input', 'filterfield', 'filter-field', 'filterbox', 'filter-box'
'filter', 'filterable', 'filterinput', 'filter-input', 'filterfield', 'filter-field', 'filterbox', 'filter-box',
// Settings and configuration fields
'setting', 'settings', 'config', 'configuration', 'timeout', 'duration', 'interval', 'refresh', 'access'
];
/**
@@ -179,8 +182,11 @@ export const DutchFieldPatterns: FieldPatterns = {
* Dutch field exclusion patterns. These patterns identify fields that should NOT trigger autofill.
*/
export const DutchFieldExclusionPatterns: FieldExclusionPatterns = [
// Search and filter fields
'zoeken', 'zoek', 'zoekveld', 'zoek-veld', 'zoekinput', 'zoek-input', 'zoekbox', 'zoek-box',
'filter', 'filteren', 'filterveld', 'filter-veld', 'filterinput', 'filter-input'
'filter', 'filteren', 'filterveld', 'filter-veld', 'filterinput', 'filter-input',
// Settings and configuration fields
'instelling', 'instellingen', 'configuratie', 'timeout', 'interval',
];
/**

View File

@@ -211,7 +211,36 @@ export class FormDetector {
}
/**
* Check if an input field matches exclusion patterns (search, filter, query fields).
* Check if a pattern matches as a whole word or compound word in the given text.
* Uses word boundaries to avoid false positives.
*
* Examples:
* - "search" matches: "search", "user-search", "searchBox", "search_input"
* - "search" doesn't match: "research", "searchable" (part of another word)
*
* @param text - The text to search in (lowercase).
* @param pattern - The pattern to search for (lowercase).
* @returns True if the pattern matches as a whole word/compound word.
*/
private matchesWordBoundary(text: string, pattern: string): boolean {
/*
* Word boundaries: start of string, space, hyphen, underscore, or transition from lowercase to uppercase
* Pattern must be:
* - At the start: "search", "searchbox", "search-box", "search_box"
* - In the middle: "user-search", "data_search"
* - At the end: "quick-search"
* But NOT within another word: "research" (re-search), "birthdate" (date)
*/
const wordBoundaryPattern = new RegExp(
`(^|[\\s\\-_]|(?<=[a-z])(?=[A-Z]))${pattern}($|[\\s\\-_]|(?<=[a-z])(?=[A-Z]))`,
'i'
);
return wordBoundaryPattern.test(text);
}
/**
* Check if an input field matches exclusion patterns (search, filter fields).
* These fields should not trigger autofill even if they match other patterns.
* Uses whole-word matching to avoid false positives (e.g., "date" shouldn't match "birthdate").
* @param input - The input element to check.
@@ -246,22 +275,7 @@ export class FormDetector {
*/
for (const attr of attributesToCheck) {
for (const pattern of allExclusionPatterns) {
/*
* Check for whole-word matches or compound word matches (search-box, searchInput, etc.)
* Pattern must be:
* - At the start: "search", "searchbox", "search-box", "search_box"
* - In the middle: "user-search", "data_search"
* - At the end: "quick-search"
* But NOT within another word: "research" (re-search), "birthdate" (date)
*
* Word boundaries: start of string, space, hyphen, underscore, or transition from lowercase to uppercase
*/
const wordBoundaryPattern = new RegExp(
`(^|[\\s\\-_]|(?<=[a-z])(?=[A-Z]))${pattern}($|[\\s\\-_]|(?<=[a-z])(?=[A-Z]))`,
'i'
);
if (wordBoundaryPattern.test(attr)) {
if (this.matchesWordBoundary(attr, pattern)) {
return true;
}
}

View File

@@ -20,6 +20,34 @@ describe('FormDetector - Field Exclusion Patterns', () => {
});
});
describe('Real-world scenario: Settings page with token fields', () => {
const htmlFile = 'exclusion-settings-tokens.html';
it('should not trigger TOTP autofill on refresh token lifetime field', () => {
const dom = createTestDom(htmlFile);
const document = dom.window.document;
const tokenInput = document.getElementById('refreshTokenShort');
const formDetector = new FormDetector(document, tokenInput as HTMLElement);
// Should not detect as TOTP form (token fields in settings are not TOTP codes)
expect(formDetector.containsLoginForm()).toBe(false);
expect(formDetector.getDetectedFieldType()).toBeNull();
});
it('should not trigger TOTP autofill on access token lifetime field', () => {
const dom = createTestDom(htmlFile);
const document = dom.window.document;
const tokenInput = document.getElementById('accessTokenLifetime');
const formDetector = new FormDetector(document, tokenInput as HTMLElement);
// Should not detect as TOTP form
expect(formDetector.containsLoginForm()).toBe(false);
expect(formDetector.getDetectedFieldType()).toBeNull();
});
});
describe('Exclusion patterns should not affect legitimate login fields', () => {
const htmlFile = 'exclusion-legitimate-login.html';

View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Settings - Token Configuration</title>
</head>
<body>
<!-- Settings page with token lifetime fields that should NOT trigger TOTP autofill -->
<div class="settings-panel">
<h1>Authentication Settings</h1>
<!-- Refresh Token Lifetime Field (should NOT trigger TOTP) -->
<div>
<label for="refreshTokenShort" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Short Refresh Token Lifetime (hours)
</label>
<input
type="number"
id="refreshTokenShort"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
autocomplete="off">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Determines how long the user stays logged in after inactivity. Used when "Remember me" is not checked during login.
</p>
</div>
<!-- Access Token Lifetime Field (should NOT trigger TOTP) -->
<div>
<label for="accessTokenLifetime" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Access Token Lifetime (minutes)
</label>
<input
type="number"
id="accessTokenLifetime"
name="accessToken"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg"
autocomplete="off">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
JWT token duration for API requests.
</p>
</div>
<!-- Session Timeout Field (should NOT trigger TOTP) -->
<div>
<label for="sessionTimeout" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Session Timeout (minutes)
</label>
<input
type="number"
id="sessionTimeout"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg"
autocomplete="off">
</div>
</div>
</body>
</html>