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

+1 -1
View File
@@ -1,7 +1,7 @@
'use strict';
const {z}=require('zod');
const application=['vault:prepare-submit','sessions:save','sessions:list','sessions:restore','vault:status','vault:list','vault:fill','history:list','history:go','window:state','window:control','application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group','library:organize-plan','library:organize-apply'];
const AGENT_SCOPES=Object.freeze({screenshot:'browser:screenshot',sessions:'sessions:list',navigate:'browser:navigate',back:'browser:navigate',forward:'browser:navigate',reload:'browser:navigate',read:'browser:read',inspect:'browser:read',scroll:'browser:interact',click:'browser:interact',type:'browser:interact',select:'browser:interact',check:'browser:interact',ai:'ai:request',...Object.fromEntries(application.map(action=>[action,action])),'agent-request':'agent:request'});
const AGENT_SCOPES=Object.freeze({'network:background':'network:background',screenshot:'browser:screenshot',sessions:'sessions:list',navigate:'browser:navigate',back:'browser:navigate',forward:'browser:navigate',reload:'browser:navigate',read:'browser:read',inspect:'browser:read',scroll:'browser:interact',click:'browser:interact',type:'browser:interact',select:'browser:interact',check:'browser:interact',ai:'ai:request',...Object.fromEntries(application.map(action=>[action,action])),'agent-request':'agent:request'});
const AgentAuditSchema=z.object({action:z.enum(Object.keys(AGENT_SCOPES)),scope:z.enum([...new Set(Object.values(AGENT_SCOPES))]),outcome:z.enum(['authorized','completed','denied','failed']),sessionId:z.string().uuid().optional(),principalId:z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,79}$/).optional(),credentialGeneration:z.number().int().min(1).max(Number.MAX_SAFE_INTEGER).optional()}).strict().superRefine((value,ctx)=>{
if((value.principalId!==undefined)!==(value.credentialGeneration!==undefined))ctx.addIssue({code:'custom',message:'Agent principal attribution requires both identity and generation'});
if(AGENT_SCOPES[value.action]!==value.scope)ctx.addIssue({code:'custom',message:'Agent action scope mismatch'});
+1
View File
@@ -1,4 +1,5 @@
import { z } from 'zod';
// eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input.
export const BookmarkSchema=z.object({id:z.string().min(1).max(80).regex(/^[a-zA-Z0-9_-]+$/),title:z.string().trim().min(1).max(120).refine(v=>!/[\u0000-\u001f\u007f]/.test(v)),url:z.string().max(2048).url().refine(v=>{try{const u=new URL(v);return ['https:','http:'].includes(u.protocol)&&!u.username&&!u.password&&!u.search&&!u.hash;}catch{return false;}},'Bookmarks require HTTP(S) without credentials, query or fragment').transform(v=>new URL(v).href).refine(v=>v.length<=2048,'Canonical bookmark URL exceeds limit'),folder:z.string().trim().max(80).refine(v=>!/[\u0000-\u001f\u007f]/.test(v)).default('')}).strict();
export const BookmarkItemsSchema=z.array(BookmarkSchema).max(100).refine(items=>new Set(items.map(i=>i.id)).size===items.length,'Bookmark IDs must be unique').refine(items=>Buffer.byteLength(JSON.stringify(items))<=250000,'Bookmark collection exceeds byte budget');
export const PersonalBookmarksSchema=z.object({revision:z.number().int().nonnegative().safe().default(0),items:BookmarkItemsSchema.default([])}).strict().prefault({});
+1
View File
@@ -1,6 +1,7 @@
import {z} from 'zod';
export function credentialActionUrlAllowed(value){
// eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input.
try {const u=new URL(value);return u.protocol==='https:'&&!u.username&&!u.password&&!u.search&&!u.hash&&!value.includes('?')&&!value.includes('#')&&u.href===value&&!/[\u0000-\u0020\u007f]/.test(value);}catch{return false;}
}
const field=z.string().min(1).max(128).regex(/^[\x21-\x7e]+$/,'Credential field names require bounded printable ASCII');
+2
View File
@@ -4,6 +4,7 @@ const path=require('node:path');
const TYPES=['pe','elf','macho','pdf','zip','ole','png','jpeg','gif','webp','gzip','text','unknown'];
const EXT={exe:'pe',dll:'pe',sys:'pe',elf:'elf',pdf:'pdf',zip:'zip',docx:'zip',xlsx:'zip',pptx:'zip',docm:'zip',xlsm:'zip',xlsb:'zip',pptm:'zip',dotm:'zip',xlam:'zip',ppam:'zip',doc:'ole',xls:'ole',ppt:'ole',png:'png',jpg:'jpeg',jpeg:'jpeg',gif:'gif',webp:'webp',gz:'gzip',gzip:'gzip',txt:'text',csv:'text',json:'text',html:'text',js:'text',css:'text',xml:'text',md:'text'};
const MIME={'application/msword':'ole','application/vnd.ms-excel':'ole','application/vnd.ms-powerpoint':'ole','application/vnd.ms-word.document.macroenabled.12':'zip','application/vnd.ms-excel.sheet.macroenabled.12':'zip','application/vnd.ms-powerpoint.presentation.macroenabled.12':'zip','application/pdf':'pdf','application/zip':'zip','application/x-zip-compressed':'zip','application/vnd.openxmlformats-officedocument.wordprocessingml.document':'zip','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':'zip','application/vnd.openxmlformats-officedocument.presentationml.presentation':'zip','image/png':'png','image/jpeg':'jpeg','image/gif':'gif','image/webp':'webp','application/gzip':'gzip','application/x-gzip':'gzip','application/json':'text','application/xml':'text','application/javascript':'text','application/x-msdownload':'pe','application/vnd.microsoft.portable-executable':'pe'};
// eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input.
function safeFilename(value){const name=path.win32.basename(path.posix.basename(String(value||'download.bin'))).replace(/[<>:"/\\|?*\x00-\x1f\x7f]/g,'_').replace(/[. ]+$/g,'').slice(0,180);return !name||/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name)?'download.bin':name;}
function detectType(value){
const b=Buffer.from(value);const starts=(hex)=>b.length>=hex.length/2&&b.subarray(0,hex.length/2).equals(Buffer.from(hex,'hex'));
@@ -15,6 +16,7 @@ function detectType(value){
if(b.subarray(0,6).toString('ascii')==='GIF87a'||b.subarray(0,6).toString('ascii')==='GIF89a')return 'gif';
if(b.length>=12&&b.subarray(0,4).toString('ascii')==='RIFF'&&b.subarray(8,12).toString('ascii')==='WEBP')return 'webp';if(starts('1f8b'))return 'gzip';
// Decode the entire bounded payload. Prefix-only heuristics miss binary tails.
// eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input.
try{const text=new TextDecoder('utf-8',{fatal:true}).decode(b);if(!/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text))return 'text';}catch{}
return 'unknown';
}
+2
View File
@@ -2,8 +2,10 @@ import {z} from 'zod';
import {createHash} from 'node:crypto';
const id=z.string().min(1).max(80).regex(/^[a-zA-Z0-9_-]+$/);
// eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input.
const label=z.string().trim().max(80).refine(v=>!/[\u0000-\u001f\u007f]/.test(v));
export const LibraryPolicySchema=z.object({aiSuggestionsEnabled:z.boolean().default(false),aiAutoApplyEnabled:z.boolean().default(false)}).strict().prefault({});
// eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input.
export const LibraryTabsSchema=z.array(z.object({id,title:z.string().max(120).refine(v=>!/[\u0000-\u001f\u007f]/.test(v)),url:z.string().max(2048).url().refine(v=>['https:','http:'].includes(new URL(v).protocol))}).strict()).max(100).refine(v=>new Set(v.map(t=>t.id)).size===v.length);
export const LibraryPlanSchema=z.object({bookmarks:z.array(z.object({id,folder:label}).strict()).max(100),tabs:z.array(z.object({id,group:label.refine(v=>v.length<=40)}).strict()).max(100)}).strict().superRefine((v,c)=>{for(const key of ['bookmarks','tabs'])if(new Set(v[key].map(i=>i.id)).size!==v[key].length)c.addIssue({code:'custom',message:'Duplicate organization IDs'});});
export function librarySnapshotHash(tabs){return createHash('sha256').update(JSON.stringify(LibraryTabsSchema.parse(tabs))).digest('hex');}
+1
View File
@@ -13,6 +13,7 @@ export const GroupRestrictionsSchema=z.object({
lockedSections:unique(z.enum(SETTINGS_SECTIONS),SETTINGS_SECTIONS.length).default([]),
managedBookmarks:BookmarkItemsSchema.default([])
}).strict();
// eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input.
export const PolicyGroupSchema=z.object({id,name:z.string().min(1).max(100).refine(v=>!/[\u0000-\u001f\u007f]/.test(v)),
memberDeviceIds:unique(id,10000),restrictions:GroupRestrictionsSchema,
rollout:z.object({stage:z.enum(['pilot','expanded','all']),deviceIds:unique(id,10000).default([])}).strict()
+5 -3
View File
@@ -13,6 +13,7 @@ import {UserAccessSchema} from './user-profiles.mjs';
import {LibraryPolicySchema} from './library.mjs';
import {BrandSchema,StartPageSchema} from './branding.mjs';
import {EvidencePolicySchema} from './evidence.mjs';
import {inspectionTextVariants} from './text-variants.mjs';
const host = z.string().max(253).regex(/^(\*\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i);
export const PolicySchema = z.object({
@@ -29,6 +30,7 @@ export const PolicySchema = z.object({
browser: z.object({
homeUrl: z.string().url(), allowedHosts: z.array(host).max(200), blockedHosts: z.array(host).max(200),
allowDownloads: z.boolean(), allowDevTools: z.boolean(), allowWebSockets: z.boolean(),
allowAgentBackgroundWorkers: z.boolean().default(false),
settings: ManagedSettingsSchema, bookmarks: BookmarksSchema
}),
proxy: z.discriminatedUnion('mode', [
@@ -103,9 +105,9 @@ export function inspect(bytes, policy) {
try { text = new TextDecoder('utf-8',{fatal:true}).decode(data); }
catch { return {decision:'block',ruleId:'uninspectable'}; }
if(text.includes('\0')) return {decision:'block',ruleId:'uninspectable'};
const variants = [text];
try { variants.push(decodeURIComponent(text.replace(/\+/g,' '))); } catch {}
try { variants.push(JSON.stringify(JSON.parse(text))); } catch {}
let variants;
try{variants=inspectionTextVariants(text,policy.dlp.maxBytes);}
catch{return {decision:'block',ruleId:'normalization-limit'};}
for(const value of variants) {
let fragments;for(const r of policy.dlp.rules)if(r.kind==='fragment-sha256'){fragments??=fragmentHashes(value);if(fragments.has(r.value.toLowerCase()))return {decision:'block',ruleId:r.id};}
for(const phrase of policy.security?.content?.blockedPhrases||[])if(value.normalize('NFKC').toLowerCase().includes(phrase.normalize('NFKC').toLowerCase()))return {decision:'block',ruleId:'content-blocklist'};
+1 -1
View File
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { isIP } from 'node:net';
export const AgentScopes=['vault:prepare-submit','sessions:save','sessions:list','sessions:restore','vault:status','vault:list','vault:fill','history:list','history:go','window:state','window:control','sessions','navigate','back','forward','reload','read','inspect','scroll','click','type','select','check','screenshot','ai','application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group','library:organize-plan','library:organize-apply'];
export const AgentScopes=['network:background','vault:prepare-submit','sessions:save','sessions:list','sessions:restore','vault:status','vault:list','vault:fill','history:list','history:go','window:state','window:control','sessions','navigate','back','forward','reload','read','inspect','scroll','click','type','select','check','screenshot','ai','application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group','library:organize-plan','library:organize-apply'];
const host=z.string().max(253).regex(/^(\*\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i);
export const VisualOriginSchema=z.string().max(2048).refine(value=>{try{const u=new URL(value);return u.protocol==='https:'&&!u.username&&!u.password&&!u.search&&!u.hash&&u.origin===value&&!u.hostname.includes('*')&&!u.hostname.endsWith('.')&&!isIP(u.hostname.replace(/^\[|\]$/g,''))&&/^[a-z0-9.-]+$/.test(u.hostname);}catch{return false;}},'Expected a canonical exact HTTPS domain origin');
export const VisualViewportSchema=z.object({width:z.number().int().min(16).max(8192),height:z.number().int().min(16).max(8192)}).strict();
+38
View File
@@ -0,0 +1,38 @@
// Bounded decoding for common wire encodings. This is not arbitrary deobfuscation.
export function inspectionTextVariants(text,maxBytes){
const values=[],seen=new Set();let size=0,candidates=0;
const budget=Math.min(maxBytes*4,16*1024*1024);
function add(value,depth){
if(typeof value!=='string'||seen.has(value))return;
if(depth>3)throw Error('Inspection normalization limit');
const bytes=Buffer.byteLength(value);
if(values.length>=64||bytes>maxBytes||size+bytes>budget)throw Error('Inspection normalization limit');
if(value.includes('\0'))return;
seen.add(value);size+=bytes;values.push({value,depth});
}
add(text,0);
for(let i=0;i<values.length;i++){
const {value,depth}=values[i];
try{add(decodeURIComponent(value.replace(/\+/g,' ')),depth+1);}catch(error){if(error.message==='Inspection normalization limit')throw error;}
try{const parsed=JSON.parse(value);const queue=[parsed];let nodes=0;
while(queue.length){if(++nodes>4096)throw Error('Inspection normalization limit');const node=queue.pop();
if(typeof node==='string')add(node,depth+1);
else if(node&&typeof node==='object'){
const children=Object.values(node);
if(nodes+queue.length+children.length>4096)throw Error('Inspection normalization limit');
for(const child of children)queue.push(child);
}
}
}catch(error){if(error.message==='Inspection normalization limit')throw error;}
for(const match of value.matchAll(/[A-Za-z0-9+/_-]{12,}={0,2}/g)){
const encoded=match[0].replace(/-/g,'+').replace(/_/g,'/'),raw=Buffer.from(encoded,'base64');
if(raw.toString('base64').replace(/=+$/,'')!==encoded.replace(/=+$/,''))continue;
try{const decoded=new TextDecoder('utf-8',{fatal:true}).decode(raw);
if(decoded.includes('\0'))continue;
if(++candidates>256)throw Error('Inspection normalization limit');
add(decoded,depth+1);
}catch(error){if(error.message==='Inspection normalization limit')throw error;}
}
}
return values.map(row=>row.value);
}
+2 -1
View File
@@ -4,7 +4,7 @@ const unique=(schema,max)=>z.array(schema).max(max).refine(a=>new Set(a).size===
const host=z.string().max(253).regex(/^(\*\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i);
export const UserAccessSchema=z.object({delegatableFeatures:z.object({devTools:z.boolean().default(false)}).strict().prefault({})}).strict().prefault({});
export const DomainCategorySchema=z.object({id,name:z.string().min(1).max(100),hosts:unique(host,500)}).strict();
export const UserProfileSchema=z.object({id,name:z.string().min(1).max(100),subjects:unique(z.string().min(1).max(512),10000).default([]),deviceIds:unique(id,10000).default([]),allowedHosts:unique(host,500).default([]),blockedHosts:unique(host,500).default([]),blockedCategoryIds:unique(id,100).default([]),features:z.object({devTools:z.enum(['inherit','allow','deny']).default('inherit'),downloads:z.enum(['inherit','deny']).default('inherit'),ai:z.enum(['inherit','deny']).default('inherit'),agents:z.enum(['inherit','deny']).default('inherit')}).strict().prefault({}),requireDocumentScan:z.boolean().default(false)}).strict();
export const UserProfileSchema=z.object({id,name:z.string().min(1).max(100),subjects:unique(z.string().min(1).max(512),10000).default([]),deviceIds:unique(id,10000).default([]),allowedHosts:unique(host,500).default([]),blockedHosts:unique(host,500).default([]),blockedCategoryIds:unique(id,100).default([]),features:z.object({devTools:z.enum(['inherit','allow','deny']).default('inherit'),downloads:z.enum(['inherit','deny']).default('inherit'),ai:z.enum(['inherit','deny']).default('inherit'),agents:z.enum(['inherit','deny']).default('inherit'),agentBackgroundWorkers:z.enum(['inherit','deny']).default('inherit')}).strict().prefault({}),requireDocumentScan:z.boolean().default(false)}).strict();
export const UserProfileConfigurationSchema=z.object({profiles:z.array(UserProfileSchema).max(100).default([]),categories:z.array(DomainCategorySchema).max(100).default([])}).strict();
export function profileApplies(profile,device){return profile.deviceIds.includes(device.id)||!!device.verified_subject&&profile.subjects.includes(device.verified_subject);}
const covers=(a,b)=>a===b||a.startsWith('*.')&&b.endsWith(a.slice(1));
@@ -18,6 +18,7 @@ export function applyUserProfiles(policy,configuration,device){
if(p.features.downloads==='deny')result.browser.allowDownloads=false;
if(p.features.ai==='deny')result.ai.enabled=false;
if(p.features.agents==='deny')result.agents.enabled=false;
if(p.features.agentBackgroundWorkers==='deny')result.browser.allowAgentBackgroundWorkers=false;
if(p.requireDocumentScan)result.inspection.documentAnalysisEnabled=true;
}
if(profiles.some(p=>p.features.devTools==='deny'))result.browser.allowDevTools=false;