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");
}
/**
* 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
*
@@ -239,29 +338,26 @@ async function handleMessage(settings, message, sendResponse) {
break;
case "fill":
try {
var tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
await chrome.tabs.executeScript(tab.id, { file: "js/inject.dist.js" });
// check login fields
if (message.login.fields.login === null) {
throw new Error("No login is available");
}
if (message.login.fields.secret === null) {
throw new Error("No password is available");
}
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" });
// get tab info
var targetTab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
// dispatch initial fill request
var filledFields = await fillFields(targetTab, message.login, ["login", "secret"]);
// no need to check filledFields, because fillFields() already throws an error if empty
sendResponse({ status: "ok", filledFields: filledFields });
} catch (e) {
sendResponse({
status: "error",
message: "Unable to fill credentials: " + e.toString()
});
try {
sendResponse({
status: "error",
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;
default:
@@ -335,7 +431,10 @@ async function parseFields(settings, login) {
// assign to 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];
break;
}

View File

@@ -73,24 +73,60 @@
*
* @since 3.0.0
*
* @param object login Login fields
* @param bool autoSubmit Whether to autosubmit the login form
* @param object request Form fill request
* @return void
*/
function fillLogin(login, autoSubmit = false) {
function fillLogin(request) {
var autoSubmit = false;
var filledFields = [];
// get the login form
var loginForm = form();
update(USERNAME_FIELDS, login.login, loginForm);
update(PASSWORD_FIELDS, login.secret, loginForm);
// don't attempt to fill non-secret forms unless non-secret filling is allowed
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);
if (password_inputs.length > 1) {
// There is likely a field asking for OTP code, so do not submit form just yet
password_inputs[1].select();
} else {
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);
// Try to submit the form, or focus on the submit button (based on user settings)
if (submit) {
if (autoSubmit) {
submit.click();
@@ -115,6 +151,9 @@
}
});
}
// finished filling things successfully
return filledFields;
}
/**

View File

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

View File

@@ -178,13 +178,6 @@ async function withLogin(action) {
handleError("Filling login details...", "notice");
break;
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");
break;
case "copyPassword":
@@ -207,6 +200,7 @@ async function withLogin(action) {
throw new Error(response.message);
} else {
switch (action) {
// fall through to update recent
case "fill":
case "copyPassword":
case "copyUsername":