1
0
mirror of https://gitlab.com/psono/psono-client synced 2025-04-19 03:22:16 +03:00

Improving autofill in iframes

Signed-off-by: Sascha Pfeiffer <sascha.pfeiffer@esaqa.com>
This commit is contained in:
Sascha Pfeiffer 2025-04-04 21:58:15 +02:00
parent fd88cb1d1b
commit f84b7a0808
3 changed files with 184 additions and 24 deletions

View File

@ -299,7 +299,7 @@ const ClassWorkerContentScript = function (base, browser, setTimeout) {
let buttonContent = otherButtons[i].tagName.toLowerCase() === "input" ? otherButtons[i].value : otherButtons[i].innerText;
if (passwordSubmitButtonLabels.has(buttonContent.toLowerCase().trim())) {
submitButtons.append(otherButtons[i]);
submitButtons.push(otherButtons[i]);
}
}
@ -1044,6 +1044,108 @@ const ClassWorkerContentScript = function (base, browser, setTimeout) {
}
}
/**
* parses an URL and returns an object with all details separated
*
* @param {String} url The url to be parsed
* @returns {object} Returns the split up url
*/
function parseUrl(url) {
const empty = {
scheme: null,
authority: null,
authority_without_www: null,
base_url: null,
full_domain: null,
full_domain_without_www: null,
port: null,
path: null,
query: null,
fragment: null
};
if (!url) {
return empty;
}
if (!url.includes("://")) {
// Its supposed to be an url but doesn't include a schema so let's prefix it with http://
url = 'http://' + url;
}
let parsedUrl;
try {
parsedUrl = new URL(url);
} catch (e) {
return empty;
}
return {
scheme: parsedUrl.protocol.slice(0,-1),
base_url: parsedUrl.protocol + '//' + parsedUrl.host,
authority: parsedUrl.host,
authority_without_www: parsedUrl.host ? parsedUrl.host.replace(/^(www\.)/, ""): parsedUrl.host, //remove leading www.
full_domain: parsedUrl.hostname,
full_domain_without_www: parsedUrl.hostname ? parsedUrl.hostname.replace(/^(www\.)/, "") : parsedUrl.hostname,
port: parsedUrl.port || null,
path: parsedUrl.pathname,
query: parsedUrl.search || null,
fragment: parsedUrl.hash ? parsedUrl.hash.substring(1) : null,
};
}
/**
* Checks if a provided urlfilter and authority fit together
*
* @param {string} authority The "authority" of the current website, e.g. www.example.com:80
* @param {string} urlFilter The url filter, e.g. *.example.com or www.example.com
*
* @returns {boolean} Whether the string ends with the suffix or not
*/
function isUrlFilterMatch(authority, urlFilter) {
if (!authority || !urlFilter) {
return false
}
authority = authority.toLowerCase();
urlFilter = urlFilter.toLowerCase();
let directMatch = authority === urlFilter;
let wildcardMatch = urlFilter.startsWith('*.') && authority.endsWith(urlFilter.substring(1));
return directMatch || wildcardMatch
}
/**
* Returns the function that returns whether a certain leaf entry should be considered a possible condidate
* for a provided url
*
* @param {string} url The url to match
*
* @returns {(function(*): (boolean|*))|*}
*/
const getSearchWebsitePasswordsByUrlfilter = function (url) {
const parsedUrl = parseUrl(url);
const filter = function (leaf) {
if (typeof leaf.website_password_url_filter === "undefined") {
return false;
}
if (leaf.website_password_url_filter) {
const urlFilters = leaf.website_password_url_filter.split(/\s+|,|;/);
for (let i = 0; i < urlFilters.length; i++) {
if (!isUrlFilterMatch(parsedUrl.authority, urlFilters[i])) {
continue;
}
return parsedUrl.scheme === 'https' || (leaf.hasOwnProperty("allow_http") && leaf["allow_http"]);
}
}
return false;
};
return filter;
};
/**
* handles password request answer
*
@ -1051,36 +1153,61 @@ const ClassWorkerContentScript = function (base, browser, setTimeout) {
* @param sender
* @param sendResponse
*/
function onReturnSecret(data, sender, sendResponse) {
for (let i = 0; i < myForms.length; i++) {
if (
(myForms[i].username && myForms[i].username.isEqualNode(lastRequestElement)) ||
(myForms[i].password && myForms[i].password.isEqualNode(lastRequestElement)) ||
fillAll
) {
fillFieldHelper(myForms[i].username, data.website_password_username);
fillFieldHelper(myForms[i].password, data.website_password_password);
async function onReturnSecret(data, sender, sendResponse) {
for (let ii = 0; ii < dropInstances.length; ii++) {
dropInstances[ii].close();
}
dropInstances = [];
if (!fillAll) {
break;
const isIframe = window !== window.top;
function autofill() {
for (let i = 0; i < myForms.length; i++) {
if (
(myForms[i].username && myForms[i].username.isEqualNode(lastRequestElement)) ||
(myForms[i].password && myForms[i].password.isEqualNode(lastRequestElement)) ||
fillAll
) {
fillFieldHelper(myForms[i].username, data.website_password_username);
fillFieldHelper(myForms[i].password, data.website_password_password);
for (let ii = 0; ii < dropInstances.length; ii++) {
dropInstances[ii].close();
}
dropInstances = [];
if (!fillAll) {
break;
}
}
}
}
if (data.hasOwnProperty('custom_fields') && data.custom_fields) {
for (let i = 0; i < data.custom_fields.length; i++) {
const field = findFieldByName(data.custom_fields[i].name);
if (field) {
fillFieldHelper(field, data.custom_fields[i].value);
if (data.hasOwnProperty('custom_fields') && data.custom_fields) {
for (let i = 0; i < data.custom_fields.length; i++) {
const field = findFieldByName(data.custom_fields[i].name);
if (field) {
fillFieldHelper(field, data.custom_fields[i].value);
}
}
}
fillAll = false;
}
fillAll=false;
if (isIframe) {
const filter = getSearchWebsitePasswordsByUrlfilter(window.location.origin);
if (!filter(data)) {
const parsedUrl = parseUrl(window.location.origin);
const autofillId = uuid.v4();
await base.emit("approve-iframe-login", {
'authority': parsedUrl.authority,
'autofill_id': autofillId
}, (response) => {
if (response.data) {
autofill()
}
});
} else {
autofill()
}
} else {
autofill()
}
}
/**

View File

@ -1,4 +1,6 @@
{
"APPROVE_IFRAME_LOGIN": "Different origin detected",
"APPROVE_IFRAME_LOGIN_DESCRIPTION": "The form is hosted on an origin that is not in your url filter. Choose approve to autofill anyway or cancel to stop. To avoid this message from popping up again, open advanced and add {{origin}} to your url filters.",
"CONFIG_JSON_MALFORMED": "The json format of your the config.json seems to be malformed. Once fixed make sure to test in an private / incognito browser tab.",
"INVALID_TOTP_PARAMETERS": "Invalid TOTP parameters.",
"ACCOUNT_DELETED_SUCCESSFULLY": "Your user account with all its data has been deleted successfully.",

View File

@ -332,6 +332,7 @@ function fillSecretTab(secretId, tab) {
const onSuccess = function (content) {
if (leaf.type === 'website_password') {
browserClient.emitTab(tab.id, "fillpassword", {
username: content.website_password_username,
password: content.website_password_password,
@ -394,6 +395,7 @@ function onMessage(request, sender, sendResponse) {
"request-secret": onRequestSecret,
"open-tab": onOpenTab,
"generate-password": onGeneratePassword,
"approve-iframe-login": approveIframeLogin,
"login-form-submit": loginFormSubmit,
"oidc-saml-redirect-detected": oidcSamlRedirectDetected,
"decrypt-gpg": decryptPgp,
@ -770,7 +772,7 @@ function onWebsitePasswordRefresh(request, sender, sendResponse) {
return;
}
searchWebsitePasswordsByUrlfilter(sender.url, false).then(function (leafs) {
searchWebsitePasswordsByUrlfilter(sender.tab.url, false).then(function (leafs) {
const update = [];
for (let ii = 0; ii < leafs.length; ii++) {
@ -1398,6 +1400,35 @@ function onAuthRequired(details, callbackFn) {
});
}
/**
* Being fired once a content script wants to ask a user to approve iframe login
*
* @returns {Promise}
*/
function approveIframeLogin(request, sender, sendResponse) {
notificationBarService.create(
i18n.t("APPROVE_IFRAME_LOGIN"),
i18n.t("APPROVE_IFRAME_LOGIN_DESCRIPTION", {'origin': request.data.origin}),
[
{
title: i18n.t("ALLOW"),
onClick: () => {
sendResponse({ event: "approve-iframe-login-response", data: true });
},
color: "primary",
},
{
title: i18n.t("CANCEL"),
onClick: () => {
sendResponse({ event: "approve-iframe-login-response", data: false });
},
},
],
)
return true; // Important, do not remove! Otherwise Async return wont work
}
/**
* Saves the last login credentials in the datastore
*