n8n Vulnerable to Remote Code Execution via Git Node Pre-Commit Hook
Description
n8n is an open source workflow automation platform. Prior to 1.113.0, a remote code execution vulnerability exists in the Git Node component available in both Cloud and Self-Hosted versions of n8n. When a malicious actor clones a remote repository containing a pre-commit hook, the subsequent use of the Commit operation in the Git Node can inadvertently trigger the hook’s execution. This allows attackers to execute arbitrary code within the n8n environment, potentially compromising the system and any connected credentials or workflows. This vulnerability is fixed in 1.113.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
n8nnpm | < 1.113.0 | 1.113.0 |
Affected products
1Patches
15bf3db5ba84dfeat: Add N8N_GIT_NODE_DISABLE_BARE_REPOS environment variable to allow users to disable bare repositories in Git Node (#19559)
7 files changed · +180 −0
packages/cli/BREAKING-CHANGES.md+10 −0 modified@@ -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?
packages/cli/src/deprecation/deprecation.service.ts+6 −0 modified@@ -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. */
packages/cli/src/deprecation/__tests__/deprecation.service.test.ts+28 −0 modified@@ -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(); + }, + ); + }); });
packages/@n8n/config/src/configs/security.config.ts+6 −0 modified@@ -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; }
packages/@n8n/config/test/config.test.ts+1 −0 modified@@ -307,6 +307,7 @@ describe('GlobalConfig', () => { contentSecurityPolicy: '{}', contentSecurityPolicyReportOnly: false, disableWebhookHtmlSandboxing: false, + disableBareRepos: false, }, executions: { timeout: -1,
packages/nodes-base/nodes/Git/Git.node.ts+12 −0 modified@@ -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<SimpleGitOptions> = { baseDir: repositoryPath, + config: gitConfig, }; const git: SimpleGit = simpleGit(gitOptions)
packages/nodes-base/nodes/Git/__test__/Git.node.test.ts+117 −0 added@@ -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: [], + }), + ); + }); + }); +});
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-xgp7-7qjq-vg47ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-62726ghsaADVISORY
- github.com/n8n-io/n8n/commit/5bf3db5ba84d3195bbe11bbd3c62f7086e090997ghsax_refsource_MISCWEB
- github.com/n8n-io/n8n/pull/19559ghsax_refsource_MISCWEB
- github.com/n8n-io/n8n/security/advisories/GHSA-xgp7-7qjq-vg47ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.