Skip to content

Commit b675be1

Browse files
RomanDavydchukpllusin
authored andcommitted
feat: Add N8N_GIT_NODE_DISABLE_BARE_REPOS environment variable to allow users to disable bare repositories in Git Node (n8n-io#19559)
1 parent 052344b commit b675be1

File tree

7 files changed

+180
-0
lines changed

7 files changed

+180
-0
lines changed

packages/@n8n/config/src/configs/security.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,10 @@ export class SecurityConfig {
4545
*/
4646
@Env('N8N_INSECURE_DISABLE_WEBHOOK_IFRAME_SANDBOX')
4747
disableWebhookHtmlSandboxing: boolean = false;
48+
49+
/**
50+
* Whether to disable bare repositories support in the Git node.
51+
*/
52+
@Env('N8N_GIT_NODE_DISABLE_BARE_REPOS')
53+
disableBareRepos: boolean = false;
4854
}

packages/@n8n/config/test/config.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ describe('GlobalConfig', () => {
307307
contentSecurityPolicy: '{}',
308308
contentSecurityPolicyReportOnly: false,
309309
disableWebhookHtmlSandboxing: false,
310+
disableBareRepos: false,
310311
},
311312
executions: {
312313
timeout: -1,

packages/cli/BREAKING-CHANGES.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
This list shows all the versions which include breaking changes and how to upgrade.
44

5+
# 1.113.0
6+
7+
### What changed?
8+
9+
Support for bare repositories in Git Node was dropped in the cloud version of n8n due to security reasons. Also, an environment variable `N8N_GIT_NODE_DISABLE_BARE_REPOS` was added that allows self-hosted users to disable bare repositories as well.
10+
11+
### When is action necessary?
12+
13+
If you have workflows that use the Git Node and work with bare git repositories.
14+
515
# 1.109.0
616

717
### What changed?

packages/cli/src/deprecation/__tests__/deprecation.service.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe('DeprecationService', () => {
2020
// this test suite.
2121
process.env = {
2222
N8N_BLOCK_ENV_ACCESS_IN_NODE: 'false',
23+
N8N_GIT_NODE_DISABLE_BARE_REPOS: 'false',
2324
};
2425

2526
jest.resetAllMocks();
@@ -140,6 +141,7 @@ describe('DeprecationService', () => {
140141
process.env = {
141142
N8N_RUNNERS_ENABLED: 'true',
142143
N8N_BLOCK_ENV_ACCESS_IN_NODE: 'false',
144+
N8N_GIT_NODE_DISABLE_BARE_REPOS: 'false',
143145
};
144146

145147
jest.spyOn(config, 'getEnv').mockImplementation((key) => {
@@ -239,6 +241,7 @@ describe('DeprecationService', () => {
239241
beforeEach(() => {
240242
process.env = {
241243
N8N_RUNNERS_ENABLED: 'true',
244+
N8N_GIT_NODE_DISABLE_BARE_REPOS: 'false',
242245
};
243246

244247
jest.resetAllMocks();
@@ -259,4 +262,29 @@ describe('DeprecationService', () => {
259262
},
260263
);
261264
});
265+
266+
describe('N8N_GIT_NODE_DISABLE_BARE_REPOS', () => {
267+
beforeEach(() => {
268+
process.env = {
269+
N8N_RUNNERS_ENABLED: 'true',
270+
N8N_BLOCK_ENV_ACCESS_IN_NODE: 'false',
271+
};
272+
jest.resetAllMocks();
273+
});
274+
275+
test('should warn when N8N_GIT_NODE_DISABLE_BARE_REPOS is not set', () => {
276+
delete process.env.N8N_GIT_NODE_DISABLE_BARE_REPOS;
277+
deprecationService.warn();
278+
expect(logger.warn).toHaveBeenCalled();
279+
});
280+
281+
test.each(['false', 'true'])(
282+
'should not warn when N8N_GIT_NODE_DISABLE_BARE_REPOS is %s',
283+
(value) => {
284+
process.env.N8N_GIT_NODE_DISABLE_BARE_REPOS = value;
285+
deprecationService.warn();
286+
expect(logger.warn).not.toHaveBeenCalled();
287+
},
288+
);
289+
});
262290
});

packages/cli/src/deprecation/deprecation.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ export class DeprecationService {
109109
'The default value of N8N_BLOCK_ENV_ACCESS_IN_NODE will be changed from false to true in a future version. If you need to access environment variables from the Code Node or from expressions, please set N8N_BLOCK_ENV_ACCESS_IN_NODE=false. Learn more: https://docs.n8n.io/hosting/configuration/environment-variables/security/',
110110
checkValue: (value: string | undefined) => value === undefined || value === '',
111111
},
112+
{
113+
envVar: 'N8N_GIT_NODE_DISABLE_BARE_REPOS',
114+
message:
115+
'Support for bare repositories in the Git Node will be removed in a future version due to security concerns. If you are not using bare repositories in the Git Node, please set N8N_GIT_NODE_DISABLE_BARE_REPOS=true. Learn more: https://docs.n8n.io/hosting/configuration/environment-variables/security/',
116+
checkValue: (value: string | undefined) => value === undefined || value === '',
117+
},
112118
];
113119

114120
/** Runtime state of deprecation-related env vars. */

packages/nodes-base/nodes/Git/Git.node.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
switchBranchFields,
2121
tagFields,
2222
} from './descriptions';
23+
import { Container } from '@n8n/di';
24+
import { DeploymentConfig, SecurityConfig } from '@n8n/config';
2325

2426
export class Git implements INodeType {
2527
description: INodeTypeDescription = {
@@ -291,8 +293,18 @@ export class Git implements INodeType {
291293
}
292294
}
293295

296+
const gitConfig: string[] = [];
297+
const deploymentConfig = Container.get(DeploymentConfig);
298+
const isCloud = deploymentConfig.type === 'cloud';
299+
const securityConfig = Container.get(SecurityConfig);
300+
const disableBareRepos = securityConfig.disableBareRepos;
301+
if (isCloud || disableBareRepos) {
302+
gitConfig.push('safe.bareRepository=explicit');
303+
}
304+
294305
const gitOptions: Partial<SimpleGitOptions> = {
295306
baseDir: repositoryPath,
307+
config: gitConfig,
296308
};
297309

298310
const git: SimpleGit = simpleGit(gitOptions)
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { DeploymentConfig, SecurityConfig } from '@n8n/config';
2+
import { Container } from '@n8n/di';
3+
import { mock } from 'jest-mock-extended';
4+
import type { IExecuteFunctions } from 'n8n-workflow';
5+
import type { SimpleGit } from 'simple-git';
6+
import simpleGit from 'simple-git';
7+
8+
import { Git } from '../Git.node';
9+
10+
const mockGit = {
11+
log: jest.fn(),
12+
env: jest.fn().mockReturnThis(),
13+
};
14+
15+
jest.mock('simple-git');
16+
const mockSimpleGit = simpleGit as jest.MockedFunction<typeof simpleGit>;
17+
mockSimpleGit.mockReturnValue(mockGit as unknown as SimpleGit);
18+
19+
describe('Git Node', () => {
20+
let gitNode: Git;
21+
let executeFunctions: jest.Mocked<IExecuteFunctions>;
22+
let deploymentConfig: jest.Mocked<DeploymentConfig>;
23+
let securityConfig: jest.Mocked<SecurityConfig>;
24+
25+
beforeEach(() => {
26+
jest.clearAllMocks();
27+
28+
deploymentConfig = mock<DeploymentConfig>({
29+
type: 'default',
30+
});
31+
securityConfig = mock<SecurityConfig>({
32+
disableBareRepos: false,
33+
});
34+
Container.set(DeploymentConfig, deploymentConfig);
35+
Container.set(SecurityConfig, securityConfig);
36+
37+
executeFunctions = mock<IExecuteFunctions>({
38+
getInputData: jest.fn().mockReturnValue([{ json: {} }]),
39+
getNodeParameter: jest.fn(),
40+
helpers: {
41+
returnJsonArray: jest
42+
.fn()
43+
.mockImplementation((data: unknown[]) => data.map((item: unknown) => ({ json: item }))),
44+
},
45+
});
46+
executeFunctions.getNodeParameter.mockImplementation((name: string) => {
47+
switch (name) {
48+
case 'operation':
49+
return 'log';
50+
case 'repositoryPath':
51+
return '/tmp/test-repo';
52+
case 'options':
53+
return {};
54+
default:
55+
return '';
56+
}
57+
});
58+
59+
mockGit.log.mockResolvedValue({ all: [] });
60+
61+
gitNode = new Git();
62+
});
63+
64+
describe('Bare Repository Configuration', () => {
65+
it('should add safe.bareRepository=explicit when deployment type is cloud', async () => {
66+
deploymentConfig.type = 'cloud';
67+
securityConfig.disableBareRepos = false;
68+
69+
await gitNode.execute.call(executeFunctions);
70+
71+
expect(mockSimpleGit).toHaveBeenCalledWith(
72+
expect.objectContaining({
73+
config: ['safe.bareRepository=explicit'],
74+
}),
75+
);
76+
});
77+
78+
it('should add safe.bareRepository=explicit when disableBareRepos is true', async () => {
79+
deploymentConfig.type = 'default';
80+
securityConfig.disableBareRepos = true;
81+
82+
await gitNode.execute.call(executeFunctions);
83+
84+
expect(mockSimpleGit).toHaveBeenCalledWith(
85+
expect.objectContaining({
86+
config: ['safe.bareRepository=explicit'],
87+
}),
88+
);
89+
});
90+
91+
it('should add safe.bareRepository=explicit when both cloud and disableBareRepos are true', async () => {
92+
deploymentConfig.type = 'cloud';
93+
securityConfig.disableBareRepos = true;
94+
95+
await gitNode.execute.call(executeFunctions);
96+
97+
expect(mockSimpleGit).toHaveBeenCalledWith(
98+
expect.objectContaining({
99+
config: ['safe.bareRepository=explicit'],
100+
}),
101+
);
102+
});
103+
104+
it('should not add safe.bareRepository=explicit when neither cloud nor disableBareRepos is true', async () => {
105+
deploymentConfig.type = 'default';
106+
securityConfig.disableBareRepos = false;
107+
108+
await gitNode.execute.call(executeFunctions);
109+
110+
expect(mockSimpleGit).toHaveBeenCalledWith(
111+
expect.objectContaining({
112+
config: [],
113+
}),
114+
);
115+
});
116+
});
117+
});

0 commit comments

Comments
 (0)