Add policy-bound credential submission and native DLP audit proofs
This commit is contained in:
1 parent
b991ee9da6
commit
c8b6ff9cdd
45 files changed
+777
-98
No files matched your search
@@ -1,12 +1,12 @@
|
||||
'use strict';
|
||||
// Runs only in the isolated main-frame world. No arbitrary selectors or scripts from callers.
|
||||
function fillDocument(expectedUrl,origin,username,password,expiresAt){
|
||||
function fillDocument(expectedUrl,origin,username,password,expiresAt,approvedForms){
|
||||
if(Date.now()>=expiresAt||window.top!==window||!window.isSecureContext||location.href!==expectedUrl||location.origin!==origin)throw Error('Login destination changed');
|
||||
const visible=element=>{
|
||||
const style=getComputedStyle(element);
|
||||
return !element.disabled&&!element.readOnly&&!element.closest('[hidden],[inert],[aria-hidden="true"]')&&style.display!=='none'&&style.visibility!=='hidden'&&Number(style.opacity)!==0&&element.getClientRects().length>0;
|
||||
if(element.disabled||element.readOnly||element.closest('[hidden],[inert],[aria-hidden="true"],[aria-disabled="true"],fieldset[disabled]')||element.getClientRects().length===0)return false;
|
||||
for(let node=element;node;node=node.parentElement){const style=getComputedStyle(node);if(style.display==='none'||style.visibility==='hidden'||Number(style.opacity)===0)return false;}return true;
|
||||
};
|
||||
const passwords=[...document.querySelectorAll('input[type="password"]')].filter(element=>visible(element)&&element.autocomplete!=='new-password');
|
||||
const passwords=[...document.querySelectorAll('input[type="password"]')].filter(element=>visible(element)&&!String(element.autocomplete||'').toLowerCase().split(/\s+/).includes('new-password'));
|
||||
if(passwords.length!==1)throw Error('A single visible login password field is required');
|
||||
const target=passwords[0],form=target.form;
|
||||
if(!form||form.method.toLowerCase()!=='post'||!['','_self'].includes(form.target.toLowerCase()))throw Error('A same-window POST login form is required');
|
||||
@@ -25,6 +25,14 @@ function fillDocument(expectedUrl,origin,username,password,expiresAt){
|
||||
if(username&&candidates.length!==1)throw Error('A single login username field is required');
|
||||
const user=username?candidates[0]:null;
|
||||
if(user&&user.form!==form)throw Error('Login fields belong to different forms');
|
||||
let approved;
|
||||
if(approvedForms!==undefined){
|
||||
if(!Array.isArray(approvedForms)||approvedForms.length>32||!user||form.enctype.toLowerCase()!=='application/x-www-form-urlencoded')throw Error('Controlled login form is unsupported');
|
||||
const matches=approvedForms.filter(rule=>rule.actionUrl===action.href&&rule.usernameField===user.name&&rule.passwordField===target.name);
|
||||
if(matches.length!==1||[...(form.elements||[])].filter(element=>element.name===user.name).length!==1||[...(form.elements||[])].filter(element=>element.name===target.name).length!==1)throw Error('Login form is not explicitly approved');
|
||||
approved=matches[0];
|
||||
for(const control of [...(form.elements||[])]){if(control.disabled||typeof control.getAttribute!=='function')continue;const destination=control.getAttribute('formaction'),encoding=control.getAttribute('formenctype');if(destination!==null&&new URL(destination||location.href,location.href).href!==action.href||encoding!==null&&encoding.toLowerCase()!=='application/x-www-form-urlencoded')throw Error('Controlled login submit differs');}
|
||||
}
|
||||
const setter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set;
|
||||
// Recheck inside the target renderer, after queued native dispatch. A hidden
|
||||
// or blurred document must not receive credentials after a tab switch.
|
||||
@@ -33,7 +41,7 @@ function fillDocument(expectedUrl,origin,username,password,expiresAt){
|
||||
if(user)setter.call(user,username);setter.call(target,password);
|
||||
if(user)user.dispatchEvent(new Event('input',{bubbles:true}));
|
||||
target.dispatchEvent(new Event('input',{bubbles:true}));
|
||||
return {completed:true};
|
||||
return {completed:true,...(approved?{formId:approved.id}:{})};
|
||||
}
|
||||
function fillScript(expectedUrl,credential,expiresAt=Date.now()+10000){
|
||||
const url=new URL(expectedUrl);
|
||||
@@ -42,3 +50,10 @@ function fillScript(expectedUrl,credential,expiresAt=Date.now()+10000){
|
||||
return `(${fillDocument.toString()})(${JSON.stringify(url.href)},${JSON.stringify(credential.origin)},${JSON.stringify(credential.username)},${JSON.stringify(credential.password)},${JSON.stringify(expiresAt)})`;
|
||||
}
|
||||
module.exports={fillDocument,fillScript};
|
||||
function prepareFillScript(expectedUrl,credential,forms,expiresAt=Date.now()+10000){
|
||||
const url=new URL(expectedUrl);if(url.protocol!=='https:'||url.username||url.password||url.origin!==credential.origin||!Array.isArray(forms)||!Number.isFinite(expiresAt)||expiresAt<=Date.now())throw Error('Approved controlled login required');
|
||||
const rules=forms.filter(row=>new URL(row.actionUrl).origin===credential.origin).map(({id,actionUrl,usernameField,passwordField})=>({id,actionUrl,usernameField,passwordField}));
|
||||
if(!rules.length)throw Error('No approved login endpoint');
|
||||
return `(${fillDocument.toString()})(${JSON.stringify(url.href)},${JSON.stringify(credential.origin)},${JSON.stringify(credential.username)},${JSON.stringify(credential.password)},${JSON.stringify(expiresAt)},${JSON.stringify(rules)})`;
|
||||
}
|
||||
module.exports.prepareFillScript=prepareFillScript;
|
||||
Reference in new issue
Block a user