Skip to content

Commit 5f547ff

Browse files
ersinkocclaude
andcommitted
fix(security): cap persisted error-stack length (EXPOSE-001)
Full JS error stacks were stored unbounded in request_logs.error_stack, which is included in per-user DB exports — leaking internal file paths / code structure (and bloating the table). Cap stored stacks at 2000 chars (top frames retained for debugging). Fuller mitigation (path redaction / prod-gating) is a product decision, noted in REMEDIATION.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5d478c9 commit 5f547ff

2 files changed

Lines changed: 21 additions & 2 deletions

File tree

packages/gateway/src/db/repositories/logs.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,18 @@ describe('LogsRepository', () => {
178178
expect(params[18]).toBeNull(); // userAgent
179179
});
180180

181+
it('truncates an oversized error stack to the cap (EXPOSE-001)', async () => {
182+
mockAdapter.execute.mockResolvedValueOnce({ changes: 1 });
183+
mockAdapter.queryOne.mockResolvedValueOnce(makeLogRow());
184+
185+
const hugeStack = 'Error: boom\n' + ' at frame\n'.repeat(5000);
186+
await repo.log({ type: 'chat', errorStack: hugeStack });
187+
188+
const params = mockAdapter.execute.mock.calls[0]![1] as unknown[];
189+
expect((params[16] as string).length).toBe(2000); // persisted stack capped
190+
expect(params[16]).toBe(hugeStack.slice(0, 2000));
191+
});
192+
181193
it('should return a fallback log entry when insert fails', async () => {
182194
mockAdapter.execute.mockRejectedValueOnce(new Error('DB connection lost'));
183195

packages/gateway/src/db/repositories/logs.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import { getLog } from '../../services/log.js';
99

1010
const log = getLog('LogsRepo');
1111

12+
// EXPOSE-001: bound how much of a JS error stack we persist. Full stacks leak
13+
// internal file paths / code structure into request_logs (which is included in
14+
// per-user DB exports) and bloat the table. Keep the top frames (enough to
15+
// debug) and drop the rest.
16+
const MAX_ERROR_STACK_LEN = 2000;
17+
1218
// =====================================================
1319
// TYPES
1420
// =====================================================
@@ -152,6 +158,7 @@ export class LogsRepository extends BaseRepository {
152158
async log(input: CreateLogInput): Promise<RequestLog> {
153159
const id = crypto.randomUUID();
154160
const now = new Date().toISOString();
161+
const errorStack = input.errorStack ? input.errorStack.slice(0, MAX_ERROR_STACK_LEN) : null;
155162

156163
try {
157164
await this.execute(
@@ -177,7 +184,7 @@ export class LogsRepository extends BaseRepository {
177184
input.totalTokens || null,
178185
input.durationMs || null,
179186
input.error || null,
180-
input.errorStack || null,
187+
errorStack,
181188
input.ipAddress || null,
182189
input.userAgent || null,
183190
now,
@@ -207,7 +214,7 @@ export class LogsRepository extends BaseRepository {
207214
totalTokens: input.totalTokens || null,
208215
durationMs: input.durationMs || null,
209216
error: input.error || null,
210-
errorStack: input.errorStack || null,
217+
errorStack,
211218
ipAddress: input.ipAddress || null,
212219
userAgent: input.userAgent || null,
213220
createdAt: new Date(now),

0 commit comments

Comments
 (0)