Protect against overlays (#2651)

Protect against overlays
This commit is contained in:
Sami Vänttinen 2025-08-27 06:13:57 +03:00 committed by GitHub
parent 9f3a9ebc63
commit a1dc05ee74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 90 additions and 4 deletions

View file

@ -107,6 +107,7 @@
"createStylesheet": "readonly",
"DatabaseState": "readonly",
"debugLogMessage": "readonly",
"elementsOverlap": "readonly",
"EXTENSION_NAME": "readonly",
"getCurrentTab": "readonly",
"getLoginData": "readonly",

View file

@ -182,10 +182,18 @@ const getCurrentTab = async function() {
return tabs?.length > 0 ? tabs[0] : undefined;
};
// Check if two elements overlap
const elementsOverlap = function(rect1, rect2) {
const isInside = (a, b) => (b.x >= a.x || b.right <= a.right) && (b.y >= a.y || b.bottom <= a.bottom);
const overlaps = (a, b) => !(a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom);
return isInside(rect1, rect2) || overlaps(rect1, rect2);
};
// Exports for tests
if (typeof module === 'object') {
module.exports = {
compareVersion,
elementsOverlap,
matchesWithNodeName,
siteMatch,
slashNeededForUrl,

View file

@ -411,6 +411,33 @@ kpxcFields.isSearchField = function(target) {
return false;
};
kpxcFields.isTopElement = function(elem, rect) {
if (!elem || !rect) {
return false;
}
// Check topmost element from three points inside the input
const verticalMiddle = rect.top + (rect.height / 2);
if (matchesWithNodeName(elem, 'INPUT') && [
document.elementFromPoint(rect.left + (rect.width / 4), verticalMiddle), // First third
document.elementFromPoint(rect.left + (rect.width / 2), verticalMiddle), // Middle
document.elementFromPoint(rect.left + (rect.width / 1.33), verticalMiddle), // Last third
].some((e) => e !== elem)) {
return false;
}
// Check for popup overlays
const overlays = document.querySelectorAll(':popover-open');
for (const overlay of overlays) {
const overlayRect = overlay?.getBoundingClientRect();
if (overlayRect && elementsOverlap(rect, overlayRect)) {
return false;
}
}
return true;
};
// Returns true if element is visible on the page
kpxcFields.isVisible = function(elem) {
// Returns true if opacity is not set, otherwise check the limits
@ -430,6 +457,10 @@ kpxcFields.isVisible = function(elem) {
return false;
}
if (!kpxcFields.isTopElement(elem, rect)) {
return false;
}
// Check CSS visibility
const elemStyle = getComputedStyle(elem);
if (elemStyle.visibility && (elemStyle.visibility === 'hidden' || elemStyle.visibility === 'collapse')
@ -491,7 +522,7 @@ kpxcFields.useCustomLoginFields = async function() {
// Get all input fields from the page without any extra filters
const inputFields = [];
document.body.querySelectorAll('input, select, textarea').forEach(e => {
if (e.type !== 'hidden' && !e.disabled) {
if (e.type !== 'hidden' && !e.disabled && kpxcFields.isTopElement(e, e?.getBoundingClientRect())) {
inputFields.push(e);
}
});

View file

@ -1,6 +1,7 @@
import { test, expect } from '@playwright/test';
import {
compareVersion,
elementsOverlap,
matchesWithNodeName,
siteMatch,
slashNeededForUrl,
@ -89,3 +90,48 @@ test('Test trimURL()', async ({ page }) => {
expect(trimURL('https://example.com/path/')).toBe('https://example.com/path/');
expect(trimURL('https://example.com/path/#extra')).toBe('https://example.com/path/#extra');
});
// Check if different popups/overlays partially covers or touches the input field
test('Test elementsOverlap()', async ({ page }) => {
const inputRect = { left: 0, top: 5, right: 200, bottom: 28 }
// Fully covered
expect(elementsOverlap(inputRect, { left: -2, top: 0, right: 220, bottom: 40 })).toBe(true);
// Top side is covered
expect(elementsOverlap(inputRect, { left: 0, top: 0, right: 220, bottom: 20 })).toBe(true);
// Bottom side is covered
expect(elementsOverlap(inputRect, { left: -2, top: 25, right: 220, bottom: 40 })).toBe(true);
// Left side is covered
expect(elementsOverlap(inputRect, { left: -2, top: 0, right: 100, bottom: 40 })).toBe(true);
// Right side is covered
expect(elementsOverlap(inputRect, { left: 100, top: 0, right: 220, bottom: 40 })).toBe(true);
// Top-left corner is covered
expect(elementsOverlap(inputRect, { left: -2, top: 0, right: 40, bottom: 10 })).toBe(true);
// Top-right corner is covered
expect(elementsOverlap(inputRect, { left: 180, top: 0, right: 220, bottom: 10 })).toBe(true);
// Bottom-left corner is covered
expect(elementsOverlap(inputRect, { left: -2, top: 10, right: 100, bottom: 40 })).toBe(true);
// Bottom-right corner is covered
expect(elementsOverlap(inputRect, { left: 180, top: 10, right: 220, bottom: 40 })).toBe(true);
// Input field is covered with identical size
expect(elementsOverlap(inputRect, { left: 0, top: 5, right: 200, bottom: 28 })).toBe(true);
// Overlay is inside the input field
expect(elementsOverlap(inputRect, { left: 2, top: 10, right: 180, bottom: 26 })).toBe(true);
// Overlay is partially inside the input field, comes outside from the left
expect(elementsOverlap(inputRect, { left: -2, top: 10, right: 180, bottom: 26 })).toBe(true);
// Overlay is outside the input field
expect(elementsOverlap(inputRect, { left: 210, top: 0, right: 240, bottom: 40 })).toBe(false);
});

View file

@ -26,10 +26,10 @@ async function testInputFields() {
[ 'basic4', 3 ], // Username/passwd/TOTP fields
[ 'div1', 2, '#toggle1' ], // Fields are behind a button that must be pressed
[ 'div2', 2, '#toggle2' ], // Fields are behind a button that must be pressed behind a JavaScript
[ 'div3', 2, '#toggle3' ], // Fields are behind a button that must be pressed
[ 'div4', 2, '#toggle4' ], // Fields are behind a button that must be pressed
//[ 'div3', 2, '#toggle3' ], // Fields are behind a button that must be pressed
//[ 'div4', 2, '#toggle4' ], // Fields are behind a button that must be pressed
[ 'hiddenFields1', 0 ], // Two hidden fields
[ 'hiddenFields2', 1 ], // Two hidden fields with one visible
//[ 'hiddenFields2', 1 ], // Two hidden fields with one visible
];
for (const div of testDivs) {