Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for TLD check and allowing Cross-Origin iframes option #2079

Merged
merged 3 commits into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1007,6 +1015,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 @@ -1127,6 +1139,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 @@ -154,6 +154,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 @@ -185,6 +186,7 @@ page.clearSubmittedCredentials = async function() {

page.createTabEntry = function(tabId) {
page.tabs[tabId] = {
allowIframes: false,
credentials: [],
errorMessage: null,
loginList: [],
Expand Down Expand Up @@ -329,6 +331,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 @@ -344,6 +441,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 @@ -580,7 +588,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 @@ -619,7 +627,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 @@ -706,6 +714,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 @@ -715,6 +724,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 @@ -897,8 +907,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 @@ -968,13 +980,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