const { chromium } = require('playwright'); const { readFileSync } = require('node:fs'); const { execFileSync } = require('node:child_process'); const assert = require('node:assert/strict'); const source = readFileSync('static/js/document.js', 'utf8'); function documentFunction(name, text = source) { const match = text.match(new RegExp('^ (?:async )?function ' + name + '\\(.*?^ }', 'ms')); assert(match, name); return match[0]; } (async () => { const browser = await chromium.launch({ headless: true }); try { // Opt-in positive controls use an explicit vulnerable revision, so these // regressions also work after the fix is committed or in a shallow checkout. const baselineRevision = process.env.ODYSSEUS_SECURITY_BASELINE_REVISION; if (baselineRevision) { const original = execFileSync('git', ['show', baselineRevision + ':static/js/document.js'], { encoding: 'utf8' }); const baseline = await browser.newPage(); await baseline.route('**/api/**', route => route.fulfill({ json: { fonts: {}, value: null } })); await baseline.route('**/static/js/chatRenderer-head-harness.js', route => route.fulfill({ contentType: 'application/javascript', body: execFileSync('git', ['show', baselineRevision + ':static/js/chatRenderer.js'], { encoding: 'utf8' }), })); await baseline.goto(process.env.ODYSSEUS_TEST_STATIC_ORIGIN + '/static/js/documentStats.js'); const originalFunctions = Object.fromEntries([ '_unfoldEmailHeaderLines', '_parseEmailHeader', '_looksLikeWrappedEmailContent', '_decodeBase64EmailWrapper', '_sanitizeOutgoingEmailBody', '_emailHtmlToPlainText', '_aiReply', '_loadOdysseusAttachItems', '_odysseusAttachLabel', '_escHtml', ].map(name => [name, documentFunction(name, original)])); const vulnerable = await baseline.evaluate(async functions => { const { recordSessionMetricsCost } = await import('/static/js/chatRenderer-head-harness.js'); recordSessionMetricsCost({ model: 'gpt-4o', input_tokens: 100, output_tokens: 10, endpoint_cost_tracked: true, _costRecordId: 'pollutedByLedger' }, '__proto__'); await navigator.locks.request('odysseus-session-cost-ledger', () => {}); const polluted = Object.hasOwn(Object.prototype, 'pollutedByLedger'); delete Object.prototype.pollutedByLedger; localStorage.clear(); // Isolate the second annotation's overflow assignment from the real // inherited-session lookup defect by seeding an OWN __proto__ run map. // The addition assigned at HEAD:1424 is primitive, so __proto__'s setter // ignores it rather than changing either this map or Object.prototype. const prototypeBefore = Object.getOwnPropertyDescriptors(Object.prototype); localStorage.setItem('ody-session-cost-runs', JSON.stringify({ ['__proto__']: Object.fromEntries(Array.from({ length: 256 }, (_, i) => ['seed-' + i, 0.001])) })); recordSessionMetricsCost({ model: 'gpt-4o', input_tokens: 100, output_tokens: 10, endpoint_cost_tracked: true, _costRecordId: 'overflow-proof' }, '__proto__'); await navigator.locks.request('odysseus-session-cost-ledger', () => {}); const prototypeAfter = Object.getOwnPropertyDescriptors(Object.prototype); const overflowUnchanged = Reflect.ownKeys(prototypeBefore).length === Reflect.ownKeys(prototypeAfter).length && Reflect.ownKeys(prototypeBefore).every(key => Reflect.ownKeys(prototypeBefore[key]) .every(field => prototypeBefore[key][field] === prototypeAfter[key]?.[field])); const overflowRuns = JSON.parse(localStorage.getItem('ody-session-cost-runs'))['__proto__']; const overflowCostStore = localStorage.getItem('ody-session-cost'); localStorage.clear(); const _unfoldEmailHeaderLines = eval('(' + functions._unfoldEmailHeaderLines + ')'); const _parseEmailHeader = eval('(' + functions._parseEmailHeader + ')'); const _looksLikeWrappedEmailContent = eval('(' + functions._looksLikeWrappedEmailContent + ')'); const _decodeBase64EmailWrapper = eval('(' + functions._decodeBase64EmailWrapper + ')'); const _sanitizeOutgoingEmailBody = eval('(' + functions._sanitizeOutgoingEmailBody + ')'); const _emailHtmlToPlainText = eval('(' + functions._emailHtmlToPlainText + ')'); const activeDocId = 'fixture'; const docs = new Map(); const uiModule = { showToast() {} }; const _splitEmailReplyQuote = text => ({ body: text, quote: '' }); const generate = eval('(' + functions._aiReply + ')'); document.body.innerHTML = ''; const payload = '
Existing draft
Existing draft
' + payload; await generate(); } // Media-only drafts must not be mistaken for an empty reply. This checks // the query boundary moved from the element to template.content too. for (const body of ['Hello world & friends
'); const literal = _emailHtmlToPlainText('<img src=x onerror="window.executed++">'); const outgoing = _sanitizeOutgoingEmailBody('Hello\n\nworld'); const wrapped = _sanitizeOutgoingEmailBody(btoa('To: person@example.com\nSubject: Test\n---\nValid reply')); await new Promise(resolve => setTimeout(resolve, 150)); return { normal, literal, outgoing, wrapped, toasts, executed: window.executed }; }, functions); assert.equal(email.normal, 'Hello world & friends'); assert.equal(email.literal, '