Skip to content

Commit e253a0d

Browse files
authored
Fix/block unsafe 2603 (#1135)
* Add detection for more potential attack vectors through `git -c` configuration switches
1 parent c077260 commit e253a0d

File tree

4 files changed

+96
-26
lines changed

4 files changed

+96
-26
lines changed

.changeset/shaky-readers-share.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"simple-git": patch
3+
---
4+
5+
Enhanced `git -c` checks in `unsafe` plugin.
6+
7+
Thanks to @JohannesLks for identifying the issue

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

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,40 @@ function isCloneSwitch(char: string, arg: string | unknown) {
1616
return /^[\dlsqvnobucj]+\b/.test(token);
1717
}
1818

19-
function preventProtocolOverride(arg: string, next: string) {
20-
if (!isConfigSwitch(arg)) {
21-
return;
22-
}
23-
24-
if (!/^\s*protocol(.[a-z]+)?.allow/i.test(next)) {
25-
return;
26-
}
19+
function preventConfigBuilder(
20+
config: string | RegExp,
21+
setting: keyof SimpleGitPluginConfig['unsafe'],
22+
message = String(config)
23+
) {
24+
const regex = typeof config === 'string' ? new RegExp(`\\s*${config}`, 'i') : config;
2725

28-
throw new GitPluginError(
29-
undefined,
30-
'unsafe',
31-
'Configuring protocol.allow is not permitted without enabling allowUnsafeExtProtocol'
32-
);
26+
return function preventCommand(
27+
options: Partial<SimpleGitPluginConfig['unsafe']>,
28+
arg: string,
29+
next: string
30+
) {
31+
if (options[setting] !== true && isConfigSwitch(arg) && regex.test(next)) {
32+
throw new GitPluginError(
33+
undefined,
34+
'unsafe',
35+
`Configuring ${message} is not permitted without enabling ${setting}`
36+
);
37+
}
38+
};
3339
}
3440

41+
const preventUnsafeConfig = [
42+
preventConfigBuilder(
43+
/^\s*protocol(.[a-z]+)?.allow/i,
44+
'allowUnsafeProtocolOverride',
45+
'protocol.allow'
46+
),
47+
preventConfigBuilder('core.sshCommand', 'allowUnsafeSshCommand'),
48+
preventConfigBuilder('core.gitProxy', 'allowUnsafeGitProxy'),
49+
preventConfigBuilder('core.hooksPath', 'allowUnsafeHooksPath'),
50+
preventConfigBuilder('diff.external', 'allowUnsafeDiffExternal'),
51+
];
52+
3553
function preventUploadPack(arg: string, method: string) {
3654
if (/^\s*--(upload|receive)-pack/.test(arg)) {
3755
throw new GitPluginError(
@@ -59,17 +77,17 @@ function preventUploadPack(arg: string, method: string) {
5977
}
6078

6179
export function blockUnsafeOperationsPlugin({
62-
allowUnsafeProtocolOverride = false,
6380
allowUnsafePack = false,
81+
...options
6482
}: SimpleGitPluginConfig['unsafe'] = {}): SimpleGitPlugin<'spawn.args'> {
6583
return {
6684
type: 'spawn.args',
6785
action(args, context) {
6886
args.forEach((current, index) => {
6987
const next = index < args.length ? args[index + 1] : '';
7088

71-
allowUnsafeProtocolOverride || preventProtocolOverride(current, next);
7289
allowUnsafePack || preventUploadPack(current, context.method);
90+
preventUnsafeConfig.forEach((helper) => helper(options, current, next));
7391
});
7492

7593
return args;

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export interface SimpleGitPluginConfig {
137137
allowUnsafeCustomBinary?: boolean;
138138

139139
/**
140-
* By default `simple-git` prevents the use of inline configuration
140+
* By default, `simple-git` prevents the use of inline configuration
141141
* options to override the protocols available for the `git` child
142142
* process to prevent accidental security vulnerabilities when
143143
* unsanitised user data is passed directly into operations such as
@@ -156,6 +156,30 @@ export interface SimpleGitPluginConfig {
156156
* Enable this override to permit the use of these arguments.
157157
*/
158158
allowUnsafePack?: boolean;
159+
160+
/**
161+
* Using a `-c` switch to enable custom SSH commands opens up a potential
162+
* attack vector for running arbitrary commands.
163+
*/
164+
allowUnsafeSshCommand?: boolean;
165+
166+
/**
167+
* Using a `-c` switch to enable custom proxy command for the `git://` transport
168+
* exposes and attack vector for running arbitrary commands.
169+
*/
170+
allowUnsafeGitProxy?: boolean;
171+
172+
/**
173+
* Using a `-c` switch to enable custom hooks path commands to be run automatically
174+
* exposes and attack vector for running arbitrary commands.
175+
*/
176+
allowUnsafeHooksPath?: boolean;
177+
178+
/**
179+
* Using a `-c` switch to enable setting binary for processing diffs
180+
* exposes and attack vector for running arbitrary commands.
181+
*/
182+
allowUnsafeDiffExternal?: boolean;
159183
};
160184
}
161185

simple-git/test/unit/plugins/plugin.unsafe.spec.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { promiseError } from '@kwsites/promise-result';
2-
import {
3-
assertExecutedCommands,
4-
assertGitError,
5-
closeWithSuccess,
6-
newSimpleGit,
7-
} from '../__fixtures__';
2+
import { assertExecutedCommands, assertGitError, closeWithSuccess, newSimpleGit } from '../__fixtures__';
83

94
describe('blockUnsafeOperationsPlugin', () => {
105
it.each([
@@ -13,12 +8,12 @@ describe('blockUnsafeOperationsPlugin', () => {
138
['Protocol.Allow=always'],
149
['PROTOCOL.allow=always'],
1510
['protocol.ALLOW=always'],
16-
])('blocks protocol overide in format %s', async (cmd) => {
11+
])('blocks protocol override in format %s', async (cmd) => {
1712
const task = ['config', '-c', cmd, 'config', '--list'];
1813

1914
assertGitError(
2015
await promiseError(newSimpleGit().raw(...task)),
21-
'allowUnsafeExtProtocol'
16+
'allowUnsafeProtocolOverride',
2217
);
2318

2419
const err = promiseError(
@@ -38,18 +33,44 @@ describe('blockUnsafeOperationsPlugin', () => {
3833
])('allows %s %s only when using override', async (cmd, option) => {
3934
assertGitError(
4035
await promiseError(newSimpleGit({ unsafe: {} }).raw(cmd, option)),
41-
'allowUnsafePack'
36+
'allowUnsafePack',
4237
);
4338

4439
const err = promiseError(
45-
newSimpleGit({ unsafe: { allowUnsafePack: true } }).raw(cmd, option)
40+
newSimpleGit({ unsafe: { allowUnsafePack: true } }).raw(cmd, option),
4641
);
4742

4843
await closeWithSuccess();
4944
expect(await err).toBeUndefined();
5045
assertExecutedCommands(cmd, option);
5146
});
5247

48+
describe.each([
49+
['allowUnsafeSshCommand', `core.sshCommand=sh -c 'id > pwned'`],
50+
['allowUnsafeGitProxy', `core.gitProxy=sh -c 'id > pwned'`],
51+
['allowUnsafeHooksPath', `core.hooksPath=sh -c 'id > pwned'`],
52+
['allowUnsafeDiffExternal', `diff.external=sh -c 'id > pwned'`],
53+
])('unsafe config option - %s', (setting, command) => {
54+
55+
it('blocks by default', async () => {
56+
const err = promiseError(
57+
newSimpleGit().clone('remote', 'local', ['-c', command]),
58+
);
59+
await promiseError(closeWithSuccess());
60+
61+
assertGitError(await err, setting);
62+
});
63+
64+
it('allows with override', async () => {
65+
const err = promiseError(
66+
newSimpleGit({ unsafe: { [setting]: true } }).clone('remote', 'local', ['-c', command]),
67+
);
68+
await closeWithSuccess();
69+
70+
expect(await err).toBeUndefined();
71+
});
72+
});
73+
5374
it('allows -u for non-clone commands', async () => {
5475
const git = newSimpleGit({ unsafe: {} });
5576
const err = promiseError(git.raw('push', '-u', 'origin/main'));

0 commit comments

Comments
 (0)