From 6bba193cdd742a56f3d495a3de5d478df636f444 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 30 May 2026 14:53:55 -0400 Subject: [PATCH] fix: harden cliproxy usage cache --- src/web-server/usage/cliproxy-usage-syncer.ts | 29 +++++++++++++------ .../usage/cliproxy-usage-transformer.ts | 22 +++++++++----- .../web-server/cliproxy-usage-syncer.test.ts | 15 ++++++++++ .../cliproxy-usage-transformer.test.ts | 15 ++++++++++ 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/web-server/usage/cliproxy-usage-syncer.ts b/src/web-server/usage/cliproxy-usage-syncer.ts index 83d15a69..887f2a7a 100644 --- a/src/web-server/usage/cliproxy-usage-syncer.ts +++ b/src/web-server/usage/cliproxy-usage-syncer.ts @@ -50,6 +50,8 @@ const HISTORY_RETENTION_DAYS = Math.max( ); const HISTORY_RETENTION_MS = HISTORY_RETENTION_DAYS * 24 * 60 * 60 * 1000; const MAX_WRITE_ATTEMPTS = 3; +const PRIVATE_DIR_MODE = 0o700; +const PRIVATE_FILE_MODE = 0o600; /** Sync interval in ms, configurable via CCS_CLIPROXY_SYNC_INTERVAL env var (default: 5 min) */ const SYNC_INTERVAL_MS = Math.max( @@ -68,11 +70,19 @@ function getLatestSnapshotPath(): string { return path.join(getCliproxyCacheDir(), 'latest.json'); } +function ensurePrivateDirectory(dir: string): void { + fs.mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR_MODE }); + fs.chmodSync(dir, PRIVATE_DIR_MODE); +} + function ensureCliproxyCacheDir(): void { - const dir = getCliproxyCacheDir(); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } + const ccsDir = getCcsDir(); + const cacheDir = path.join(ccsDir, 'cache'); + const cliproxyCacheDir = path.join(cacheDir, 'cliproxy-usage'); + + ensurePrivateDirectory(ccsDir); + ensurePrivateDirectory(cacheDir); + ensurePrivateDirectory(cliproxyCacheDir); } function getSnapshotTimestamp(): number { @@ -112,8 +122,6 @@ function buildLegacyHistoryDetails( details.push({ model: breakdown.modelName, timestamp: buildHourlyTimestamp(hour.hour), - source: hour.source, - authIndex: 'legacy-hourly', inputTokens: breakdown.inputTokens, outputTokens: breakdown.outputTokens, cacheReadTokens: breakdown.cacheReadTokens, @@ -135,8 +143,6 @@ function buildLegacyHistoryDetails( details.push({ model: breakdown.modelName, timestamp: buildDailyTimestamp(day.date), - source: day.source, - authIndex: 'legacy-daily', inputTokens: breakdown.inputTokens, outputTokens: breakdown.outputTokens, cacheReadTokens: breakdown.cacheReadTokens, @@ -255,7 +261,11 @@ async function writeSnapshotWithMerge( const snapshot = buildSnapshot(baseSnapshot?.details ?? [], incomingDetails); const tempFile = `${snapshotPath}.${process.pid}.${snapshot.timestamp}.tmp`; - fs.writeFileSync(tempFile, JSON.stringify(snapshot), 'utf-8'); + fs.writeFileSync(tempFile, JSON.stringify(snapshot), { + encoding: 'utf-8', + mode: PRIVATE_FILE_MODE, + }); + fs.chmodSync(tempFile, PRIVATE_FILE_MODE); const latestSnapshot = readSnapshot(false); const latestTimestamp = latestSnapshot?.timestamp ?? -Infinity; @@ -265,6 +275,7 @@ async function writeSnapshotWithMerge( } fs.renameSync(tempFile, snapshotPath); + fs.chmodSync(snapshotPath, PRIVATE_FILE_MODE); console.log(ok('CLIProxy usage snapshot updated')); return; } diff --git a/src/web-server/usage/cliproxy-usage-transformer.ts b/src/web-server/usage/cliproxy-usage-transformer.ts index 5f96d632..68675274 100644 --- a/src/web-server/usage/cliproxy-usage-transformer.ts +++ b/src/web-server/usage/cliproxy-usage-transformer.ts @@ -22,8 +22,6 @@ export interface CliproxyUsageHistoryDetail { model: string; provider?: string; timestamp: string; - source: string; - authIndex: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; @@ -68,8 +66,6 @@ function createHistoryDetail( model, provider: pricingProvider, timestamp: detail.timestamp, - source: detail.source, - authIndex: String(detail.auth_index), inputTokens: detail.tokens?.input_tokens ?? 0, outputTokens: detail.tokens?.output_tokens ?? 0, cacheReadTokens: detail.tokens?.cached_tokens ?? 0, @@ -128,13 +124,25 @@ export function extractCliproxyUsageHistoryDetails( return results; } +function sanitizeHistoryDetail(detail: CliproxyUsageHistoryDetail): CliproxyUsageHistoryDetail { + return { + model: detail.model, + ...(detail.provider && { provider: detail.provider }), + timestamp: detail.timestamp, + inputTokens: detail.inputTokens, + outputTokens: detail.outputTokens, + cacheReadTokens: detail.cacheReadTokens, + requestCount: detail.requestCount, + cost: detail.cost, + failed: detail.failed, + }; +} + function createHistorySignature(detail: CliproxyUsageHistoryDetail): string { return [ detail.model, detail.provider ?? '', detail.timestamp, - detail.source, - detail.authIndex, detail.inputTokens, detail.outputTokens, detail.cacheReadTokens, @@ -182,7 +190,7 @@ export function mergeCliproxyUsageHistoryDetails( const merged: CliproxyUsageHistoryDetail[] = []; for (const { detail, count } of existingCounts.values()) { for (let index = 0; index < count; index++) { - merged.push({ ...detail }); + merged.push(sanitizeHistoryDetail(detail)); } } diff --git a/tests/unit/web-server/cliproxy-usage-syncer.test.ts b/tests/unit/web-server/cliproxy-usage-syncer.test.ts index db9dffa1..2daf3666 100644 --- a/tests/unit/web-server/cliproxy-usage-syncer.test.ts +++ b/tests/unit/web-server/cliproxy-usage-syncer.test.ts @@ -109,6 +109,21 @@ describe('cliproxy usage syncer', () => { const snapshotPath = path.join(ccsDir, 'cache', 'cliproxy-usage', 'latest.json'); expect(fs.existsSync(snapshotPath)).toBe(true); + const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8')) as { + details: Array>; + }; + expect(snapshot.details[0]).not.toHaveProperty('source'); + expect(snapshot.details[0]).not.toHaveProperty('authIndex'); + + if (process.platform !== 'win32') { + const cacheDir = path.join(ccsDir, 'cache'); + const cliproxyCacheDir = path.dirname(snapshotPath); + expect(fs.statSync(ccsDir).mode & 0o777).toBe(0o700); + expect(fs.statSync(cacheDir).mode & 0o777).toBe(0o700); + expect(fs.statSync(cliproxyCacheDir).mode & 0o777).toBe(0o700); + expect(fs.statSync(snapshotPath).mode & 0o777).toBe(0o600); + } + const cached = await runWithScopedConfigDir(ccsDir, async () => { return await loadCachedCliproxyData(); }); diff --git a/tests/unit/web-server/cliproxy-usage-transformer.test.ts b/tests/unit/web-server/cliproxy-usage-transformer.test.ts index 4e590aa6..83c78f98 100644 --- a/tests/unit/web-server/cliproxy-usage-transformer.test.ts +++ b/tests/unit/web-server/cliproxy-usage-transformer.test.ts @@ -109,6 +109,8 @@ describe('cliproxy usage transformer', () => { const flat = extractCliproxyUsageHistoryDetails(sampleResponse); expect(flat).toHaveLength(4); expect(flat[0].provider).toBe('google'); + expect(flat[0]).not.toHaveProperty('source'); + expect(flat[0]).not.toHaveProperty('authIndex'); expect( flat.some( (entry) => @@ -134,6 +136,19 @@ describe('cliproxy usage transformer', () => { expect(merged).toHaveLength(details.length); }); + it('strips legacy account identifiers when merging persisted history', () => { + const details = extractCliproxyUsageHistoryDetails(sampleResponse); + const legacyDetail = { + ...details[0], + source: 'user@example.com', + authIndex: 'auth-file-7', + }; + const merged = mergeCliproxyUsageHistoryDetails([legacyDetail], []); + + expect(merged[0]).not.toHaveProperty('source'); + expect(merged[0]).not.toHaveProperty('authIndex'); + }); + it('preserves legitimate duplicate requests when the incoming batch has more occurrences', () => { const details = extractCliproxyUsageHistoryDetails(sampleResponse); const repeated = [details[0], { ...details[0] }];