Add policy-bound credential submission and native DLP audit proofs

This commit is contained in:
Karl committed 2026-10-13 21:00:41 +02:00
1 parent b991ee9da6
commit c8b6ff9cdd
45 files changed
+777 -98

No files matched your search

+20 -5
View File
@@ -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;