fix(api): avoid logging bearer tokens in request diagnostics (#1987)

This commit is contained in:
Ruifeng Xue
2026-05-17 01:38:46 -07:00
committed by GitHub
parent c23b14fe4f
commit 5f8241254b
2 changed files with 70 additions and 2 deletions
@@ -4,10 +4,20 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('@tauri-apps/api/app', () => ({ getVersion: vi.fn() }));
const configMock = vi.hoisted(() => ({ isDev: true }));
vi.mock('../../utils/config', () => ({
APP_VERSION: '0.0.0-test',
get IS_DEV() {
return configMock.isDev;
},
}));
describe('apiClient version headers', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
configMock.isDev = true;
vi.mocked(isTauri).mockReturnValue(false);
vi.stubGlobal('fetch', vi.fn());
});
@@ -49,6 +59,54 @@ describe('apiClient version headers', () => {
expect(headers).not.toHaveProperty('x-web-version');
});
it('logs request diagnostics in dev without leaking authorization headers', async () => {
const fetchMock = vi.mocked(fetch);
fetchMock.mockResolvedValueOnce({
ok: true,
headers: new Headers({ 'content-type': 'application/json' }),
json: async () => ({ success: true }),
} as Response);
const { apiClient, setStoreForApiClient } = await import('../apiClient');
setStoreForApiClient(() => 'secret-session-token');
await apiClient.post('/version-check', { ok: true });
const requestInit = fetchMock.mock.calls[0][1] as RequestInit;
const requestHeaders = requestInit.headers as Record<string, string>;
expect(requestHeaders.Authorization).toBe('Bearer secret-session-token');
const logMock = vi.mocked(console.log);
expect(logMock).toHaveBeenCalledWith(
'request',
expect.objectContaining({
method: 'POST',
headers: expect.not.objectContaining({
Authorization: expect.any(String),
authorization: expect.any(String),
}),
})
);
});
it('does not log request diagnostics in production', async () => {
configMock.isDev = false;
const fetchMock = vi.mocked(fetch);
fetchMock.mockResolvedValueOnce({
ok: true,
headers: new Headers({ 'content-type': 'application/json' }),
json: async () => ({ success: true }),
} as Response);
const { apiClient, setStoreForApiClient } = await import('../apiClient');
setStoreForApiClient(() => 'secret-session-token');
await apiClient.get('/version-check');
expect(console.log).not.toHaveBeenCalledWith('request', expect.anything());
});
it('retries tauri version lookup after a transient failure', async () => {
vi.mocked(isTauri).mockReturnValue(true);
vi.mocked(getVersion)
+12 -2
View File
@@ -1,4 +1,5 @@
import type { ApiError } from '../types/api';
import { IS_DEV } from '../utils/config';
import { getBackendUrl } from './backendUrl';
import { getClientVersionHeaders } from './clientVersionHeaders';
@@ -37,7 +38,7 @@ class ApiClient {
/**
* Build headers for the request
*/
private async buildHeaders(options: RequestOptions): Promise<HeadersInit> {
private async buildHeaders(options: RequestOptions): Promise<Record<string, string>> {
const versionHeaders = await getClientVersionHeaders();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
@@ -56,6 +57,13 @@ class ApiClient {
return headers;
}
private sanitizeRequestLogHeaders(headers: HeadersInit): Record<string, string> {
const safeHeaders = { ...(headers as Record<string, string>) };
delete safeHeaders.Authorization;
delete safeHeaders.authorization;
return safeHeaders;
}
/**
* Make an API request
*/
@@ -66,7 +74,9 @@ class ApiClient {
const url = `${baseUrl}${endpoint}`;
const headers = await this.buildHeaders({ ...options, requireAuth });
console.log('request', { url, headers, body, method });
if (IS_DEV) {
console.log('request', { url, headers: this.sanitizeRequestLogHeaders(headers), method });
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);