Skip to content

Commit 47030d5

Browse files
authored
Merge pull request #861 from steveukx/security/protocols
Create the `unsafe` plugin to configure how `simple-git` treats known potentially unsafe operations.
2 parents 3324eed + 6b3c631 commit 47030d5

File tree

7 files changed

+159
-1
lines changed

7 files changed

+159
-1
lines changed

Diff for: 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+

Diff for: 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

Diff for: 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+
}

Diff for: 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';

Diff for: 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
/**

Diff for: simple-git/test/integration/plugin.unsafe.spec.ts

+56
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)