Intelligently fill forms in iframes (#22)

This commit is contained in:
Erayd
2019-02-24 07:08:49 +13:00
committed by Maxim Baz
parent 348cf9be1b
commit 53ae12e603
4 changed files with 169 additions and 39 deletions

View File

@@ -60,6 +60,105 @@ function copyToClipboard(text) {
document.execCommand("copy"); document.execCommand("copy");
} }
/**
* Call injected form-fill code
*
* @param object tab Target tab
* @param object fillRequest Fill request details
* @param boolean allFrames Dispatch to all frames
* @param boolean allowForeign Allow foreign-origin iframes
* @param boolean allowNoSecret Allow forms that don't contain a password field
* @return array list of filled fields
*/
async function dispatchFill(
tab,
fillRequest,
allFrames = false,
allowForeign = false,
allowNoSecret = false
) {
fillRequest = Object.assign({}, fillRequest, {
allowForeign: allowForeign,
allowNoSecret: allowNoSecret
});
var filledFields = await chrome.tabs.executeScript(tab.id, {
allFrames: allFrames,
code: `window.browserpass.fillLogin(${JSON.stringify(fillRequest)});`
});
// simplify the list of filled fields
filledFields = filledFields
.reduce((fields, addFields) => fields.concat(addFields), [])
.reduce(function(fields, field) {
if (!fields.includes(field)) {
fields.push(field);
}
return fields;
}, []);
return filledFields;
}
/**
* Fill form fields
*
* @param object tab Target tab
* @param object login Login object
* @param array fields List of fields to fill
* @return array List of filled fields
*/
async function fillFields(tab, login, fields) {
// check that required fields are present
for (var field of fields) {
if (login.fields[field] === null) {
throw new Error(`Required field is missing: ${field}`);
}
}
// inject script
await chrome.tabs.executeScript(tab.id, {
allFrames: true,
file: "js/inject.dist.js"
});
// build fill request
var fillRequest = {
origin: new URL(tab.url).origin,
login: login,
fields: fields
};
// fill form via injected script
var filledFields = await dispatchFill(tab, fillRequest);
// try again using same-origin frames if we couldn't fill a password field
if (!filledFields.includes("secret")) {
filledFields = filledFields.concat(await dispatchFill(tab, fillRequest, true));
}
// try again using all available frames if we couldn't fill a password field
if (!filledFields.includes("secret")) {
filledFields = filledFields.concat(await dispatchFill(tab, fillRequest, true, true));
}
// try again using same-origin frames, and don't require a password field
if (!filledFields.length) {
filledFields = filledFields.concat(await dispatchFill(tab, fillRequest, true, false, true));
}
// try again using all available frames, and don't require a password field
if (!filledFields.length) {
filledFields = filledFields.concat(await dispatchFill(tab, fillRequest, true, true, true));
}
if (!filledFields.length) {
throw new Error(`No fillable forms available for fields: ${fields.join(", ")}`);
}
return filledFields;
}
/** /**
* Get Local settings from the extension * Get Local settings from the extension
* *
@@ -239,29 +338,26 @@ async function handleMessage(settings, message, sendResponse) {
break; break;
case "fill": case "fill":
try { try {
var tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0]; // get tab info
await chrome.tabs.executeScript(tab.id, { file: "js/inject.dist.js" }); var targetTab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
// check login fields
if (message.login.fields.login === null) { // dispatch initial fill request
throw new Error("No login is available"); var filledFields = await fillFields(targetTab, message.login, ["login", "secret"]);
}
if (message.login.fields.secret === null) { // no need to check filledFields, because fillFields() already throws an error if empty
throw new Error("No password is available"); sendResponse({ status: "ok", filledFields: filledFields });
}
var fillFields = JSON.stringify({
login: message.login.fields.login,
secret: message.login.fields.secret
});
// fill form via injected script
await chrome.tabs.executeScript(tab.id, {
code: `window.browserpass.fillLogin(${fillFields});`
});
sendResponse({ status: "ok" });
} catch (e) { } catch (e) {
try {
sendResponse({ sendResponse({
status: "error", status: "error",
message: "Unable to fill credentials: " + e.toString() message: e.toString()
}); });
} catch (e) {
// TODO An error here is typically a closed message port, due to a popup taking focus
// away from the extension menu and the menu closing as a result. Need to investigate
// whether triggering the extension menu from the background script is possible.
console.log(e);
}
} }
break; break;
default: default:
@@ -335,7 +431,10 @@ async function parseFields(settings, login) {
// assign to fields // assign to fields
for (var key in login.fields) { for (var key in login.fields) {
if (Array.isArray(login.fields[key]) && login.fields[key].indexOf(parts[0]) >= 0) { if (
Array.isArray(login.fields[key]) &&
login.fields[key].indexOf(parts[0].toLowerCase()) >= 0
) {
login.fields[key] = parts[1]; login.fields[key] = parts[1];
break; break;
} }

View File

@@ -73,24 +73,60 @@
* *
* @since 3.0.0 * @since 3.0.0
* *
* @param object login Login fields * @param object request Form fill request
* @param bool autoSubmit Whether to autosubmit the login form
* @return void * @return void
*/ */
function fillLogin(login, autoSubmit = false) { function fillLogin(request) {
var autoSubmit = false;
var filledFields = [];
// get the login form
var loginForm = form(); var loginForm = form();
update(USERNAME_FIELDS, login.login, loginForm); // don't attempt to fill non-secret forms unless non-secret filling is allowed
update(PASSWORD_FIELDS, login.secret, loginForm); if (!find(PASSWORD_FIELDS, loginForm) && !request.allowNoSecret) {
return filledFields;
}
// ensure the origin is the same, or ask the user for permissions to continue
if (window.location.origin !== request.origin) {
var message =
"You have requested to fill login credentials into an embedded document from a " +
"different origin than the main document in this tab. Do you wish to proceed?\n\n" +
`Tab origin: ${request.origin}\n` +
`Embedded origin: ${window.location.origin}`;
if (!request.allowForeign || !confirm(message)) {
return filledFields;
}
}
// fill login field
if (
request.fields.includes("login") &&
update(USERNAME_FIELDS, request.login.fields.login, loginForm)
) {
filledFields.push("login");
}
// fill secret field
if (
request.fields.includes("secret") &&
update(PASSWORD_FIELDS, request.login.fields.secret, loginForm)
) {
filledFields.push("secret");
}
// check for multiple password fields in the login form
var password_inputs = queryAllVisible(document, PASSWORD_FIELDS, loginForm); var password_inputs = queryAllVisible(document, PASSWORD_FIELDS, loginForm);
if (password_inputs.length > 1) { if (password_inputs.length > 1) {
// There is likely a field asking for OTP code, so do not submit form just yet // There is likely a field asking for OTP code, so do not submit form just yet
password_inputs[1].select(); password_inputs[1].select();
} else { } else {
window.requestAnimationFrame(function() { window.requestAnimationFrame(function() {
// Try to submit the form, or focus on the submit button (based on user settings) // try to locate the submit button
var submit = find(SUBMIT_FIELDS, loginForm); var submit = find(SUBMIT_FIELDS, loginForm);
// Try to submit the form, or focus on the submit button (based on user settings)
if (submit) { if (submit) {
if (autoSubmit) { if (autoSubmit) {
submit.click(); submit.click();
@@ -115,6 +151,9 @@
} }
}); });
} }
// finished filling things successfully
return filledFields;
} }
/** /**

View File

@@ -25,9 +25,7 @@
"permissions": [ "permissions": [
"activeTab", "activeTab",
"nativeMessaging", "nativeMessaging",
"notifications" "notifications",
],
"optional_permissions": [
"webRequest", "webRequest",
"webRequestBlocking", "webRequestBlocking",
"http://*/*", "http://*/*",

View File

@@ -178,13 +178,6 @@ async function withLogin(action) {
handleError("Filling login details...", "notice"); handleError("Filling login details...", "notice");
break; break;
case "launch": case "launch":
var havePermission = await chrome.permissions.request({
permissions: ["webRequest", "webRequestBlocking"],
origins: ["http://*/*", "https://*/*"]
});
if (!havePermission) {
throw new Error("Browserpass requires additional permissions to proceed");
}
handleError("Launching URL...", "notice"); handleError("Launching URL...", "notice");
break; break;
case "copyPassword": case "copyPassword":
@@ -207,6 +200,7 @@ async function withLogin(action) {
throw new Error(response.message); throw new Error(response.message);
} else { } else {
switch (action) { switch (action) {
// fall through to update recent
case "fill": case "fill":
case "copyPassword": case "copyPassword":
case "copyUsername": case "copyUsername":