diff --git a/packages/@n8n/config/src/configs/security.config.ts b/packages/@n8n/config/src/configs/security.config.ts index 843d0605cd..2d0db0ff10 100644 --- a/packages/@n8n/config/src/configs/security.config.ts +++ b/packages/@n8n/config/src/configs/security.config.ts @@ -45,4 +45,10 @@ export class SecurityConfig { */ @Env('N8N_INSECURE_DISABLE_WEBHOOK_IFRAME_SANDBOX') disableWebhookHtmlSandboxing: boolean = false; + + /** + * Whether to disable bare repositories support in the Git node. + */ + @Env('N8N_GIT_NODE_DISABLE_BARE_REPOS') + disableBareRepos: boolean = false; } diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index 8eecbb6e42..e693e6d1ee 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -307,6 +307,7 @@ describe('GlobalConfig', () => { contentSecurityPolicy: '{}', contentSecurityPolicyReportOnly: false, disableWebhookHtmlSandboxing: false, + disableBareRepos: false, }, executions: { timeout: -1, diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 253c46fb07..eb72ee82c2 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,16 @@ 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 ### What changed? diff --git a/packages/cli/src/deprecation/__tests__/deprecation.service.test.ts b/packages/cli/src/deprecation/__tests__/deprecation.service.test.ts index b78e9293f7..4ceecf71cd 100644 --- a/packages/cli/src/deprecation/__tests__/deprecation.service.test.ts +++ b/packages/cli/src/deprecation/__tests__/deprecation.service.test.ts @@ -20,6 +20,7 @@ describe('DeprecationService', () => { // this test suite. process.env = { N8N_BLOCK_ENV_ACCESS_IN_NODE: 'false', + N8N_GIT_NODE_DISABLE_BARE_REPOS: 'false', }; jest.resetAllMocks(); @@ -140,6 +141,7 @@ describe('DeprecationService', () => { process.env = { N8N_RUNNERS_ENABLED: 'true', N8N_BLOCK_ENV_ACCESS_IN_NODE: 'false', + N8N_GIT_NODE_DISABLE_BARE_REPOS: 'false', }; jest.spyOn(config, 'getEnv').mockImplementation((key) => { @@ -239,6 +241,7 @@ describe('DeprecationService', () => { beforeEach(() => { process.env = { N8N_RUNNERS_ENABLED: 'true', + N8N_GIT_NODE_DISABLE_BARE_REPOS: 'false', }; 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(); + }, + ); + }); }); diff --git a/packages/cli/src/deprecation/deprecation.service.ts b/packages/cli/src/deprecation/deprecation.service.ts index e4b0532411..86429cb3bc 100644 --- a/packages/cli/src/deprecation/deprecation.service.ts +++ b/packages/cli/src/deprecation/deprecation.service.ts @@ -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/', 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. */ diff --git a/packages/nodes-base/nodes/Git/Git.node.ts b/packages/nodes-base/nodes/Git/Git.node.ts index 80a91abea7..be44c970d4 100644 --- a/packages/nodes-base/nodes/Git/Git.node.ts +++ b/packages/nodes-base/nodes/Git/Git.node.ts @@ -20,6 +20,8 @@ import { switchBranchFields, tagFields, } from './descriptions'; +import { Container } from '@n8n/di'; +import { DeploymentConfig, SecurityConfig } from '@n8n/config'; export class Git implements INodeType { 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 = { baseDir: repositoryPath, + config: gitConfig, }; const git: SimpleGit = simpleGit(gitOptions) diff --git a/packages/nodes-base/nodes/Git/__test__/Git.node.test.ts b/packages/nodes-base/nodes/Git/__test__/Git.node.test.ts new file mode 100644 index 0000000000..748cbc18a2 --- /dev/null +++ b/packages/nodes-base/nodes/Git/__test__/Git.node.test.ts @@ -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; +mockSimpleGit.mockReturnValue(mockGit as unknown as SimpleGit); + +describe('Git Node', () => { + let gitNode: Git; + let executeFunctions: jest.Mocked; + let deploymentConfig: jest.Mocked; + let securityConfig: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + + deploymentConfig = mock({ + type: 'default', + }); + securityConfig = mock({ + disableBareRepos: false, + }); + Container.set(DeploymentConfig, deploymentConfig); + Container.set(SecurityConfig, securityConfig); + + executeFunctions = mock({ + 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: [], + }), + ); + }); + }); +});