Skip to content

Commit e322d2a

Browse files
committed
Redact credentials from Git URLs in logs
Credentials embedded in repository URLs (tokens, usernames, passwords) are now replaced with ****** before logging. This prevents sensitive data from appearing in development logs when cloning private repos. Fixes #177
1 parent 17d2a4d commit e322d2a

File tree

2 files changed

+78
-1
lines changed

2 files changed

+78
-1
lines changed

packages/sandbox/src/clients/git-client.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,10 @@ export class GitClient extends BaseHttpClient {
5858
data
5959
);
6060

61+
const redactedUrl = this.redactCredentials(repoUrl);
6162
this.logSuccess(
6263
'Repository cloned',
63-
`${repoUrl} (branch: ${response.branch}) -> ${response.targetDir}`
64+
`${redactedUrl} (branch: ${response.branch}) -> ${response.targetDir}`
6465
);
6566

6667
return response;
@@ -88,4 +89,12 @@ export class GitClient extends BaseHttpClient {
8889
return repoName.replace(/\.git$/, '') || 'repo';
8990
}
9091
}
92+
93+
/**
94+
* Redact credentials from repository URLs for secure logging
95+
*/
96+
private redactCredentials(repoUrl: string): string {
97+
// Replace any credentials (username:password or token) between :// and @ with ******
98+
return repoUrl.replace(/\/\/[^@]+@/, '//******@');
99+
}
91100
}

packages/sandbox/tests/git-client.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,4 +412,72 @@ describe('GitClient', () => {
412412
expect(fullOptionsClient).toBeInstanceOf(GitClient);
413413
});
414414
});
415+
416+
describe('credential redaction in logs', () => {
417+
it('should redact credentials from URLs but leave public URLs unchanged', async () => {
418+
const mockLogger = {
419+
info: vi.fn(),
420+
warn: vi.fn(),
421+
error: vi.fn(),
422+
debug: vi.fn()
423+
};
424+
425+
const clientWithLogger = new GitClient({
426+
baseUrl: 'http://test.com',
427+
port: 3000,
428+
logger: mockLogger
429+
});
430+
431+
// Test with credentials
432+
mockFetch.mockResolvedValueOnce(
433+
new Response(
434+
JSON.stringify({
435+
success: true,
436+
stdout: "Cloning into 'private-repo'...\nDone.",
437+
stderr: '',
438+
exitCode: 0,
439+
repoUrl: 'https://oauth2:[email protected]/user/private-repo.git',
440+
branch: 'main',
441+
targetDir: '/workspace/private-repo',
442+
timestamp: '2023-01-01T00:00:00Z'
443+
}),
444+
{ status: 200 }
445+
)
446+
);
447+
448+
await clientWithLogger.checkout(
449+
'https://oauth2:[email protected]/user/private-repo.git',
450+
'test-session'
451+
);
452+
453+
let logDetails = mockLogger.info.mock.calls[0]?.[1]?.details;
454+
expect(logDetails).not.toContain('ghp_token123');
455+
expect(logDetails).toContain('https://******@github.com/user/private-repo.git');
456+
457+
// Test without credentials
458+
mockFetch.mockResolvedValueOnce(
459+
new Response(
460+
JSON.stringify({
461+
success: true,
462+
stdout: "Cloning into 'react'...\nDone.",
463+
stderr: '',
464+
exitCode: 0,
465+
repoUrl: 'https://github.com/facebook/react.git',
466+
branch: 'main',
467+
targetDir: '/workspace/react',
468+
timestamp: '2023-01-01T00:00:00Z'
469+
}),
470+
{ status: 200 }
471+
)
472+
);
473+
474+
await clientWithLogger.checkout(
475+
'https://github.com/facebook/react.git',
476+
'test-session'
477+
);
478+
479+
logDetails = mockLogger.info.mock.calls[1]?.[1]?.details;
480+
expect(logDetails).toContain('https://github.com/facebook/react.git');
481+
});
482+
});
415483
});

0 commit comments

Comments
 (0)