diff --git a/src/web-server/usage/codex-native-usage-collector.ts b/src/web-server/usage/codex-native-usage-collector.ts index 160fcfb7..a0ba3d8e 100644 --- a/src/web-server/usage/codex-native-usage-collector.ts +++ b/src/web-server/usage/codex-native-usage-collector.ts @@ -14,6 +14,8 @@ interface CodexNativeUsageCollectorOptions { } const CODEX_NATIVE_USAGE_CACHE_VERSION = 1; +const SECURE_CACHE_DIR_MODE = 0o700; +const SECURE_CACHE_FILE_MODE = 0o600; interface CachedRolloutFile { path: string; @@ -133,6 +135,14 @@ function isCodexNativeUsageCache(value: unknown): value is CodexNativeUsageCache return Object.values(value.files).every(isCachedRolloutFile); } +function chmodBestEffort(targetPath: string, mode: number): void { + try { + fs.chmodSync(targetPath, mode); + } catch { + // Cache reads and writes remain best-effort on filesystems that do not support chmod. + } +} + function readUsageCache( cacheDir: string, includeCliproxySessions: boolean @@ -153,11 +163,17 @@ function readUsageCache( function writeUsageCache(cacheDir: string, cache: CodexNativeUsageCache): void { try { - fs.mkdirSync(cacheDir, { recursive: true }); + fs.mkdirSync(cacheDir, { recursive: true, mode: SECURE_CACHE_DIR_MODE }); + chmodBestEffort(cacheDir, SECURE_CACHE_DIR_MODE); const cachePath = getCacheFilePath(cacheDir, cache.includeCliproxySessions); const tempPath = `${cachePath}.${process.pid}.tmp`; - fs.writeFileSync(tempPath, JSON.stringify(cache), 'utf8'); + fs.writeFileSync(tempPath, JSON.stringify(cache), { + encoding: 'utf8', + mode: SECURE_CACHE_FILE_MODE, + }); + chmodBestEffort(tempPath, SECURE_CACHE_FILE_MODE); fs.renameSync(tempPath, cachePath); + chmodBestEffort(cachePath, SECURE_CACHE_FILE_MODE); } catch { // Best-effort only. } diff --git a/tests/unit/web-server/codex-native-usage-collector.test.ts b/tests/unit/web-server/codex-native-usage-collector.test.ts index 50d68991..0111f560 100644 --- a/tests/unit/web-server/codex-native-usage-collector.test.ts +++ b/tests/unit/web-server/codex-native-usage-collector.test.ts @@ -311,6 +311,28 @@ describe('codex native usage collector', () => { expect(entries).toHaveLength(2); }); + it('writes native usage caches with owner-only permissions', async () => { + if (process.platform === 'win32') return; + + writeCodexRollout(tempRoot); + const cacheDir = getCacheDir(); + const previousUmask = process.umask(0o022); + + try { + await scanCodexNativeUsageEntries({ + env: { CODEX_HOME: tempRoot }, + homeDir: tempRoot, + cacheDir, + }); + } finally { + process.umask(previousUmask); + } + + const cachePath = path.join(cacheDir, 'codex-native-usage-v1.json'); + expect(fs.statSync(cacheDir).mode & 0o777).toBe(0o700); + expect(fs.statSync(cachePath).mode & 0o777).toBe(0o600); + }); + it('keeps default and include-cliproxy cache entries separate', async () => { writeCodexRollout(tempRoot, { modelProvider: 'cliproxy' }); const cacheDir = getCacheDir();