mirror of
https://github.com/keepassxreboot/keepassxc-browser.git
synced 2026-03-11 08:54:43 +00:00
Improve Top-Level-Domain checks for iframes
This commit is contained in:
parent
364c7fe416
commit
d64a7fca34
8 changed files with 124 additions and 23 deletions
|
|
@ -28,6 +28,7 @@ KeePassXC-Browser extension requests the following permissions:
|
|||
| ----- | ----- |
|
||||
| `activeTab` | To get URL of the current tab |
|
||||
| `contextMenus` | To show context menu items |
|
||||
| `cookies` | To access browser's internal Public Suffix List |
|
||||
| `clipboardWrite` | Allows password to be copied from password generator to clipboard |
|
||||
| `nativeMessaging` | Allows communication with KeePassXC application |
|
||||
| `notifications` | To show browser notifications |
|
||||
|
|
@ -42,7 +43,7 @@ KeePassXC-Browser extension requests the following permissions:
|
|||
|
||||
## Protocol
|
||||
|
||||
The details about the messaging protocol used with the browser extension and KeePassXC can be found [here](keepassxc-protocol.md).
|
||||
Check [keepassxc-protocol](keepassxc-protocol.md) for the details about the messaging protocol used between the browser extension and KeePassXC.
|
||||
|
||||
## Translations
|
||||
|
||||
|
|
|
|||
|
|
@ -253,6 +253,7 @@ kpxcEvent.messageHandlers = {
|
|||
'hide_troubleshooting_guide_alert': kpxcEvent.hideTroubleshootingGuideAlert,
|
||||
'init_http_auth': kpxcEvent.initHttpAuth,
|
||||
'is_connected': kpxcEvent.getIsKeePassXCAvailable,
|
||||
'is_iframe_allowed': page.isIframeAllowed,
|
||||
'load_keyring': kpxcEvent.onLoadKeyRing,
|
||||
'load_settings': kpxcEvent.onLoadSettings,
|
||||
'lock_database': kpxcEvent.lockDatabase,
|
||||
|
|
|
|||
|
|
@ -582,7 +582,7 @@ keepass.requestAutotype = async function(tab, args = []) {
|
|||
|
||||
const kpAction = kpActions.REQUEST_AUTOTYPE;
|
||||
const nonce = keepassClient.getNonce();
|
||||
const search = getTopLevelDomainFromUrl(args[0]);
|
||||
const search = page.getTopLevelDomainFromUrl(args[0]);
|
||||
|
||||
const messageData = {
|
||||
action: kpAction,
|
||||
|
|
|
|||
|
|
@ -329,6 +329,87 @@ page.updatePopup = function(tab) {
|
|||
browserAction.showDefault(tab);
|
||||
};
|
||||
|
||||
page.isIframeAllowed = async function(tab, args = []) {
|
||||
const [ url, hostname ] = args;
|
||||
const baseDomain = await page.getBaseDomainFromUrl(hostname, url);
|
||||
|
||||
// Allow iframe if the base domain is included in iframes' and tab's hostname
|
||||
const tabUrl = new URL(tab?.url);
|
||||
return hostname.endsWith(baseDomain) && tabUrl.hostname?.endsWith(baseDomain);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the top level domain from URL.
|
||||
* @param {string} domain Current iframe's hostname
|
||||
* @param {string} url Current iframe's full URL
|
||||
* @returns {string} TLD e.g. https://another.example.co.uk -> co.uk
|
||||
*/
|
||||
page.getTopLevelDomainFromUrl = async function(domain, url) {
|
||||
// A simple check for IPv4 address. TLD cannot be numeric, and if hostname is just numbers, it's probably an IPv4.
|
||||
// TODO: Handle IPv6 addresses. Is there some internal API for these?
|
||||
if (!isNaN(Number(domain?.replaceAll('.', '')))) {
|
||||
return domain;
|
||||
}
|
||||
|
||||
// Only loop the amount of different domain parts found
|
||||
const numberOfDomainParts = domain?.split('.')?.length;
|
||||
for (let i = 0; i < numberOfDomainParts; ++i) {
|
||||
// Cut the first part from host
|
||||
const index = domain?.indexOf('.');
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if dummy cookie's domain/TLD matches with public suffix list.
|
||||
// If setting the cookie fails, TLD has been found.
|
||||
try {
|
||||
domain = domain?.substring(index + 1);
|
||||
const reply = await browser.cookies.set({
|
||||
domain: domain,
|
||||
name: 'kpxc',
|
||||
sameSite: 'strict',
|
||||
url: url,
|
||||
value: ''
|
||||
});
|
||||
|
||||
// Delete the temporary cookie immediately
|
||||
if (reply) {
|
||||
await browser.cookies.remove({
|
||||
name: 'kpxc',
|
||||
url: url
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
|
||||
return domain;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the base domain of URL or hostname.
|
||||
* Up-to-date list can be found: https://publicsuffix.org/list/public_suffix_list.dat
|
||||
* @param {string} domain Current iframe's hostname
|
||||
* @param {string} url Current iframe's full URL
|
||||
* @returns {string} The base domain, e.g. https://another.example.co.uk -> example.co.uk
|
||||
*/
|
||||
page.getBaseDomainFromUrl = async function(hostname, url) {
|
||||
const tld = await page.getTopLevelDomainFromUrl(hostname, url);
|
||||
if (tld.length === 0 || tld === hostname) {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
// Remove the top level domain part from the hostname, e.g. https://another.example.co.uk -> https://another.example
|
||||
const finalDomain = hostname.slice(0, hostname.indexOf(tld) - 1);
|
||||
// Split the URL and select the last part, e.g. https://another.example -> example
|
||||
let baseDomain = finalDomain.split('.')?.at(-1);
|
||||
// Append the top level domain back to the URL, e.g. example -> example.co.uk
|
||||
baseDomain = baseDomain + '.' + tld;
|
||||
|
||||
return baseDomain;
|
||||
};
|
||||
|
||||
const createContextMenuItem = function({ action, args, ...options }) {
|
||||
return browser.contextMenus.create({
|
||||
contexts: menuContexts,
|
||||
|
|
@ -344,6 +425,7 @@ const createContextMenuItem = function({ action, args, ...options }) {
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
const logDebug = function(message, extra) {
|
||||
if (page.settings.debugLogging) {
|
||||
debugLogMessage(message, extra);
|
||||
|
|
|
|||
|
|
@ -111,19 +111,6 @@ const slashNeededForUrl = function(pattern) {
|
|||
return matchPattern.exec(pattern);
|
||||
};
|
||||
|
||||
// Returns the top level domain, e.g. https://another.example.co.uk -> example.co.uk
|
||||
// This is done because a top level domain will probably give better matches with Auto-Type than a full hostname.
|
||||
const getTopLevelDomainFromUrl = function(hostname) {
|
||||
const domainRegex = new RegExp(/(\w+).(com|net|org|edu|co)*(.\w+)$/g);
|
||||
const domainMatch = domainRegex.exec(hostname);
|
||||
|
||||
if (domainMatch) {
|
||||
return domainMatch[0];
|
||||
}
|
||||
|
||||
return hostname;
|
||||
};
|
||||
|
||||
function tr(key, params) {
|
||||
return browser.i18n.getMessage(key, params);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -580,7 +580,7 @@ kpxc.rememberCredentialsFromContextMenu = async function() {
|
|||
// The basic function for retrieving credentials from KeePassXC.
|
||||
// Credential Banner can force the retrieval for reloading new/modified credentials.
|
||||
kpxc.retrieveCredentials = async function(force = false) {
|
||||
if (!isIframeAllowed()) {
|
||||
if (!await isIframeAllowed()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -619,7 +619,7 @@ kpxc.retrieveCredentialsCallback = async function(credentials) {
|
|||
// If credentials are not received, request them again
|
||||
kpxc.receiveCredentialsIfNecessary = async function() {
|
||||
if (kpxc.credentials.length === 0 && !_called.retrieveCredentials) {
|
||||
if (!isIframeAllowed()) {
|
||||
if (!await isIframeAllowed()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -968,12 +968,19 @@ kpxc.reconnect = async function() {
|
|||
return true;
|
||||
};
|
||||
|
||||
// Check for Cross-domain security error when inspecting window.top.location.href
|
||||
const isIframeAllowed = function() {
|
||||
const isIframeAllowed = async function() {
|
||||
try {
|
||||
// Check for Cross-domain security error when inspecting window.top.location.href
|
||||
const currentLocation = window.top.location.href;
|
||||
return true;
|
||||
} catch (err) {
|
||||
// Inspect iframe using TLD and the tab's original URL
|
||||
const allowed = await sendMessage('is_iframe_allowed', [ window.location.href, window.location.hostname ]);
|
||||
if (allowed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Allow user to add a manual exception for iframes with this tab URL
|
||||
logDebug(`Error: Credential request ignored from another domain: ${window.self.location.host}`);
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@
|
|||
"permissions": [
|
||||
"activeTab",
|
||||
"contextMenus",
|
||||
"cookies",
|
||||
"clipboardWrite",
|
||||
"nativeMessaging",
|
||||
"notifications",
|
||||
|
|
|
|||
|
|
@ -72,18 +72,40 @@ async function testGeneral() {
|
|||
assertRegex(siteMatch(m[0], m[1]), m[2], testCard, `siteMatch() for ${m[1]}`);
|
||||
}
|
||||
|
||||
// Base domain parsing (window.location.hostname)
|
||||
// Top-Level-Domain tests
|
||||
const tldUrls = [
|
||||
[ 'another.example.co.uk', 'co.uk' ],
|
||||
[ 'www.example.com', 'com' ],
|
||||
[ 'github.com', 'com' ],
|
||||
[ 'test.net', 'net' ],
|
||||
[ 'so.many.subdomains.co.jp', 'co.jp' ],
|
||||
[ '192.168.0.1', '192.168.0.1' ],
|
||||
[ 'www.nic.ar','ar' ],
|
||||
[ 'no.no.no', 'no' ],
|
||||
[ 'www.blogspot.com.ar', 'blogspot.com.ar' ], // blogspot.com.ar is a TLD
|
||||
[ 'jap.an.ide.kyoto.jp', 'ide.kyoto.jp' ], // ide.kyoto.jp is a TLD
|
||||
];
|
||||
|
||||
for (const d of domains) {
|
||||
kpxcAssert(page.getTopLevelDomainFromUrl(d[0]), d[1], testCard, 'getTopLevelDomainFromUrl() for ' + d[0]);
|
||||
}
|
||||
|
||||
// Base domain tests
|
||||
const domains = [
|
||||
[ 'another.example.co.uk', 'example.co.uk' ],
|
||||
[ 'www.example.com', 'example.com' ],
|
||||
[ 'test.net', 'test.net' ],
|
||||
[ 'so.many.subdomains.co.jp', 'subdomains.co.jp' ],
|
||||
[ 'test.site.example.com.au', 'example.com.au' ],
|
||||
[ '192.168.0.1', '192.168.0.1' ]
|
||||
[ '192.168.0.1', '192.168.0.1' ],
|
||||
[ 'www.nic.ar', 'nic.ar' ],
|
||||
[ 'www.blogspot.com.ar', 'www.blogspot.com.ar' ], // blogspot.com.ar is a TLD
|
||||
[ 'www.arpa', 'www.arpa' ],
|
||||
[ 'jap.an.ide.kyoto.jp', 'an.ide.kyoto.jp' ], // ide.kyoto.jp is a TLD
|
||||
[ 'kobe.jp', 'kobe.jp' ],
|
||||
];
|
||||
|
||||
for (const d of domains) {
|
||||
kpxcAssert(getTopLevelDomainFromUrl(d[0]), d[1], testCard, 'getBaseDomainFromUrl() for ' + d[0]);
|
||||
kpxcAssert(page.getBaseDomainFromUrl(d[0]), d[1], testCard, 'getBaseDomainFromUrl() for ' + d[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue