Harden Windows DLP enforcement and Electron runtime packaging

This commit is contained in:
Karl committed 2026-10-13 23:19:22 +02:00
1 parent e7d778aeac
commit 8a253eda89
120 files changed
+3569 -135

No files matched your search

+3
View File
@@ -18,11 +18,14 @@ function createCredentialSubmissionBroker({isCurrent,traceOperation=(_operation,
// bounded check covers two whole header-value fragments in either order.
const pairCredential=(l,values)=>stage('credential.match',()=>{if(!Array.isArray(values)||values.length>128)fail();let chars=0;const variants=[];for(let index=0;index<values.length;index++){let value=values[index];if(typeof value!=='string')fail();const seen=new Set();for(let round=0;round<3;round++){if(seen.has(value))break;seen.add(value);chars+=value.length;if(chars>131072)fail();variants.push({index,value});try{const next=decodeURIComponent(value);if(next===value)break;value=next;}catch{break;}}}let work=0;for(const a of variants)for(const b of variants){if(a.index===b.index)continue;if(++work>65536)fail();const length=a.value.length+b.value.length;for(const part of ['username','password'])if(length===l[part].length&&match(l,part,a.value+b.value))return true;}return false;});
const native=(l,details,context)=>stage('credential.native',()=>{if(!Number.isSafeInteger(details.id)||details.id<0||details.method!=='POST'||details.resourceType!=='mainFrame'||details.url!==l.form.actionUrl||details.webContentsId!==context.webContents.id||!details.frame||details.frame!==context.mainFrame||details.initiatorOrigin!==new URL(l.context.sourceURL).origin)fail();});
// eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input.
function issue({context,formId,username,password,vault,vaultGeneration}){if(closed)fail();expire();if(blocked.size>=1024)fail();if(!context?.policy?.passwordManager?.controlledSubmission?.enabled||!context.token||!context.webContents||!context.mainFrame||context.webContents.mainFrame!==context.mainFrame||!vault||vault.status().locked||vault.accessGeneration()!==vaultGeneration||isCurrent(context,{phase:'issued'})!==true)fail();const forms=context.policy.passwordManager.controlledSubmission.forms;if(!Array.isArray(forms)||forms.length>32)fail();const form=forms.find(f=>f.id===formId);if(!form||!field.test(form.usernameField)||!field.test(form.passwordField)||form.usernameField===form.passwordField)fail();let action,source;try{action=new URL(form.actionUrl);source=new URL(context.sourceURL);}catch{fail();}if(action.protocol!=='https:'||source.protocol!=='https:'||action.origin!==source.origin||action.username||action.password||action.search||action.hash||action.href!==form.actionUrl)fail();if(typeof username!=='string'||typeof password!=='string'||!username||!password||username.length>512||password.length>4096||/[\u0000\uD800-\uDFFF]/u.test(username+password))fail();if(issued.size+pending.size+redirected.size>=32&&!issued.has(context.webContents.id))fail();const old=issued.get(context.webContents.id);if(old)wipe(old);const key=randomBytes(32),lease={context,actor:JSON.stringify(context.actor??null),form:{...form},vault,vaultGeneration,key,username:{digest:digest(key,username),length:username.length},password:{digest:digest(key,password),length:password.length},expires:now()+15000};issued.set(context.webContents.id,lease);return {prepared:true,expiresAt:lease.expires};}
// eslint-disable-next-line no-control-regex, no-useless-assignment -- Explicitly reject control characters in security-sensitive input. Explicitly clear sensitive transient text after use.
function claim(details,bytes,context){expire();if(redirected.has(details.id)){validateRedirect(details,context);return null;}if(blocked.has(details.id)||pending.has(details.id))fail();const l=issued.get(details.webContentsId);if(!l)return null;const candidate=details.method==='POST'&&details.resourceType==='mainFrame'&&details.url===l.form.actionUrl;let observed=false;try{observed=containsCredential(l,details.url);if(!candidate&&bytes?.length){if(!Buffer.isBuffer(bytes)||bytes.length>65536)fail();observed=observed||containsCredential(l,new TextDecoder('utf-8',{fatal:true}).decode(bytes));}}catch{observed=true;}if(!candidate&&!observed)return null;issued.delete(details.webContentsId);blocked.set(details.id,now()+60000);try{authority(l,context,'request',details);native(l,details,context);if(!Buffer.isBuffer(bytes)||bytes.length<1||bytes.length>65536||!Array.isArray(details.uploadData)||details.uploadData.some(p=>!p.bytes||p.file||p.blobUUID)||hash(Buffer.concat(details.uploadData.map(p=>Buffer.from(p.bytes))))!==hash(bytes))fail();const text=new TextDecoder('utf-8',{fatal:true}).decode(bytes);if(/[\u0000-\u001f\u007f]/.test(text)||text.split('&').length>128)fail();const entries=stage('credential.parse',()=>text.split('&').map(raw=>{const index=raw.indexOf('=');if(index<1||/%(?![0-9a-f]{2})/i.test(raw))fail();let name,value;try{name=decodeURIComponent(raw.slice(0,index).replace(/\+/g,' '));value=decodeURIComponent(raw.slice(index+1).replace(/\+/g,' '));}catch{fail();}if(!field.test(name)||/[\u0000-\u001f\u007f]/.test(value))fail();return {name,value,raw};}));if(new Set(entries.map(e=>e.name)).size!==entries.length)fail();const u=entries.find(e=>e.name===l.form.usernameField),p=entries.find(e=>e.name===l.form.passwordField);if(!u||!p||!stage('credential.match',()=>match(l,'username',u.value)&&match(l,'password',p.value)))fail();const others=entries.filter(e=>e!==u&&e!==p);if(others.some(e=>containsCredential(l,e.name+'\n'+e.value))||containsCredential(l,others.map(e=>e.value).join(''))||containsCredential(l,others.map(e=>e.name).join('')))fail();const residual=stage('credential.residual',()=>Buffer.from([others.map(e=>e.raw).join('&'),others.map(e=>e.name+'\n'+e.value).join('\n'),others.map(e=>e.value).join(''),entries.map(e=>e.name).join('\n')].join('\n')));l.bodyHash=hash(bytes);l.requestId=details.id;l.headersValidated=false;pending.set(details.id,l);return {requestId:details.id,residual,bytes:bytes.length};}catch(error){wipe(l);throw error;}}
function validateHeaders(details,context){expire();const l=pending.get(details.id);if(!l||l.finalized)fail();try{authority(l,context,'headers',details);native(l,details,context);const headers=details.requestHeaders;if(!headers||Object.keys(headers).length>128)fail();const names=Object.keys(headers);if(new Set(names.map(k=>k.toLowerCase())).size!==names.length)fail();const get=name=>{const found=names.filter(k=>k.toLowerCase()===name);if(found.length!==1)fail();const value=headers[found[0]];if(typeof value!=='string')fail();return value;};if(!/^application\/x-www-form-urlencoded(?:;\s*charset=utf-8)?$/i.test(get('content-type'))||names.some(k=>k.toLowerCase()==='content-encoding'))fail();if(details.uploadData&&hash(Buffer.concat(details.uploadData.map(p=>Buffer.from(p.bytes||[]))))!==l.bodyHash)fail();if(stage('credential.header-values',()=>containsCredential(l,details.url)||names.some(k=>containsCredential(l,k)||containsCredential(l,headers[k]))||containsCredential(l,names.map(k=>headers[k]).join(''))||containsCredential(l,names.join(''))||pairCredential(l,names.map(k=>headers[k]))||pairCredential(l,names)))fail();l.headersValidated=true;return {requestId:details.id};}catch(error){pending.delete(details.id);wipe(l);throw error;}}
function finalize(id,context,details){expire();const l=pending.get(id);if(!l||l.finalized||!l.headersValidated)fail();try{authority(l,context,'headers',details);native(l,details,context);l.finalized=true;return {authorized:true};}catch(error){pending.delete(id);wipe(l);throw error;}}
function validateRedirect(details,context){expire();const l=redirected.get(details.id);if(!l){if(blocked.has(details.id))fail();return false;}authority(l,context,'redirect',details);if(!['GET','HEAD'].includes(details.method)||details.uploadData?.length||details.resourceType!=='mainFrame'||details.webContentsId!==l.context.webContents.id||!details.frame||details.frame!==l.context.mainFrame||details.initiatorOrigin!==new URL(l.context.sourceURL).origin)fail();if(containsCredential(l,details.url))fail();if(details.requestHeaders){const headers=details.requestHeaders,names=Object.keys(headers);if(names.length>128||new Set(names.map(k=>k.toLowerCase())).size!==names.length)fail();for(const name of names)if(containsCredential(l,name)||containsCredential(l,headers[name]))fail();if(containsCredential(l,names.map(k=>headers[k]).join(''))||containsCredential(l,names.join(''))||pairCredential(l,names.map(k=>headers[k]))||pairCredential(l,names))fail();}return true;}
// eslint-disable-next-line no-useless-assignment -- Explicitly clear sensitive transient text after use.
function guardIssued(details,context){expire();if(pending.has(details.id)||redirected.has(details.id))return false;if(blocked.has(details.id))fail();const l=issued.get(details.webContentsId);if(!l)return false;let matched=false;try{authority(l,context,'watch',details);if(details.webContentsId!==l.context.webContents.id)fail();matched=containsCredential(l,details.url);if(details.requestHeaders){const entries=Object.entries(Object.getOwnPropertyDescriptors(details.requestHeaders));if(entries.length>128)fail();const names=[],values=[];for(const [name,descriptor]of entries){if(!('value'in descriptor)||typeof descriptor.value!=='string')fail();names.push(name);values.push(descriptor.value);if(containsCredential(l,name)||containsCredential(l,descriptor.value))matched=true;}if(new Set(names.map(n=>n.toLowerCase())).size!==names.length)fail();if(containsCredential(l,values.join(''))||containsCredential(l,names.join(''))||pairCredential(l,values)||pairCredential(l,names))matched=true;}if(!matched)return false;}catch(error){issued.delete(details.webContentsId);blocked.set(details.id,now()+60000);wipe(l);throw error;}issued.delete(details.webContentsId);blocked.set(details.id,now()+60000);wipe(l);fail();}
function complete(id){let released=false;for(const map of [pending,redirected]){const l=map.get(id);if(l){released=released||l.finalized===true;map.delete(id);wipe(l);}}blocked.delete(id);return {released};}
function redirect(id){const l=pending.get(id);if(!l)return;pending.delete(id);if(!l.finalized){blocked.set(id,now()+60000);wipe(l);return;}redirected.set(id,l);blocked.delete(id);}