Skip to content

Commit 6b3c631

Browse files
committed
Create the unsafe plugin to configure how simple-git treats known potentially unsafe operations.
Snyk recognised a potential vulnerability caused by passing unsanitised user data into commands that operate on a git remote (eg `clone`, `fetch`, `pull` and `push` etc) whereby an inline configuration argument could be used to enable the `ext::` protocol to switch out the remote for an arbitrary named binary run on the host machine. While this highlights the need to sanitise user input in the application that consumes it before passing it through to any library, `simple-git` will now limit the use of these options unless the developer explicitly opts in to them with a new `allowUnsafeProtocolOverride` option.
1 parent 3324eed commit 6b3c631

File tree

7 files changed

+159
-1
lines changed

7 files changed

+159
-1
lines changed

docs/PLUGIN-UNSAFE-ACTIONS.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
## Unsafe Actions
2+
3+
As `simple-git` passes generated arguments through to a child process of the calling node.js process, it is recommended
4+
that any parameter sourced from user input is validated before being passed to the `simple-git` api.
5+
6+
In some cases where there is an elevated potential for harm `simple-git` will throw an exception unless you have
7+
explicitly opted in to the potentially unsafe action.
8+
9+
### Overriding allowed protocols
10+
11+
A standard installation of `git` permits `file`, `http` and `ssh` protocols for a remote. A range of
12+
[git remote helpers](https://git-scm.com/docs/gitremote-helpers) other than these default few can be
13+
used by referring to te helper name in the remote protocol - for example the git file descriptor transport
14+
[git-remote-fd](https://git-scm.com/docs/git-remote-fd) would be used in a remote protocol such as:
15+
16+
```
17+
git fetch "fd::<infd>[,<outfd>][/<anything>]"
18+
```
19+
20+
To avoid accidentally triggering a helper transport by passing through unsanitised user input to a function
21+
that expects a remote, the use of `-c protocol.fd.allow=always` (or any variant of protocol permission changes)
22+
will cause `simple-git` to throw unless it has been configured with:
23+
24+
```typescript
25+
import { simpleGit } from 'simple-git';
26+
27+
// throws
28+
await simpleGit()
29+
.raw('clone', 'ext::git-server-alias foo %G/repo', '-c', 'protocol.ext.allow=always');
30+
31+
// allows calling clone with a helper transport
32+
await simpleGit({ unsafe: { allowUnsafeProtocolOverride: true } })
33+
.raw('clone', 'ext::git-server-alias foo %G/repo', '-c', 'protocol.ext.allow=always');
34+
```
35+
36+
> *Be advised* helper transports can be used to call arbitrary binaries on the host machine.
37+
> Do not allow them in applications where you are not in control of the input parameters.
38+

simple-git/readme.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ await git.pull();
111111
- [Timeout](https://github.com/steveukx/git-js/blob/main/docs/PLUGIN-TIMEOUT.md)
112112
Automatically kill the wrapped `git` process after a rolling timeout.
113113

114+
- [Unsafe](https://github.com/steveukx/git-js/blob/main/docs/PLUGIN-UNSAFE-ACTIONS.md)
115+
Selectively opt out of `simple-git` safety precautions - for advanced users and use cases.
116+
114117
## Using Task Promises
115118

116119
Each task in the API returns the `simpleGit` instance for chaining together multiple tasks, and each
@@ -436,7 +439,8 @@ application hasn't been making use of non-documented APIs by importing from a su
436439

437440
See also:
438441

439-
- [release notes v2](https://github.com/steveukx/git-js/blob/main/docs/RELEASE-NOTES-V2.md)
442+
- [release notes v3](https://github.com/steveukx/git-js/blob/main/simple-git/CHANGELOG.md)
443+
- [release notes v2](https://github.com/steveukx/git-js/blob/main/docs/RELEASE-NOTES-V2.md)
440444

441445
# Concurrent / Parallel Requests
442446

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

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { SimpleGitFactory } from '../../typings';
33
import * as api from './api';
44
import {
55
abortPlugin,
6+
blockUnsafeOperationsPlugin,
67
commandConfigPrefixingPlugin,
78
completionDetectionPlugin,
89
errorDetectionHandler,
@@ -55,6 +56,7 @@ export function gitInstanceFactory(
5556
plugins.add(commandConfigPrefixingPlugin(config.config));
5657
}
5758

59+
plugins.add(blockUnsafeOperationsPlugin(config.unsafe));
5860
plugins.add(completionDetectionPlugin(config.completion));
5961
config.abort && plugins.add(abortPlugin(config.abort));
6062
config.progress && plugins.add(progressMonitorPlugin(config.progress));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { SimpleGitPlugin } from './simple-git-plugin';
2+
3+
import { GitPluginError } from '../errors/git-plugin-error';
4+
import type { SimpleGitPluginConfig } from '../types';
5+
6+
function isConfigSwitch(arg: string) {
7+
return arg.trim().toLowerCase() === '-c';
8+
}
9+
10+
function preventProtocolOverride(arg: string, next: string) {
11+
if (!isConfigSwitch(arg)) {
12+
return;
13+
}
14+
15+
if (!/^\s*protocol(.[a-z]+)?.allow/.test(next)) {
16+
return;
17+
}
18+
19+
throw new GitPluginError(
20+
undefined,
21+
'unsafe',
22+
'Configuring protocol.allow is not permitted without enabling allowUnsafeExtProtocol'
23+
);
24+
}
25+
26+
export function blockUnsafeOperationsPlugin({
27+
allowUnsafeProtocolOverride = false,
28+
}: SimpleGitPluginConfig['unsafe'] = {}): SimpleGitPlugin<'spawn.args'> {
29+
return {
30+
type: 'spawn.args',
31+
action(args, _context) {
32+
args.forEach((current, index) => {
33+
const next = index < args.length ? args[index + 1] : '';
34+
35+
allowUnsafeProtocolOverride || preventProtocolOverride(current, next);
36+
});
37+
38+
return args;
39+
},
40+
};
41+
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './abort-plugin';
2+
export * from './block-unsafe-operations-plugin';
23
export * from './command-config-prefixing-plugin';
34
export * from './completion-detection.plugin';
45
export * from './error-detection.plugin';

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

+16
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,22 @@ export interface SimpleGitPluginConfig {
108108
};
109109

110110
spawnOptions: Pick<SpawnOptions, 'uid' | 'gid'>;
111+
112+
unsafe: {
113+
/**
114+
* By default `simple-git` prevents the use of inline configuration
115+
* options to override the protocols available for the `git` child
116+
* process to prevent accidental security vulnerabilities when
117+
* unsanitised user data is passed directly into operations such as
118+
* `git.addRemote`, `git.clone` or `git.raw`.
119+
*
120+
* Enable this override to use the `ext::` protocol (see examples on
121+
* [git-scm.com](https://git-scm.com/docs/git-remote-ext#_examples)).
122+
*
123+
* See documentation for use in
124+
*/
125+
allowUnsafeProtocolOverride?: boolean;
126+
};
111127
}
112128

113129
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { promiseError, promiseResult } from '@kwsites/promise-result';
2+
import {
3+
assertGitError,
4+
createTestContext,
5+
newSimpleGit,
6+
SimpleGitTestContext,
7+
} from '@simple-git/test-utils';
8+
9+
import { GitPluginError } from '../..';
10+
11+
describe('add', () => {
12+
let context: SimpleGitTestContext;
13+
14+
beforeEach(async () => (context = await createTestContext()));
15+
16+
it('allows overriding protocol when opting in to unsafe practices', async () => {
17+
const { threw } = await promiseResult(
18+
newSimpleGit(context.root, { unsafe: { allowUnsafeProtocolOverride: true } }).raw(
19+
'-c',
20+
'protocol.ext.allow=always',
21+
'init'
22+
)
23+
);
24+
25+
expect(threw).toBe(false);
26+
});
27+
28+
it('prevents overriding protocol.ext.allow before the method of a command', async () => {
29+
assertGitError(
30+
await promiseError(context.git.raw('-c', 'protocol.ext.allow=always', 'init')),
31+
'Configuring protocol.allow is not permitted',
32+
GitPluginError
33+
);
34+
});
35+
36+
it('prevents overriding protocol.ext.allow after the method of a command', async () => {
37+
assertGitError(
38+
await promiseError(context.git.raw('init', '-c', 'protocol.ext.allow=always')),
39+
'Configuring protocol.allow is not permitted',
40+
GitPluginError
41+
);
42+
});
43+
44+
it('prevents adding a remote with vulnerable ext transport', async () => {
45+
assertGitError(
46+
await promiseError(
47+
context.git.clone(`ext::sh -c touch% /tmp/pwn% >&2`, '/tmp/example-new-repo', [
48+
'-c',
49+
'protocol.ext.allow=always',
50+
])
51+
),
52+
'Configuring protocol.allow is not permitted',
53+
GitPluginError
54+
);
55+
});
56+
});

0 commit comments

Comments
 (0)