mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-08 16:02:20 +02:00
fix(security): isolate session cost ledger keys
Use Map-backed cost ledgers so externally derived session and run identifiers never cross ordinary object prototype semantics. Preserve the existing JSON storage format and extend browser and isolated ledger regressions for replay, overflow, legacy data, and reserved keys.
This commit is contained in:
@@ -114,7 +114,7 @@ function documentFunction(name, text = source) {
|
||||
await page.setContent('<!doctype html><textarea id="doc-editor-textarea"></textarea>');
|
||||
|
||||
const ledger = await page.evaluate(async () => {
|
||||
const { recordSessionMetricsCost, getSessionCost } = await import('/static/js/chatRenderer.js');
|
||||
const { recordSessionMetricsCost, getSessionCost, resetSessionCost } = await import('/static/js/chatRenderer.js');
|
||||
const metrics = id => ({ model: 'gpt-4o', input_tokens: 100, output_tokens: 10,
|
||||
endpoint_cost_tracked: true, _costRecordId: id });
|
||||
const before = Object.getOwnPropertyDescriptors(Object.prototype);
|
||||
@@ -149,11 +149,37 @@ function documentFunction(name, text = source) {
|
||||
await navigator.locks.request('odysseus-session-cost-ledger', () => {});
|
||||
for (let i = 0; i < 257; i++) recordSessionMetricsCost(metrics('run-' + i), 'overflow-session');
|
||||
await navigator.locks.request('odysseus-session-cost-ledger', () => {});
|
||||
// Snapshot all ordinary-ledger results before the reserved-key fixture
|
||||
// deliberately replaces localStorage below.
|
||||
const replayCost = getSessionCost('safe-session');
|
||||
const legacyCost = getSessionCost('legacy-session');
|
||||
const dottedCost = getSessionCost('safe.__proto__.session');
|
||||
const overflowCost = getSessionCost('overflow-session');
|
||||
const runWire = JSON.parse(localStorage.getItem('ody-session-cost-runs') || '{}');
|
||||
const costWire = JSON.parse(localStorage.getItem('ody-session-cost') || '{}');
|
||||
const retainedRuns = Object.keys(runWire['overflow-session']).length;
|
||||
const wireShape = !Array.isArray(runWire)
|
||||
&& !Array.isArray(costWire)
|
||||
&& !!runWire['overflow-session']
|
||||
&& !Array.isArray(runWire['overflow-session']);
|
||||
|
||||
// A persisted legacy/reserved key must remain data rather than becoming
|
||||
// prototype state. Reading and removing it must also be side-effect free.
|
||||
localStorage.setItem('ody-session-cost', '{"__proto__":1}');
|
||||
localStorage.setItem('ody-session-cost-runs', '{"__proto__":{"legacy-run":2}}');
|
||||
const legacyReservedCost = getSessionCost('__proto__');
|
||||
resetSessionCost('__proto__');
|
||||
const clearedReserved = !Object.hasOwn(
|
||||
JSON.parse(localStorage.getItem('ody-session-cost') || '{}'),
|
||||
'__proto__',
|
||||
) && !Object.hasOwn(
|
||||
JSON.parse(localStorage.getItem('ody-session-cost-runs') || '{}'),
|
||||
'__proto__',
|
||||
);
|
||||
|
||||
const after = Object.getOwnPropertyDescriptors(Object.prototype);
|
||||
return { cost, replayCost: getSessionCost('safe-session'), legacyCost: getSessionCost('legacy-session'),
|
||||
dottedCost: getSessionCost('safe.__proto__.session'),
|
||||
overflowCost: getSessionCost('overflow-session'),
|
||||
retainedRuns: Object.keys(JSON.parse(localStorage.getItem('ody-session-cost-runs'))['overflow-session']).length,
|
||||
return { cost, replayCost, legacyCost, dottedCost,
|
||||
overflowCost, retainedRuns, wireShape, legacyReservedCost, clearedReserved,
|
||||
unchanged: Reflect.ownKeys(before).length === Reflect.ownKeys(after).length
|
||||
&& Reflect.ownKeys(before).every(key => Reflect.ownKeys(before[key]).every(field => before[key][field] === after[key]?.[field])) };
|
||||
});
|
||||
@@ -164,6 +190,9 @@ function documentFunction(name, text = source) {
|
||||
assert.equal(ledger.dottedCost, ledger.cost);
|
||||
assert(Math.abs(ledger.overflowCost - 257 * ledger.cost) < 1e-10);
|
||||
assert.equal(ledger.retainedRuns, 256);
|
||||
assert.equal(ledger.wireShape, true);
|
||||
assert.equal(ledger.legacyReservedCost, 3);
|
||||
assert.equal(ledger.clearedReserved, true);
|
||||
|
||||
const names = ['_unfoldEmailHeaderLines', '_parseEmailHeader', '_looksLikeWrappedEmailContent', '_decodeBase64EmailWrapper',
|
||||
'_sanitizeOutgoingEmailBody', '_emailHtmlToPlainText', '_aiReply',
|
||||
|
||||
@@ -122,6 +122,8 @@ def test_repeated_live_metrics_render_records_session_cost_once():
|
||||
"function getModelCost(_model, inputTokens, outputTokens) { return (inputTokens + outputTokens) / 1000; }",
|
||||
_function_source("_billableCost"),
|
||||
_function_source("_metricsBillableCost"),
|
||||
_function_source("_readCostLedger"),
|
||||
_function_source("_writeCostLedger"),
|
||||
_function_source("recordSessionMetricsCost"),
|
||||
"const metrics = {model: 'paid-model', input_tokens: 100, output_tokens: 10, endpoint_cost_tracked: true};",
|
||||
"recordSessionMetricsCost(metrics);",
|
||||
@@ -150,6 +152,10 @@ def test_replayed_metrics_use_run_identity_for_durable_cost_deduplication():
|
||||
"function getModelCost(_model, inputTokens, outputTokens) { return (inputTokens + outputTokens) / 1000; }",
|
||||
_function_source("_billableCost"),
|
||||
_function_source("_metricsBillableCost"),
|
||||
_function_source("_readCostLedger"),
|
||||
_function_source("_writeCostLedger"),
|
||||
_function_source("_readCostRunLedger"),
|
||||
_function_source("_writeCostRunLedger"),
|
||||
_function_source("recordSessionMetricsCost"),
|
||||
_function_source("getSessionCost"),
|
||||
"const firstObject = {model: 'paid-model', input_tokens: 100, output_tokens: 10, endpoint_cost_tracked: true, _costRecordId: 'run-1'};",
|
||||
@@ -180,6 +186,10 @@ def test_run_cost_ledger_sums_segments_and_updates_repeated_segment_metrics():
|
||||
"function getModelCost(_model, inputTokens, outputTokens) { return (inputTokens + outputTokens) / 1000; }",
|
||||
_function_source("_billableCost"),
|
||||
_function_source("_metricsBillableCost"),
|
||||
_function_source("_readCostLedger"),
|
||||
_function_source("_writeCostLedger"),
|
||||
_function_source("_readCostRunLedger"),
|
||||
_function_source("_writeCostRunLedger"),
|
||||
_function_source("recordSessionMetricsCost"),
|
||||
_function_source("getSessionCost"),
|
||||
"recordSessionMetricsCost({model: 'student', input_tokens: 100, output_tokens: 10, _costRecordId: 'run:primary'});",
|
||||
@@ -207,6 +217,10 @@ def test_local_selected_endpoint_does_not_erase_paid_fallback_ledger():
|
||||
"const badge = {style: {}, textContent: ''};",
|
||||
"const document = {getElementById() { return badge; }};",
|
||||
"const window = {sessionModule: {getCurrentSessionId() { return 'session'; }, getCurrentEndpointUrl() { return 'local'; }}};",
|
||||
_function_source("_readCostLedger"),
|
||||
_function_source("_writeCostLedger"),
|
||||
_function_source("_readCostRunLedger"),
|
||||
_function_source("_writeCostRunLedger"),
|
||||
_function_source("getSessionCost"),
|
||||
_function_source("updateSessionCostUI"),
|
||||
"updateSessionCostUI();",
|
||||
|
||||
Reference in New Issue
Block a user