VYPR
Critical severityNVD Advisory· Published Feb 4, 2026· Updated Feb 5, 2026

n8n Has an Expression Escape Vulnerability Leading to RCE

CVE-2026-25049

Description

n8n is an open source workflow automation platform. Prior to versions 1.123.17 and 2.5.2, an authenticated user with permission to create or modify workflows could abuse crafted expressions in workflow parameters to trigger unintended system command execution on the host running n8n. This issue has been patched in versions 1.123.17 and 2.5.2.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
n8nnpm
< 1.123.171.123.17
n8nnpm
>= 2.0.0, < 2.5.22.5.2

Affected products

1

Patches

2
7860896909b3

fix(Git Node): Clean up URLs returned from config (#24754)

https://github.com/n8n-io/n8nRomanDavydchukJan 23, 2026via ghsa
3 files changed · +179 7
  • packages/nodes-base/nodes/Git/GenericFunctions.ts+43 0 added
    @@ -0,0 +1,43 @@
    +import type { ConfigListSummary } from 'simple-git';
    +
    +const REMOTE_ORIGIN_URL_KEY = 'remote.origin.url';
    +
    +const REMOTE_ORIGIN_PUSH_URL_KEY = 'remote.origin.pushurl';
    +
    +function sanitizeUrl(url: string): string {
    +	const urlObj = new URL(url);
    +	urlObj.username = '';
    +	urlObj.password = '';
    +	return urlObj.toString();
    +}
    +
    +export function mapGitConfigList(config: ConfigListSummary) {
    +	const data = [];
    +	for (const fileName of Object.keys(config.values)) {
    +		let remoteOriginUrl = config.values[fileName][REMOTE_ORIGIN_URL_KEY];
    +		if (remoteOriginUrl) {
    +			if (Array.isArray(remoteOriginUrl)) {
    +				remoteOriginUrl = remoteOriginUrl.map(sanitizeUrl);
    +			} else {
    +				remoteOriginUrl = sanitizeUrl(remoteOriginUrl);
    +			}
    +		}
    +
    +		let remoteOriginPushUrl = config.values[fileName][REMOTE_ORIGIN_PUSH_URL_KEY];
    +		if (remoteOriginPushUrl) {
    +			if (Array.isArray(remoteOriginPushUrl)) {
    +				remoteOriginPushUrl = remoteOriginPushUrl.map(sanitizeUrl);
    +			} else {
    +				remoteOriginPushUrl = sanitizeUrl(remoteOriginPushUrl);
    +			}
    +		}
    +
    +		data.push({
    +			_file: fileName,
    +			...config.values[fileName],
    +			[REMOTE_ORIGIN_URL_KEY]: remoteOriginUrl,
    +			[REMOTE_ORIGIN_PUSH_URL_KEY]: remoteOriginPushUrl,
    +		});
    +	}
    +	return data;
    +}
    
  • packages/nodes-base/nodes/Git/Git.node.ts+2 7 modified
    @@ -28,6 +28,7 @@ import {
     } from './descriptions';
     import { Container } from '@n8n/di';
     import { DeploymentConfig, SecurityConfig } from '@n8n/config';
    +import { mapGitConfigList } from './GenericFunctions';
     
     export class Git implements INodeType {
     	description: INodeTypeDescription = {
    @@ -563,13 +564,7 @@ export class Git implements INodeType {
     
     					const config = await git.listConfig();
     
    -					const data = [];
    -					for (const fileName of Object.keys(config.values)) {
    -						data.push({
    -							_file: fileName,
    -							...config.values[fileName],
    -						});
    -					}
    +					const data = mapGitConfigList(config);
     
     					returnItems.push(
     						...this.helpers.returnJsonArray(data).map((item) => {
    
  • packages/nodes-base/nodes/Git/__test__/GenericFunctions.test.ts+134 0 added
    @@ -0,0 +1,134 @@
    +import { mockDeep } from 'jest-mock-extended';
    +import type { ConfigListSummary } from 'simple-git';
    +
    +import { mapGitConfigList } from '../GenericFunctions';
    +
    +describe('GenericFunctions', () => {
    +	describe('mapGitConfigList', () => {
    +		it('should map the git config list', () => {
    +			const config = mockDeep<ConfigListSummary>({
    +				values: {
    +					'.git/config': {
    +						'user.name': 'test',
    +						'core.autocrlf': 'true',
    +						'remote.origin.url': undefined,
    +						'remote.origin.pushurl': undefined,
    +					},
    +					'/other/config': {
    +						'user.name': 'other',
    +						'core.autocrlf': 'false',
    +						'remote.origin.url': undefined,
    +						'remote.origin.pushurl': undefined,
    +					},
    +				},
    +			});
    +
    +			const result = mapGitConfigList(config);
    +
    +			expect(result).toEqual([
    +				{
    +					_file: '.git/config',
    +					'user.name': 'test',
    +					'core.autocrlf': 'true',
    +				},
    +				{
    +					_file: '/other/config',
    +					'user.name': 'other',
    +					'core.autocrlf': 'false',
    +				},
    +			]);
    +		});
    +
    +		it('should sanitize the remote origin url', () => {
    +			const config = mockDeep<ConfigListSummary>({
    +				values: {
    +					'.git/config': {
    +						'remote.origin.url': 'https://user:password@github.com/test/test.git',
    +						'remote.origin.pushurl': undefined,
    +					},
    +				},
    +			});
    +
    +			const result = mapGitConfigList(config);
    +
    +			expect(result).toEqual([
    +				{
    +					_file: '.git/config',
    +					'remote.origin.url': 'https://github.com/test/test.git',
    +				},
    +			]);
    +		});
    +
    +		it('should sanitize the remote origin urls', () => {
    +			const config = mockDeep<ConfigListSummary>({
    +				values: {
    +					'.git/config': {
    +						'remote.origin.url': [
    +							'https://user:password@github.com/test/test.git',
    +							'https://user:password@github.com/test/other.git',
    +						],
    +						'remote.origin.pushurl': undefined,
    +					},
    +				},
    +			});
    +
    +			const result = mapGitConfigList(config);
    +
    +			expect(result).toEqual([
    +				{
    +					_file: '.git/config',
    +					'remote.origin.url': [
    +						'https://github.com/test/test.git',
    +						'https://github.com/test/other.git',
    +					],
    +				},
    +			]);
    +		});
    +
    +		it('should sanitize the remote origin push url', () => {
    +			const config = mockDeep<ConfigListSummary>({
    +				values: {
    +					'.git/config': {
    +						'remote.origin.pushurl': 'https://user:password@github.com/test/test.git',
    +						'remote.origin.url': undefined,
    +					},
    +				},
    +			});
    +
    +			const result = mapGitConfigList(config);
    +
    +			expect(result).toEqual([
    +				{
    +					_file: '.git/config',
    +					'remote.origin.pushurl': 'https://github.com/test/test.git',
    +				},
    +			]);
    +		});
    +
    +		it('should sanitize the remote origin push urls', () => {
    +			const config = mockDeep<ConfigListSummary>({
    +				values: {
    +					'.git/config': {
    +						'remote.origin.pushurl': [
    +							'https://user:password@github.com/test/test.git',
    +							'https://user:password@github.com/test/other.git',
    +						],
    +						'remote.origin.url': undefined,
    +					},
    +				},
    +			});
    +
    +			const result = mapGitConfigList(config);
    +
    +			expect(result).toEqual([
    +				{
    +					_file: '.git/config',
    +					'remote.origin.pushurl': [
    +						'https://github.com/test/test.git',
    +						'https://github.com/test/other.git',
    +					],
    +				},
    +			]);
    +		});
    +	});
    +});
    
936c06cfc1ad

fix(Git Node): Clean up URLs returned from config (#24713)

https://github.com/n8n-io/n8nRomanDavydchukJan 23, 2026via ghsa
3 files changed · +179 8
  • packages/nodes-base/nodes/Git/GenericFunctions.ts+43 0 modified
    @@ -1,5 +1,6 @@
     import type { INode } from 'n8n-workflow';
     import { NodeOperationError } from 'n8n-workflow';
    +import type { ConfigListSummary } from 'simple-git';
     
     /**
      * Validates a git reference to prevent command injection attacks
    @@ -40,3 +41,45 @@ export function validateGitReference(reference: string, node: INode): void {
     		);
     	}
     }
    +
    +const REMOTE_ORIGIN_URL_KEY = 'remote.origin.url';
    +
    +const REMOTE_ORIGIN_PUSH_URL_KEY = 'remote.origin.pushurl';
    +
    +function sanitizeUrl(url: string): string {
    +	const urlObj = new URL(url);
    +	urlObj.username = '';
    +	urlObj.password = '';
    +	return urlObj.toString();
    +}
    +
    +export function mapGitConfigList(config: ConfigListSummary) {
    +	const data = [];
    +	for (const fileName of Object.keys(config.values)) {
    +		let remoteOriginUrl = config.values[fileName][REMOTE_ORIGIN_URL_KEY];
    +		if (remoteOriginUrl) {
    +			if (Array.isArray(remoteOriginUrl)) {
    +				remoteOriginUrl = remoteOriginUrl.map(sanitizeUrl);
    +			} else {
    +				remoteOriginUrl = sanitizeUrl(remoteOriginUrl);
    +			}
    +		}
    +
    +		let remoteOriginPushUrl = config.values[fileName][REMOTE_ORIGIN_PUSH_URL_KEY];
    +		if (remoteOriginPushUrl) {
    +			if (Array.isArray(remoteOriginPushUrl)) {
    +				remoteOriginPushUrl = remoteOriginPushUrl.map(sanitizeUrl);
    +			} else {
    +				remoteOriginPushUrl = sanitizeUrl(remoteOriginPushUrl);
    +			}
    +		}
    +
    +		data.push({
    +			_file: fileName,
    +			...config.values[fileName],
    +			[REMOTE_ORIGIN_URL_KEY]: remoteOriginUrl,
    +			[REMOTE_ORIGIN_PUSH_URL_KEY]: remoteOriginPushUrl,
    +		});
    +	}
    +	return data;
    +}
    
  • packages/nodes-base/nodes/Git/Git.node.ts+2 8 modified
    @@ -29,7 +29,7 @@ import {
     	switchBranchFields,
     	tagFields,
     } from './descriptions';
    -import { validateGitReference } from './GenericFunctions';
    +import { mapGitConfigList, validateGitReference } from './GenericFunctions';
     
     export class Git implements INodeType {
     	description: INodeTypeDescription = {
    @@ -623,13 +623,7 @@ export class Git implements INodeType {
     
     					const config = await git.listConfig();
     
    -					const data = [];
    -					for (const fileName of Object.keys(config.values)) {
    -						data.push({
    -							_file: fileName,
    -							...config.values[fileName],
    -						});
    -					}
    +					const data = mapGitConfigList(config);
     
     					returnItems.push(
     						...this.helpers.returnJsonArray(data).map((item) => {
    
  • packages/nodes-base/nodes/Git/__test__/GenericFunctions.test.ts+134 0 added
    @@ -0,0 +1,134 @@
    +import { mockDeep } from 'jest-mock-extended';
    +import type { ConfigListSummary } from 'simple-git';
    +
    +import { mapGitConfigList } from '../GenericFunctions';
    +
    +describe('GenericFunctions', () => {
    +	describe('mapGitConfigList', () => {
    +		it('should map the git config list', () => {
    +			const config = mockDeep<ConfigListSummary>({
    +				values: {
    +					'.git/config': {
    +						'user.name': 'test',
    +						'core.autocrlf': 'true',
    +						'remote.origin.url': undefined,
    +						'remote.origin.pushurl': undefined,
    +					},
    +					'/other/config': {
    +						'user.name': 'other',
    +						'core.autocrlf': 'false',
    +						'remote.origin.url': undefined,
    +						'remote.origin.pushurl': undefined,
    +					},
    +				},
    +			});
    +
    +			const result = mapGitConfigList(config);
    +
    +			expect(result).toEqual([
    +				{
    +					_file: '.git/config',
    +					'user.name': 'test',
    +					'core.autocrlf': 'true',
    +				},
    +				{
    +					_file: '/other/config',
    +					'user.name': 'other',
    +					'core.autocrlf': 'false',
    +				},
    +			]);
    +		});
    +
    +		it('should sanitize the remote origin url', () => {
    +			const config = mockDeep<ConfigListSummary>({
    +				values: {
    +					'.git/config': {
    +						'remote.origin.url': 'https://user:password@github.com/test/test.git',
    +						'remote.origin.pushurl': undefined,
    +					},
    +				},
    +			});
    +
    +			const result = mapGitConfigList(config);
    +
    +			expect(result).toEqual([
    +				{
    +					_file: '.git/config',
    +					'remote.origin.url': 'https://github.com/test/test.git',
    +				},
    +			]);
    +		});
    +
    +		it('should sanitize the remote origin urls', () => {
    +			const config = mockDeep<ConfigListSummary>({
    +				values: {
    +					'.git/config': {
    +						'remote.origin.url': [
    +							'https://user:password@github.com/test/test.git',
    +							'https://user:password@github.com/test/other.git',
    +						],
    +						'remote.origin.pushurl': undefined,
    +					},
    +				},
    +			});
    +
    +			const result = mapGitConfigList(config);
    +
    +			expect(result).toEqual([
    +				{
    +					_file: '.git/config',
    +					'remote.origin.url': [
    +						'https://github.com/test/test.git',
    +						'https://github.com/test/other.git',
    +					],
    +				},
    +			]);
    +		});
    +
    +		it('should sanitize the remote origin push url', () => {
    +			const config = mockDeep<ConfigListSummary>({
    +				values: {
    +					'.git/config': {
    +						'remote.origin.pushurl': 'https://user:password@github.com/test/test.git',
    +						'remote.origin.url': undefined,
    +					},
    +				},
    +			});
    +
    +			const result = mapGitConfigList(config);
    +
    +			expect(result).toEqual([
    +				{
    +					_file: '.git/config',
    +					'remote.origin.pushurl': 'https://github.com/test/test.git',
    +				},
    +			]);
    +		});
    +
    +		it('should sanitize the remote origin push urls', () => {
    +			const config = mockDeep<ConfigListSummary>({
    +				values: {
    +					'.git/config': {
    +						'remote.origin.pushurl': [
    +							'https://user:password@github.com/test/test.git',
    +							'https://user:password@github.com/test/other.git',
    +						],
    +						'remote.origin.url': undefined,
    +					},
    +				},
    +			});
    +
    +			const result = mapGitConfigList(config);
    +
    +			expect(result).toEqual([
    +				{
    +					_file: '.git/config',
    +					'remote.origin.pushurl': [
    +						'https://github.com/test/test.git',
    +						'https://github.com/test/other.git',
    +					],
    +				},
    +			]);
    +		});
    +	});
    +});
    

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

News mentions

0

No linked articles in our index yet.