Skip to content

Commit

Permalink
Merge pull request #2079 from keepassxreboot/feature/top-level_domain…
Browse files Browse the repository at this point in the history
…_check

Add support for TLD check and allowing Cross-Origin iframes option
  • Loading branch information
varjolintu authored Jan 27, 2024
2 parents 6b3e189 + b91820f commit d321dbf
Show file tree
Hide file tree
Showing 14 changed files with 248 additions and 44 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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

Expand Down
16 changes: 16 additions & 0 deletions keepassxc-browser/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@
"message": "Add Username-only option for the site",
"description": "Button text for adding Username-only option."
},
"popupAllowIframeButton": {
"message": "Allow Cross-Origin iframes for the site",
"description": "Button text for allowing Cross-Origin iframes option."
},
"popupErrorEncountered": {
"message": "KeePassXC-Browser has encountered an error:",
"description": "A text shown above error message in the popup."
Expand Down Expand Up @@ -447,6 +451,10 @@
"message": "Only a single username field was detected. Add the URL to Site Preferences with Username-only option enabled?",
"description": "Text shown when page can be added to Site Preferences with Username-only option enabled."
},
"popupIframeDetected": {
"message": "A Cross-Origin iframe was detected. Add the URL to Site Preferences with Allow Cross-Origin iframes option enabled?",
"description": "Text shown when page can be added to Site Preferences with Allow Cross-Origin iframes option enabled."
},
"rememberInfoText": {
"message": "Username or password changed! Save it?",
"description": "Message when username or password has changed."
Expand Down Expand Up @@ -1015,6 +1023,10 @@
"message": "Improved Input Field Detection",
"description": "Improved Input Field Detection column text."
},
"optionsColumnAllowIframes": {
"message": "Allow Cross-Origin iframes",
"description": "Allow iframes column text."
},
"optionsColumnDelete": {
"message": "Delete",
"description": "Site preferences list column title."
Expand Down Expand Up @@ -1135,6 +1147,10 @@
"message": "Improved Input Field Detection allows more detailed dynamic input field detection. However, it might affect the page performance. Use with caution.",
"description": "Improved Input Field Detection help text."
},
"optionsSitePreferencesAllowIframesHelpText": {
"message": "Allowing Cross-Origin iframes will enable credential retrieval for iframes from another domain. Use at your own risk.",
"description": "Allow Cross-Origin iframes help text."
},
"optionsSitePreferencesManualAddText": {
"message": "Add URL manually",
"description": "Label for adding site manually on Site preferences tab."
Expand Down
20 changes: 15 additions & 5 deletions keepassxc-browser/background/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,21 @@ kpxcEvent.showStatus = async function(tab, configured, internalPoll) {

const errorMessage = page.tabs[tab.id]?.errorMessage ?? undefined;
const usernameFieldDetected = page.tabs[tab.id]?.usernameFieldDetected ?? false;
const iframeDetected = page.tabs[tab.id]?.iframeDetected ?? false;

return {
identifier: keyId,
associated: keepass.isAssociated(),

configured: configured,
databaseClosed: keepass.isDatabaseClosed,
keePassXCAvailable: keepass.isKeePassXCAvailable,
encryptionKeyUnrecognized: keepass.isEncryptionKeyUnrecognized,
associated: keepass.isAssociated(),
error: errorMessage,
usernameFieldDetected: usernameFieldDetected,
iframeDetected: iframeDetected,
identifier: keyId,
keePassXCAvailable: keepass.isKeePassXCAvailable,
showGettingStartedGuideAlert: page.settings.showGettingStartedGuideAlert,
showTroubleshootingGuideAlert: page.settings.showTroubleshootingGuideAlert
showTroubleshootingGuideAlert: page.settings.showTroubleshootingGuideAlert,
usernameFieldDetected: usernameFieldDetected
};
};

Expand Down Expand Up @@ -177,6 +180,10 @@ kpxcEvent.onUsernameFieldDetected = async function(tab, detected) {
page.tabs[tab.id].usernameFieldDetected = detected;
};

kpxcEvent.onIframeDetected = async function(tab, detected) {
page.tabs[tab.id].iframeDetected = detected;
};

kpxcEvent.passwordGetFilled = async function() {
return page.passwordFilled;
};
Expand Down Expand Up @@ -251,8 +258,10 @@ kpxcEvent.messageHandlers = {
'get_totp': keepass.getTotp,
'hide_getting_started_guide_alert': kpxcEvent.hideGettingStartedGuideAlert,
'hide_troubleshooting_guide_alert': kpxcEvent.hideTroubleshootingGuideAlert,
'iframe_detected': kpxcEvent.onIframeDetected,
'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,
Expand All @@ -263,6 +272,7 @@ kpxcEvent.messageHandlers = {
'page_get_manual_fill': page.getManualFill,
'page_get_redirect_count': kpxcEvent.pageGetRedirectCount,
'page_get_submitted': page.getSubmitted,
'page_set_allow_iframes': page.setAllowIframes,
'page_set_autosubmit_performed': page.setAutoSubmitPerformed,
'page_set_login_id': page.setLoginId,
'page_set_manual_fill': page.setManualFill,
Expand Down
2 changes: 1 addition & 1 deletion keepassxc-browser/background/keepass.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
98 changes: 98 additions & 0 deletions keepassxc-browser/background/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ page.clearLogins = function(tabId) {
return;
}

page.tabs[tabId].allowIframes = false;
page.tabs[tabId].credentials = [];
page.tabs[tabId].loginList = [];
page.currentRequest = {};
Expand Down Expand Up @@ -186,6 +187,7 @@ page.clearSubmittedCredentials = async function() {

page.createTabEntry = function(tabId) {
page.tabs[tabId] = {
allowIframes: false,
credentials: [],
errorMessage: null,
loginList: [],
Expand Down Expand Up @@ -330,6 +332,101 @@ page.updatePopup = function(tab) {
browserAction.showDefault(tab);
};

page.setAllowIframes = async function(tab, args = []) {
const [ allowIframes, site ] = args;

// Only set when main windows' URL is used
if (tab?.url === site) {
page.tabs[tab.id].allowIframes = allowIframes;
}
};

page.isIframeAllowed = async function(tab, args = []) {
const [ url, hostname ] = args;
const baseDomain = await page.getBaseDomainFromUrl(hostname, url);

// Allow if exception has been set from Site Preferences
if (page.tabs[tab.id]?.allowIframes) {
return true;
}

// 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,
Expand All @@ -345,6 +442,7 @@ const createContextMenuItem = function({ action, args, ...options }) {
});
};


const logDebug = function(message, extra) {
if (page.settings.debugLogging) {
debugLogMessage(message, extra);
Expand Down
13 changes: 0 additions & 13 deletions keepassxc-browser/common/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
42 changes: 31 additions & 11 deletions keepassxc-browser/content/keepassxc-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ kpxc.singleInputEnabledForPage = false;
kpxc.submitUrl = null;
kpxc.url = null;

// Add page to Site Preferences with Username-only detection enabled. Set from the popup
kpxc.addToSitePreferences = async function() {
// Add page to Site Preferences with a selected option enabled. Set from the popup.
kpxc.addToSitePreferences = async function(optionName, addWildcard = false) {
// Returns a predefined URL for certain sites
let site = trimURL(window.top.location.href).toLowerCase();

Expand All @@ -37,24 +37,32 @@ kpxc.addToSitePreferences = async function() {
for (const existingSite of kpxc.settings['sitePreferences']) {
if (existingSite.url === site) {
existingSite.ignore = IGNORE_NOTHING;
existingSite.usernameOnly = true;
existingSite[optionName] = true;
siteExists = true;
}
}

if (!siteExists) {
// Add wildcard to the URL
site = site.slice(0, site.lastIndexOf('/') + 1) + '*';
if (addWildcard) {
site = site.slice(0, site.lastIndexOf('/') + 1) + '*';
}

kpxc.settings['sitePreferences'].push({
url: site,
ignore: IGNORE_NOTHING,
usernameOnly: true
[optionName]: true
});
}

await sendMessage('save_settings', kpxc.settings);
sendMessage('username_field_detected', false);

if (optionName === 'allowIframes') {
await sendMessage('page_set_allow_iframes', [ true, site ]);
await sendMessage('iframe_detected', false);
} else if (optionName === 'usernameOnly') {
await sendMessage('username_field_detected', false);
}
};

// Clears all from the content and background scripts, including autocomplete
Expand Down Expand Up @@ -584,7 +592,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 [];
}

Expand Down Expand Up @@ -623,7 +631,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 [];
}

Expand Down Expand Up @@ -710,6 +718,7 @@ kpxc.siteIgnored = async function(condition) {
currentLocation = window.self.location.href.toLowerCase();
}

// Refresh current settings for the site
const currentSetting = condition || IGNORE_FULL;
for (const site of kpxc.settings.sitePreferences) {
if (siteMatch(site.url, currentLocation) || site.url === currentLocation) {
Expand All @@ -719,6 +728,7 @@ kpxc.siteIgnored = async function(condition) {

kpxc.singleInputEnabledForPage = site.usernameOnly;
kpxc.improvedFieldDetectionEnabledForPage = site.improvedFieldDetection;
await sendMessage('page_set_allow_iframes', [ site.allowIframes, currentLocation ]);
}
}

Expand Down Expand Up @@ -901,8 +911,10 @@ browser.runtime.onMessage.addListener(async function(req, sender) {

if (req.action === 'activated_tab') {
kpxc.triggerActivatedTab();
} else if (req.action === 'add_allow_iframes_option') {
kpxc.addToSitePreferences('allowIframes');
} else if (req.action === 'add_username_only_option') {
kpxc.addToSitePreferences();
kpxc.addToSitePreferences('usernameOnly', true);
} else if (req.action === 'check_database_hash' && 'hash' in req) {
kpxc.detectDatabaseChange(req);
} else if (req.action === 'choose_credential_fields') {
Expand Down Expand Up @@ -972,13 +984,21 @@ 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() {
sendMessage('iframe_detected', false);
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;
}

logDebug(`Error: Credential request ignored from another domain: ${window.self.location.host}`);
sendMessage('iframe_detected', true);
return false;
}
};
1 change: 1 addition & 0 deletions keepassxc-browser/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
"permissions": [
"activeTab",
"contextMenus",
"cookies",
"clipboardWrite",
"nativeMessaging",
"notifications",
Expand Down
Loading

0 comments on commit d321dbf

Please sign in to comment.