Skip to content

Commit f5b26df

Browse files
🔒 Fix Path Traversal via Unsanitized Filename in Save Dialog (#284)
Uses `path.basename` to sanitize filenames passed to `saveFile`, preventing directory traversal sequences from saving files outside the intended location. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 3336bd3 commit f5b26df

2 files changed

Lines changed: 25 additions & 1 deletion

File tree

src/hostBridge.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -906,7 +906,8 @@ export class HostBridge implements ToastService {
906906
async saveFile(filename: string, data: Uint8ArrayLike): Promise<void> {
907907
// Use the database file's directory as the default location
908908
const dbDir = path.dirname(this.document.uri.fsPath);
909-
const defaultPath = path.join(dbDir, filename);
909+
const safeFilename = path.basename(filename);
910+
const defaultPath = path.join(dbDir, safeFilename);
910911

911912
const uri = await vsc.window.showSaveDialog({
912913
defaultUri: vsc.Uri.file(defaultPath),

tests/unit/hostBridge.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,29 @@ describe('HostBridge', () => {
1111
mock.reset();
1212
});
1313

14+
it('saveFile should prevent path traversal by stripping directory components from filename', async () => {
15+
const mockDocument = {
16+
uri: vscode.Uri.parse('file:///dbDir/test.db'),
17+
documentKey: Promise.resolve('test-key'),
18+
};
19+
20+
const mockProvider = { webviews: new Map(), context: {} };
21+
const bridge = new HostBridge(mockProvider as any, mockDocument as any);
22+
23+
const showSaveDialogMock = mock.method(vscode.window, 'showSaveDialog', async () => vscode.Uri.parse('file:///dbDir/safe.txt'));
24+
const writeFileMock = mock.method(vscode.workspace.fs, 'writeFile', async () => {});
25+
26+
await bridge.saveFile('../../../etc/passwd', new Uint8Array([1, 2, 3]));
27+
28+
assert.strictEqual(showSaveDialogMock.mock.callCount(), 1);
29+
const args = showSaveDialogMock.mock.calls[0].arguments[0];
30+
// The defaultUri path should end with the base name 'passwd', not the traversed path
31+
assert.ok(args.defaultUri.path.endsWith('/dbDir/passwd'), `Expected safe path, got ${args.defaultUri.path}`);
32+
33+
assert.strictEqual(writeFileMock.mock.callCount(), 1);
34+
});
35+
36+
1437
it('openCellEditor should open correct URI for binary file with mime type', async () => {
1538
const executeCommandMock = mock.method(vscode.commands, 'executeCommand', async () => {});
1639

0 commit comments

Comments
 (0)