Skip to content

Commit b221e58

Browse files
committed
Replaces OS-specific unzip with js solution
- Filters files during extraction to only extract needed binaries - Adds path traversal protection for security - Automatically sets executable permissions on Unix systems
1 parent 54fc371 commit b221e58

File tree

5 files changed

+127
-35
lines changed

5 files changed

+127
-35
lines changed

ThirdPartyNotices.txt

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,20 @@ This project incorporates components from the projects listed below.
2323
18. billboard.js version 3.17.0 (https://github.com/naver/billboard.js)
2424
19. diff2html version 3.4.52 (https://github.com/rtfpessoa/diff2html)
2525
20. driver.js version 1.3.6 (https://github.com/kamranahmedse/driver.js)
26-
21. https-proxy-agent version 5.0.1 (https://github.com/TooTallNate/node-https-proxy-agent)
27-
22. iconv-lite version 0.6.3 (https://github.com/ashtuchkin/iconv-lite)
28-
23. lit version 3.3.1 (https://github.com/lit/lit)
29-
24. marked version 16.3.0 (https://github.com/markedjs/marked)
30-
25. microsoft/vscode (https://github.com/microsoft/vscode)
31-
26. node-fetch version 2.7.0 (https://github.com/bitinn/node-fetch)
32-
27. os-browserify version 0.3.0 (https://github.com/CoderPuppy/os-browserify)
33-
28. path-browserify version 1.0.1 (https://github.com/browserify/path-browserify)
34-
29. react-dom version 19.0.0 (https://github.com/facebook/react)
35-
30. react version 19.0.0 (https://github.com/facebook/react)
36-
31. signal-utils version 0.21.1 (https://github.com/proposal-signals/signal-utils)
37-
32. slug version 11.0.0 (https://github.com/Trott/slug)
38-
33. sortablejs version 1.15.6 (https://github.com/SortableJS/Sortable)
26+
21. fflate version 0.8.2 (https://github.com/101arrowz/fflate)
27+
22. https-proxy-agent version 5.0.1 (https://github.com/TooTallNate/node-https-proxy-agent)
28+
23. iconv-lite version 0.6.3 (https://github.com/ashtuchkin/iconv-lite)
29+
24. lit version 3.3.1 (https://github.com/lit/lit)
30+
25. marked version 16.3.0 (https://github.com/markedjs/marked)
31+
26. microsoft/vscode (https://github.com/microsoft/vscode)
32+
27. node-fetch version 2.7.0 (https://github.com/bitinn/node-fetch)
33+
28. os-browserify version 0.3.0 (https://github.com/CoderPuppy/os-browserify)
34+
29. path-browserify version 1.0.1 (https://github.com/browserify/path-browserify)
35+
30. react-dom version 19.0.0 (https://github.com/facebook/react)
36+
31. react version 19.0.0 (https://github.com/facebook/react)
37+
32. signal-utils version 0.21.1 (https://github.com/proposal-signals/signal-utils)
38+
33. slug version 11.0.0 (https://github.com/Trott/slug)
39+
34. sortablejs version 1.15.6 (https://github.com/SortableJS/Sortable)
3940

4041
%% @gk-nzaytsev/fast-string-truncated-width NOTICES AND INFORMATION BEGIN HERE
4142
=========================================
@@ -1847,6 +1848,32 @@ THE SOFTWARE.
18471848
=========================================
18481849
END OF driver.js NOTICES AND INFORMATION
18491850

1851+
%% fflate NOTICES AND INFORMATION BEGIN HERE
1852+
=========================================
1853+
MIT License
1854+
1855+
Copyright (c) 2023 Arjun Barrett
1856+
1857+
Permission is hereby granted, free of charge, to any person obtaining a copy
1858+
of this software and associated documentation files (the "Software"), to deal
1859+
in the Software without restriction, including without limitation the rights
1860+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1861+
copies of the Software, and to permit persons to whom the Software is
1862+
furnished to do so, subject to the following conditions:
1863+
1864+
The above copyright notice and this permission notice shall be included in all
1865+
copies or substantial portions of the Software.
1866+
1867+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1868+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1869+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1870+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1871+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1872+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1873+
SOFTWARE.
1874+
=========================================
1875+
END OF fflate NOTICES AND INFORMATION
1876+
18501877
%% https-proxy-agent NOTICES AND INFORMATION BEGIN HERE
18511878
=========================================
18521879
https-proxy-agent

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25069,6 +25069,7 @@
2506925069
"billboard.js": "3.17.0",
2507025070
"diff2html": "3.4.52",
2507125071
"driver.js": "1.3.6",
25072+
"fflate": "0.8.2",
2507225073
"https-proxy-agent": "5.0.1",
2507325074
"iconv-lite": "0.6.3",
2507425075
"lit": "3.3.1",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/env/node/gk/cli/integration.ts

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@ import { debug, log } from '../../../../system/decorators/log';
1717
import { Logger } from '../../../../system/logger';
1818
import { getLogScope, setLogScopeExit } from '../../../../system/logger.scope';
1919
import { compare } from '../../../../system/version';
20-
import { run } from '../../git/shell';
2120
import { getPlatform, isWeb } from '../../platform';
2221
import { CliCommandHandlers } from './commands';
2322
import type { IpcServer } from './ipcServer';
2423
import { createIpcServer } from './ipcServer';
25-
import { runCLICommand, showManualMcpSetupPrompt, toMcpInstallProvider } from './utils';
24+
import { extractZipFile, runCLICommand, showManualMcpSetupPrompt, toMcpInstallProvider } from './utils';
2625

2726
const enum CLIInstallErrorReason {
2827
UnsupportedPlatform,
@@ -611,28 +610,14 @@ export class GkCliIntegrationProvider implements Disposable {
611610
}
612611

613612
try {
614-
// Use the run function to extract the installer file from the installer zip
615-
if (platform === 'windows') {
616-
// On Windows, use PowerShell to extract the zip file.
617-
// Force overwrite if the file already exists with -Force
618-
await run(
619-
'powershell.exe',
620-
[
621-
'-Command',
622-
`Expand-Archive -Path "${cliProxyZipFilePath.fsPath}" -DestinationPath "${globalStoragePath.fsPath}" -Force`,
623-
],
624-
'utf8',
625-
);
626-
} else {
627-
// On Unix-like systems, use the unzip command to extract the zip file, forcing overwrite with -o
628-
await run('unzip', ['-o', cliProxyZipFilePath.fsPath, '-d', globalStoragePath.fsPath], 'utf8');
629-
}
613+
// Extract only the gk binary from the zip file using the fflate library (cross-platform)
614+
const expectedBinary = platform === 'windows' ? 'gk.exe' : 'gk';
615+
await extractZipFile(cliProxyZipFilePath.fsPath, globalStoragePath.fsPath, {
616+
filter: filename => filename === expectedBinary || filename.endsWith(`/${expectedBinary}`),
617+
});
630618

631619
// Check using stat to make sure the newly extracted file exists.
632-
cliExtractedProxyFilePath = Uri.joinPath(
633-
globalStoragePath,
634-
platform === 'windows' ? 'gk.exe' : 'gk',
635-
);
620+
cliExtractedProxyFilePath = Uri.joinPath(globalStoragePath, expectedBinary);
636621

637622
// This will throw if the file doesn't exist
638623
await workspace.fs.stat(cliExtractedProxyFilePath);

src/env/node/gk/cli/utils.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,81 @@
1+
import { chmod, mkdir, readFile, writeFile } from 'fs/promises';
2+
import { dirname, resolve, sep } from 'path';
13
import { window } from 'vscode';
24
import { urls } from '../../../../constants';
35
import { Container } from '../../../../container';
46
import { openUrl } from '../../../../system/-webview/vscode/uris';
57
import { run } from '../../git/shell';
68
import { getPlatform } from '../../platform';
79

10+
/**
11+
* Extracts a zip file to a destination directory using the fflate library.
12+
* This is a cross-platform alternative to using OS-specific unzip commands.
13+
*
14+
* @param zipPath - The path to the zip file to extract
15+
* @param destPath - The destination directory where files will be extracted
16+
* @param options - Optional extraction options
17+
* @param options.filter - Optional filter function to select which files to extract. Return true to extract the file.
18+
* @throws Error if extraction fails or if path traversal is detected
19+
*/
20+
export async function extractZipFile(
21+
zipPath: string,
22+
destPath: string,
23+
options?: { filter?: (filename: string) => boolean },
24+
): Promise<void> {
25+
// Dynamically import fflate to avoid bundling it when not needed
26+
const { unzip } = await import(/* webpackChunkName: "lib-unzip" */ 'fflate');
27+
28+
// Read the zip file (returns a Buffer, which extends Uint8Array in Node.js)
29+
const zipData = await readFile(zipPath);
30+
31+
// Unzip asynchronously (runs in worker thread, doesn't block main thread)
32+
// Use fflate's built-in filter to avoid decompressing unwanted files
33+
const filter = options?.filter;
34+
const unzipped = await new Promise<Record<string, Uint8Array>>((resolve, reject) => {
35+
unzip(
36+
// Buffer is a Uint8Array, but TypeScript needs the cast for strict type checking
37+
zipData as Uint8Array,
38+
{
39+
filter: filter
40+
? file => {
41+
// Skip directory entries (they end with /)
42+
if (file.name.endsWith('/')) return false;
43+
// Apply user filter
44+
return filter(file.name);
45+
}
46+
: undefined,
47+
},
48+
(err, result) => {
49+
if (err) {
50+
reject(err);
51+
} else {
52+
resolve(result);
53+
}
54+
},
55+
);
56+
});
57+
58+
// Extract the files
59+
for (const [filename, data] of Object.entries(unzipped)) {
60+
// Skip directory entries (they end with /)
61+
if (filename.endsWith('/')) continue;
62+
63+
// Resolve the full path and ensure it's within the destination (prevents path traversal)
64+
const filePath = resolve(destPath, filename);
65+
const resolvedDest = resolve(destPath);
66+
if (!filePath.startsWith(resolvedDest + sep) && filePath !== resolvedDest) {
67+
throw new Error(`Path traversal detected in zip file: ${filename}`);
68+
}
69+
70+
await mkdir(dirname(filePath), { recursive: true });
71+
await writeFile(filePath, data);
72+
// Make 'gk' executable on Unix systems
73+
if (getPlatform() !== 'windows' && (filename === 'gk' || filename.endsWith('/gk'))) {
74+
await chmod(filePath, 0o755);
75+
}
76+
}
77+
}
78+
879
export function toMcpInstallProvider<T extends string | undefined>(appHostName: T): T {
980
switch (appHostName) {
1081
case 'code':

0 commit comments

Comments
 (0)