Skip to content

Commit a263635

Browse files
authored
Clone API use pathspec (#1132)
* Use a `pathspec` wrapper for remote and local paths when using `git.clone` and `git.mirror` Extract `clone` methods to `SimpleGitApi` base class. * Fix block-unsafe test now handled with pathspec * Restore ability to use `-b non-default-branch` in `git.clone` Closes #1137 * Add tests for unsafe `-u` switch detection * Add documentation for the use of pathspecs
1 parent de825be commit a263635

File tree

12 files changed

+147
-71
lines changed

12 files changed

+147
-71
lines changed

.changeset/cold-moments-yell.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"simple-git": minor
3+
---
4+
5+
Use `pathspec` wrappers for remote and local paths when running either `git.clone` or `git.mirror` to
6+
avoid leaving them less open for unexpected outcomes when passing unsanitised data into these tasks.

simple-git/readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ in v2 (deprecation notices were logged to `stdout` as `console.warn` in v2).
250250

251251
- `mirror(repoPath, [localPath, [options]])` behaves the same as the `.clone` interface with the [`--mirror` flag](https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---mirror) enabled.
252252

253+
Note: as of version 3.33.0, `repoPath` and `localPath` are passed to `git` as "pathspec" arguments so are less open to unexpected unsafe behaviour when passing unsanitised data into the command.
254+
253255
## git config
254256

255257
- `.addConfig(key, value, append = false, scope = 'local')` add a local configuration property, when `append` is set to

simple-git/src/git.js

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ const {
2323
} = require('./lib/tasks/branch');
2424
const { checkIgnoreTask } = require('./lib/tasks/check-ignore');
2525
const { checkIsRepoTask } = require('./lib/tasks/check-is-repo');
26-
const { cloneTask, cloneMirrorTask } = require('./lib/tasks/clone');
2726
const { cleanWithOptionsTask, isCleanOptionsArray } = require('./lib/tasks/clean');
2827
const { diffSummaryTask } = require('./lib/tasks/diff');
2928
const { fetchTask } = require('./lib/tasks/fetch');
@@ -101,34 +100,6 @@ Git.prototype.stashList = function (options) {
101100
);
102101
};
103102

104-
function createCloneTask(api, task, repoPath, localPath) {
105-
if (typeof repoPath !== 'string') {
106-
return configurationErrorTask(`git.${api}() requires a string 'repoPath'`);
107-
}
108-
109-
return task(repoPath, filterType(localPath, filterString), getTrailingOptions(arguments));
110-
}
111-
112-
/**
113-
* Clone a git repo
114-
*/
115-
Git.prototype.clone = function () {
116-
return this._runTask(
117-
createCloneTask('clone', cloneTask, ...arguments),
118-
trailingFunctionArgument(arguments)
119-
);
120-
};
121-
122-
/**
123-
* Mirror a git repo
124-
*/
125-
Git.prototype.mirror = function () {
126-
return this._runTask(
127-
createCloneTask('mirror', cloneMirrorTask, ...arguments),
128-
trailingFunctionArgument(arguments)
129-
);
130-
};
131-
132103
/**
133104
* Moves one or more files to a new destination.
134105
*

simple-git/src/lib/git-factory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ export function gitInstanceFactory(
5959
}
6060

6161
plugins.add(blockUnsafeOperationsPlugin(config.unsafe));
62-
plugins.add(suffixPathsPlugin());
6362
plugins.add(completionDetectionPlugin(config.completion));
6463
config.abort && plugins.add(abortPlugin(config.abort));
6564
config.progress && plugins.add(progressMonitorPlugin(config.progress));
6665
config.timeout && plugins.add(timeoutPlugin(config.timeout));
6766
config.spawnOptions && plugins.add(spawnOptionsPlugin(config.spawnOptions));
67+
plugins.add(suffixPathsPlugin());
6868

6969
plugins.add(errorDetectionPlugin(errorDetectionHandler(true)));
7070
config.errors && plugins.add(errorDetectionPlugin(config.errors));

simple-git/src/lib/plugins/block-unsafe-operations-plugin.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ function isConfigSwitch(arg: string | unknown) {
77
return typeof arg === 'string' && arg.trim().toLowerCase() === '-c';
88
}
99

10-
function isCloneSwitch(char: string, arg: string | unknown) {
10+
export function isCloneUploadPackSwitch(char: string, arg: string | unknown) {
1111
if (typeof arg !== 'string' || !arg.includes(char)) {
1212
return false;
1313
}
1414

15-
const token = arg.replace(/\0g/, '').replace(/^(--no)?-{1,2}/, '');
16-
return /^[\dlsqvnobucj]+\b/.test(token);
15+
const cleaned = arg.trim().replace(/\0/g, '');
16+
return /^(--no)?-{1,2}[\dlsqvnobucj]+(\s|$)/.test(cleaned);
1717
}
1818

1919
function preventConfigBuilder(
@@ -59,7 +59,7 @@ function preventUploadPack(arg: string, method: string) {
5959
);
6060
}
6161

62-
if (method === 'clone' && isCloneSwitch('u', arg)) {
62+
if (method === 'clone' && isCloneUploadPackSwitch('u', arg)) {
6363
throw new GitPluginError(
6464
undefined,
6565
'unsafe',

simple-git/src/lib/plugins/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export * from './abort-plugin';
2-
export * from './block-unsafe-operations-plugin';
2+
export { blockUnsafeOperationsPlugin } from './block-unsafe-operations-plugin';
33
export * from './command-config-prefixing-plugin';
44
export * from './completion-detection.plugin';
55
export * from './custom-binary.plugin';

simple-git/src/lib/simple-git-api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
getTrailingOptions,
2525
trailingFunctionArgument,
2626
} from './utils';
27+
import clone from './tasks/clone';
2728

2829
export class SimpleGitApi implements SimpleGitBase {
2930
constructor(private _executor: SimpleGitExecutor) {}
@@ -144,6 +145,7 @@ export class SimpleGitApi implements SimpleGitBase {
144145
Object.assign(
145146
SimpleGitApi.prototype,
146147
checkout(),
148+
clone(),
147149
commit(),
148150
config(),
149151
countObjects(),

simple-git/src/lib/tasks/clone.ts

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import { configurationErrorTask, EmptyTask, straightThroughStringTask } from './task';
22
import { OptionFlags, Options, StringTask } from '../types';
3-
import { append, filterString } from '../utils';
3+
import {
4+
append,
5+
filterString,
6+
filterType,
7+
getTrailingOptions,
8+
trailingFunctionArgument,
9+
} from '../utils';
10+
import { pathspec } from '../args/pathspec';
11+
import { SimpleGit } from '../../../typings';
12+
import { SimpleGitApi } from '../simple-git-api';
413

514
export type CloneOptions = Options &
615
OptionFlags<
@@ -29,34 +38,53 @@ export type CloneOptions = Options &
2938
string
3039
>;
3140

32-
function disallowedCommand(command: string) {
33-
return /^--upload-pack(=|$)/.test(command);
34-
}
35-
36-
export function cloneTask(
41+
type CloneTaskBuilder = (
3742
repo: string | undefined,
3843
directory: string | undefined,
3944
customArgs: string[]
40-
): StringTask<string> | EmptyTask {
41-
const commands = ['clone', ...customArgs];
45+
) => StringTask<string> | EmptyTask;
4246

43-
filterString(repo) && commands.push(repo);
44-
filterString(directory) && commands.push(directory);
47+
export const cloneTask: CloneTaskBuilder = (repo, directory, customArgs) => {
48+
const commands = ['clone', ...customArgs];
4549

46-
const banned = commands.find(disallowedCommand);
47-
if (banned) {
48-
return configurationErrorTask(`git.fetch: potential exploit argument blocked.`);
49-
}
50+
filterString(repo) && commands.push(pathspec(repo));
51+
filterString(directory) && commands.push(pathspec(directory));
5052

5153
return straightThroughStringTask(commands);
52-
}
54+
};
5355

54-
export function cloneMirrorTask(
55-
repo: string | undefined,
56-
directory: string | undefined,
57-
customArgs: string[]
58-
) {
56+
export const cloneMirrorTask: CloneTaskBuilder = (repo, directory, customArgs) => {
5957
append(customArgs, '--mirror');
6058

6159
return cloneTask(repo, directory, customArgs);
60+
};
61+
62+
function createCloneTask(
63+
api: 'clone' | 'mirror',
64+
task: CloneTaskBuilder,
65+
repoPath: string | undefined,
66+
...args: unknown[]
67+
) {
68+
if (!filterString(repoPath)) {
69+
return configurationErrorTask(`git.${api}() requires a string 'repoPath'`);
70+
}
71+
72+
return task(repoPath, filterType(args[0], filterString), getTrailingOptions(arguments));
73+
}
74+
75+
export default function (): Pick<SimpleGit, 'clone' | 'mirror'> {
76+
return {
77+
clone(this: SimpleGitApi, repo: string | unknown, ...rest: unknown[]) {
78+
return this._runTask(
79+
createCloneTask('clone', cloneTask, filterType(repo, filterString), ...rest),
80+
trailingFunctionArgument(arguments)
81+
);
82+
},
83+
mirror(this: SimpleGitApi, repo: string | unknown, ...rest: unknown[]) {
84+
return this._runTask(
85+
createCloneTask('mirror', cloneMirrorTask, filterType(repo, filterString), ...rest),
86+
trailingFunctionArgument(arguments)
87+
);
88+
},
89+
};
6290
}

simple-git/src/lib/utils/argument-filters.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const filterNumber: ArgumentFilterPredicate<number> = (input: unknown): i
3939
};
4040

4141
export const filterString: ArgumentFilterPredicate<string> = (input: unknown): input is string => {
42-
return typeof input === 'string';
42+
return typeof input === 'string' || isPathSpec(input);
4343
};
4444

4545
export const filterStringOrStringArray: ArgumentFilterPredicate<string | string[]> = (

simple-git/test/integration/plugin.unsafe.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('plugin.unsafe', () => {
5757
});
5858

5959
it('command injection report', async () => {
60-
for (const i of [45, 54, 52, 45, 118, 115, 113, 110, 108]) {
60+
for (const i of [45, 54, 52, 118, 115, 113, 110, 108]) {
6161
expectError(
6262
await promiseResult(
6363
newSimpleGit({ baseDir: context.root })

0 commit comments

Comments
 (0)