feat: Add N8N_GIT_NODE_DISABLE_BARE_REPOS environment variable to allow users to disable bare repositories in Git Node (#19559)

This commit is contained in:
RomanDavydchuk
2025-09-18 18:33:32 +03:00
committed by GitHub
parent bcedf5c76f
commit 5bf3db5ba8
7 changed files with 180 additions and 0 deletions

View File

@@ -45,4 +45,10 @@ export class SecurityConfig {
*/ */
@Env('N8N_INSECURE_DISABLE_WEBHOOK_IFRAME_SANDBOX') @Env('N8N_INSECURE_DISABLE_WEBHOOK_IFRAME_SANDBOX')
disableWebhookHtmlSandboxing: boolean = false; disableWebhookHtmlSandboxing: boolean = false;
/**
* Whether to disable bare repositories support in the Git node.
*/
@Env('N8N_GIT_NODE_DISABLE_BARE_REPOS')
disableBareRepos: boolean = false;
} }

View File

@@ -307,6 +307,7 @@ describe('GlobalConfig', () => {
contentSecurityPolicy: '{}', contentSecurityPolicy: '{}',
contentSecurityPolicyReportOnly: false, contentSecurityPolicyReportOnly: false,
disableWebhookHtmlSandboxing: false, disableWebhookHtmlSandboxing: false,
disableBareRepos: false,
}, },
executions: { executions: {
timeout: -1, timeout: -1,

View File

@@ -2,6 +2,16 @@
This list shows all the versions which include breaking changes and how to upgrade. This list shows all the versions which include breaking changes and how to upgrade.
# 1.113.0
### What changed?
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.
### When is action necessary?
If you have workflows that use the Git Node and work with bare git repositories.
# 1.109.0 # 1.109.0
### What changed? ### What changed?

View File

@@ -20,6 +20,7 @@ describe('DeprecationService', () => {
// this test suite. // this test suite.
process.env = { process.env = {
N8N_BLOCK_ENV_ACCESS_IN_NODE: 'false', N8N_BLOCK_ENV_ACCESS_IN_NODE: 'false',
N8N_GIT_NODE_DISABLE_BARE_REPOS: 'false',
}; };
jest.resetAllMocks(); jest.resetAllMocks();
@@ -140,6 +141,7 @@ describe('DeprecationService', () => {
process.env = { process.env = {
N8N_RUNNERS_ENABLED: 'true', N8N_RUNNERS_ENABLED: 'true',
N8N_BLOCK_ENV_ACCESS_IN_NODE: 'false', N8N_BLOCK_ENV_ACCESS_IN_NODE: 'false',
N8N_GIT_NODE_DISABLE_BARE_REPOS: 'false',
}; };
jest.spyOn(config, 'getEnv').mockImplementation((key) => { jest.spyOn(config, 'getEnv').mockImplementation((key) => {
@@ -239,6 +241,7 @@ describe('DeprecationService', () => {
beforeEach(() => { beforeEach(() => {
process.env = { process.env = {
N8N_RUNNERS_ENABLED: 'true', N8N_RUNNERS_ENABLED: 'true',
N8N_GIT_NODE_DISABLE_BARE_REPOS: 'false',
}; };
jest.resetAllMocks(); jest.resetAllMocks();
@@ -259,4 +262,29 @@ describe('DeprecationService', () => {
}, },
); );
}); });
describe('N8N_GIT_NODE_DISABLE_BARE_REPOS', () => {
beforeEach(() => {
process.env = {
N8N_RUNNERS_ENABLED: 'true',
N8N_BLOCK_ENV_ACCESS_IN_NODE: 'false',
};
jest.resetAllMocks();
});
test('should warn when N8N_GIT_NODE_DISABLE_BARE_REPOS is not set', () => {
delete process.env.N8N_GIT_NODE_DISABLE_BARE_REPOS;
deprecationService.warn();
expect(logger.warn).toHaveBeenCalled();
});
test.each(['false', 'true'])(
'should not warn when N8N_GIT_NODE_DISABLE_BARE_REPOS is %s',
(value) => {
process.env.N8N_GIT_NODE_DISABLE_BARE_REPOS = value;
deprecationService.warn();
expect(logger.warn).not.toHaveBeenCalled();
},
);
});
}); });

View File

@@ -109,6 +109,12 @@ export class DeprecationService {
'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/', '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/',
checkValue: (value: string | undefined) => value === undefined || value === '', checkValue: (value: string | undefined) => value === undefined || value === '',
}, },
{
envVar: 'N8N_GIT_NODE_DISABLE_BARE_REPOS',
message:
'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/',
checkValue: (value: string | undefined) => value === undefined || value === '',
},
]; ];
/** Runtime state of deprecation-related env vars. */ /** Runtime state of deprecation-related env vars. */

View File

@@ -20,6 +20,8 @@ import {
switchBranchFields, switchBranchFields,
tagFields, tagFields,
} from './descriptions'; } from './descriptions';
import { Container } from '@n8n/di';
import { DeploymentConfig, SecurityConfig } from '@n8n/config';
export class Git implements INodeType { export class Git implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@@ -291,8 +293,18 @@ export class Git implements INodeType {
} }
} }
const gitConfig: string[] = [];
const deploymentConfig = Container.get(DeploymentConfig);
const isCloud = deploymentConfig.type === 'cloud';
const securityConfig = Container.get(SecurityConfig);
const disableBareRepos = securityConfig.disableBareRepos;
if (isCloud || disableBareRepos) {
gitConfig.push('safe.bareRepository=explicit');
}
const gitOptions: Partial<SimpleGitOptions> = { const gitOptions: Partial<SimpleGitOptions> = {
baseDir: repositoryPath, baseDir: repositoryPath,
config: gitConfig,
}; };
const git: SimpleGit = simpleGit(gitOptions) const git: SimpleGit = simpleGit(gitOptions)

View File

@@ -0,0 +1,117 @@
import { DeploymentConfig, SecurityConfig } from '@n8n/config';
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions } from 'n8n-workflow';
import type { SimpleGit } from 'simple-git';
import simpleGit from 'simple-git';
import { Git } from '../Git.node';
const mockGit = {
log: jest.fn(),
env: jest.fn().mockReturnThis(),
};
jest.mock('simple-git');
const mockSimpleGit = simpleGit as jest.MockedFunction<typeof simpleGit>;
mockSimpleGit.mockReturnValue(mockGit as unknown as SimpleGit);
describe('Git Node', () => {
let gitNode: Git;
let executeFunctions: jest.Mocked<IExecuteFunctions>;
let deploymentConfig: jest.Mocked<DeploymentConfig>;
let securityConfig: jest.Mocked<SecurityConfig>;
beforeEach(() => {
jest.clearAllMocks();
deploymentConfig = mock<DeploymentConfig>({
type: 'default',
});
securityConfig = mock<SecurityConfig>({
disableBareRepos: false,
});
Container.set(DeploymentConfig, deploymentConfig);
Container.set(SecurityConfig, securityConfig);
executeFunctions = mock<IExecuteFunctions>({
getInputData: jest.fn().mockReturnValue([{ json: {} }]),
getNodeParameter: jest.fn(),
helpers: {
returnJsonArray: jest
.fn()
.mockImplementation((data: unknown[]) => data.map((item: unknown) => ({ json: item }))),
},
});
executeFunctions.getNodeParameter.mockImplementation((name: string) => {
switch (name) {
case 'operation':
return 'log';
case 'repositoryPath':
return '/tmp/test-repo';
case 'options':
return {};
default:
return '';
}
});
mockGit.log.mockResolvedValue({ all: [] });
gitNode = new Git();
});
describe('Bare Repository Configuration', () => {
it('should add safe.bareRepository=explicit when deployment type is cloud', async () => {
deploymentConfig.type = 'cloud';
securityConfig.disableBareRepos = false;
await gitNode.execute.call(executeFunctions);
expect(mockSimpleGit).toHaveBeenCalledWith(
expect.objectContaining({
config: ['safe.bareRepository=explicit'],
}),
);
});
it('should add safe.bareRepository=explicit when disableBareRepos is true', async () => {
deploymentConfig.type = 'default';
securityConfig.disableBareRepos = true;
await gitNode.execute.call(executeFunctions);
expect(mockSimpleGit).toHaveBeenCalledWith(
expect.objectContaining({
config: ['safe.bareRepository=explicit'],
}),
);
});
it('should add safe.bareRepository=explicit when both cloud and disableBareRepos are true', async () => {
deploymentConfig.type = 'cloud';
securityConfig.disableBareRepos = true;
await gitNode.execute.call(executeFunctions);
expect(mockSimpleGit).toHaveBeenCalledWith(
expect.objectContaining({
config: ['safe.bareRepository=explicit'],
}),
);
});
it('should not add safe.bareRepository=explicit when neither cloud nor disableBareRepos is true', async () => {
deploymentConfig.type = 'default';
securityConfig.disableBareRepos = false;
await gitNode.execute.call(executeFunctions);
expect(mockSimpleGit).toHaveBeenCalledWith(
expect.objectContaining({
config: [],
}),
);
});
});
});