VYPR
Critical severityNVD Advisory· Published Feb 25, 2026· Updated Feb 26, 2026

n8n has Arbitrary Command Execution via File Write and Git Operations

CVE-2026-27498

Description

n8n is an open source workflow automation platform. Prior to versions 2.2.0 and 1.123.8, an authenticated user with permission to create or modify workflows could chain the Read/Write Files from Disk node with git operations to achieve remote code execution. By writing to specific configuration files and then triggering a git operation, the attacker could execute arbitrary shell commands on the n8n host. The issue has been fixed in n8n versions 2.2.0 and 1.123.8. Users should upgrade to one of these versions or later to remediate the vulnerability. If upgrading is not immediately possible, administrators should consider the following temporary mitigations. Limit workflow creation and editing permissions to fully trusted users only, and/or disable the Read/Write Files from Disk node by adding n8n-nodes-base.readWriteFile to the NODES_EXCLUDE environment variable. These workarounds do not fully remediate the risk and should only be used as short-term mitigation measures.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
n8nnpm
< 1.123.81.123.8
n8nnpm
>= 2.0.0, < 2.2.02.2.0

Affected products

1

Patches

2
97365caf2539

fix: Limit access to files based on regex pattern (#23528)

https://github.com/n8n-io/n8nShireen MissiDec 22, 2025via ghsa
4 files changed · +80 0
  • packages/core/src/execution-engine/node-execution-context/utils/file-system-helper-functions.ts+21 0 modified
    @@ -1,4 +1,5 @@
     import { isContainedWithin, safeJoinPath } from '@n8n/backend-common';
    +import { SecurityConfig } from '@n8n/config';
     import { Container } from '@n8n/di';
     import { NodeOperationError } from 'n8n-workflow';
     import type { FileSystemHelperFunctions, INode, ResolvedFilePath } from 'n8n-workflow';
    @@ -45,6 +46,22 @@ async function resolvePath(path: PathLike): Promise<ResolvedFilePath> {
     	}
     }
     
    +function isFilePatternBlocked(resolvedFilePath: ResolvedFilePath): boolean {
    +	const { blockFilePatterns } = Container.get(SecurityConfig);
    +
    +	return blockFilePatterns
    +		.split(';')
    +		.map((pattern) => pattern.trim())
    +		.filter((pattern) => pattern)
    +		.some((pattern) => {
    +			try {
    +				return new RegExp(pattern, 'mi').test(resolvedFilePath);
    +			} catch {
    +				return true;
    +			}
    +		});
    +}
    +
     function isFilePathBlocked(resolvedFilePath: ResolvedFilePath): boolean {
     	const allowedPaths = getAllowedPaths();
     	const blockFileAccessToN8nFiles = process.env[BLOCK_FILE_ACCESS_TO_N8N_FILES] !== 'false';
    @@ -56,6 +73,10 @@ function isFilePathBlocked(resolvedFilePath: ResolvedFilePath): boolean {
     		return true;
     	}
     
    +	if (isFilePatternBlocked(resolvedFilePath)) {
    +		return true;
    +	}
    +
     	if (allowedPaths.length) {
     		return !allowedPaths.some((allowedPath) => isContainedWithin(allowedPath, resolvedFilePath));
     	}
    
  • packages/core/src/execution-engine/node-execution-context/utils/__tests__/file-system-helper-functions.test.ts+50 0 modified
    @@ -1,3 +1,4 @@
    +import { SecurityConfig } from '@n8n/config';
     import { Container } from '@n8n/di';
     import type { INode } from 'n8n-workflow';
     import { constants, createReadStream } from 'node:fs';
    @@ -23,6 +24,9 @@ jest.mock('node:fs/promises');
     const originalProcessEnv = { ...process.env };
     
     let instanceSettings: InstanceSettings;
    +let securityConfig: SecurityConfig;
    +let originalBlockedFilePatterns: string;
    +
     beforeEach(() => {
     	process.env = { ...originalProcessEnv };
     
    @@ -33,6 +37,13 @@ beforeEach(() => {
     	(fsRealpath as jest.Mock).mockImplementation((path: string) => path);
     
     	instanceSettings = Container.get(InstanceSettings);
    +	securityConfig = Container.get(SecurityConfig);
    +	securityConfig.restrictFileAccessTo = '';
    +	originalBlockedFilePatterns = securityConfig.blockFilePatterns;
    +});
    +
    +afterEach(() => {
    +	securityConfig.blockFilePatterns = originalBlockedFilePatterns;
     });
     
     describe('isFilePathBlocked', () => {
    @@ -182,6 +193,45 @@ describe('isFilePathBlocked', () => {
     		(fsRealpath as jest.Mock).mockRejectedValueOnce(error);
     		expect(isFilePathBlocked(await resolvePath(filePath))).toBe(true);
     	});
    +
    +	it.each(['.git', '/.git', '/tmp/.git', '/tmp/.git/config'])(
    +		'should per default block access to %s',
    +		async (path) => {
    +			expect(isFilePathBlocked(await resolvePath(path))).toBe(true);
    +		},
    +	);
    +
    +	it('should allow access when pattern matching is disabled', async () => {
    +		securityConfig.blockFilePatterns = '';
    +		expect(isFilePathBlocked(await resolvePath('/tmp/.git'))).toBe(false);
    +	});
    +
    +	it('should block all access when using invalid pattern', async () => {
    +		securityConfig.blockFilePatterns = '(';
    +		expect(isFilePathBlocked(await resolvePath('/tmp/xo'))).toBe(true);
    +	});
    +
    +	describe('when multiple file patterns are configured', () => {
    +		beforeEach(() => {
    +			securityConfig.blockFilePatterns = 'hello; \\/there$; ^where';
    +		});
    +
    +		it.each([
    +			'hello',
    +			'xhellox',
    +			'subpath/hello/',
    +			'/there',
    +			'/subpath/there',
    +			'where',
    +			'where-is/it',
    +		])('should block access to %s', async (path) => {
    +			expect(isFilePathBlocked(await resolvePath(path))).toBe(true);
    +		});
    +
    +		it.each(['/there/is', '/where'])('should not block access to %s', async (path) => {
    +			expect(isFilePathBlocked(await resolvePath(path))).toBe(false);
    +		});
    +	});
     });
     
     describe('getFileSystemHelperFunctions', () => {
    
  • packages/@n8n/config/src/configs/security.config.ts+8 0 modified
    @@ -19,6 +19,14 @@ export class SecurityConfig {
     	@Env('N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES')
     	blockFileAccessToN8nFiles: boolean = true;
     
    +	/**
    +	 * Blocked file and folder regular expression patterns that `ReadWriteFile` and `ReadBinaryFiles` nodes cant access. Separate multiple patterns with with semicolon `;`.
    +	 * - `^(.*\/)*\.git(\/.*)*$`
    +	 * Set to empty to not block based on file patterns.
    +	 */
    +	@Env('N8N_BLOCK_FILE_PATTERNS')
    +	blockFilePatterns: string = '^(.*\\/)*\\.git(\\/.*)*$';
    +
     	/**
     	 * In a [security audit](https://docs.n8n.io/hosting/securing/security-audit/), how many days for a workflow to be considered abandoned if not executed.
     	 */
    
  • packages/@n8n/config/test/config.test.ts+1 0 modified
    @@ -320,6 +320,7 @@ describe('GlobalConfig', () => {
     		security: {
     			restrictFileAccessTo: '',
     			blockFileAccessToN8nFiles: true,
    +			blockFilePatterns: '^(.*\\/)*\\.git(\\/.*)*$',
     			daysAbandonedWorkflow: 90,
     			contentSecurityPolicy: '{}',
     			contentSecurityPolicyReportOnly: false,
    
e22acaab3dcb

fix: Limit access to files based on regex pattern (#23413)

https://github.com/n8n-io/n8nDimitri LavrenükDec 19, 2025via ghsa
4 files changed · +75 0
  • packages/core/src/execution-engine/node-execution-context/utils/file-system-helper-functions.ts+20 0 modified
    @@ -47,6 +47,22 @@ async function resolvePath(path: PathLike): Promise<ResolvedFilePath> {
     	}
     }
     
    +function isFilePatternBlocked(resolvedFilePath: ResolvedFilePath): boolean {
    +	const { blockFilePatterns } = Container.get(SecurityConfig);
    +
    +	return blockFilePatterns
    +		.split(';')
    +		.map((pattern) => pattern.trim())
    +		.filter((pattern) => pattern)
    +		.some((pattern) => {
    +			try {
    +				return new RegExp(pattern, 'mi').test(resolvedFilePath);
    +			} catch {
    +				return true;
    +			}
    +		});
    +}
    +
     function isFilePathBlocked(resolvedFilePath: ResolvedFilePath): boolean {
     	const allowedPaths = getAllowedPaths();
     	const blockFileAccessToN8nFiles = process.env[BLOCK_FILE_ACCESS_TO_N8N_FILES] !== 'false';
    @@ -58,6 +74,10 @@ function isFilePathBlocked(resolvedFilePath: ResolvedFilePath): boolean {
     		return true;
     	}
     
    +	if (isFilePatternBlocked(resolvedFilePath)) {
    +		return true;
    +	}
    +
     	if (allowedPaths.length) {
     		return !allowedPaths.some((allowedPath) => isContainedWithin(allowedPath, resolvedFilePath));
     	}
    
  • packages/core/src/execution-engine/node-execution-context/utils/__tests__/file-system-helper-functions.test.ts+46 0 modified
    @@ -24,6 +24,8 @@ const originalProcessEnv = { ...process.env };
     
     let instanceSettings: InstanceSettings;
     let securityConfig: SecurityConfig;
    +let originalBlockedFilePatterns: string;
    +
     beforeEach(() => {
     	process.env = { ...originalProcessEnv };
     
    @@ -36,6 +38,11 @@ beforeEach(() => {
     	instanceSettings = Container.get(InstanceSettings);
     	securityConfig = Container.get(SecurityConfig);
     	securityConfig.restrictFileAccessTo = '';
    +	originalBlockedFilePatterns = securityConfig.blockFilePatterns;
    +});
    +
    +afterEach(() => {
    +	securityConfig.blockFilePatterns = originalBlockedFilePatterns;
     });
     
     describe('isFilePathBlocked', () => {
    @@ -185,6 +192,45 @@ describe('isFilePathBlocked', () => {
     		(fsRealpath as jest.Mock).mockRejectedValueOnce(error);
     		expect(isFilePathBlocked(await resolvePath(filePath))).toBe(true);
     	});
    +
    +	it.each(['.git', '/.git', '/tmp/.git', '/tmp/.git/config'])(
    +		'should per default block access to %s',
    +		async (path) => {
    +			expect(isFilePathBlocked(await resolvePath(path))).toBe(true);
    +		},
    +	);
    +
    +	it('should allow access when pattern matching is disabled', async () => {
    +		securityConfig.blockFilePatterns = '';
    +		expect(isFilePathBlocked(await resolvePath('/tmp/.git'))).toBe(false);
    +	});
    +
    +	it('should block all access when using invalid pattern', async () => {
    +		securityConfig.blockFilePatterns = '(';
    +		expect(isFilePathBlocked(await resolvePath('/tmp/xo'))).toBe(true);
    +	});
    +
    +	describe('when multiple file patterns are configured', () => {
    +		beforeEach(() => {
    +			securityConfig.blockFilePatterns = 'hello; \\/there$; ^where';
    +		});
    +
    +		it.each([
    +			'hello',
    +			'xhellox',
    +			'subpath/hello/',
    +			'/there',
    +			'/subpath/there',
    +			'where',
    +			'where-is/it',
    +		])('should block access to %s', async (path) => {
    +			expect(isFilePathBlocked(await resolvePath(path))).toBe(true);
    +		});
    +
    +		it.each(['/there/is', '/where'])('should not block access to %s', async (path) => {
    +			expect(isFilePathBlocked(await resolvePath(path))).toBe(false);
    +		});
    +	});
     });
     
     describe('getFileSystemHelperFunctions', () => {
    
  • packages/@n8n/config/src/configs/security.config.ts+8 0 modified
    @@ -20,6 +20,14 @@ export class SecurityConfig {
     	@Env('N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES')
     	blockFileAccessToN8nFiles: boolean = true;
     
    +	/**
    +	 * Blocked file and folder regular expression patterns that `ReadWriteFile` and `ReadBinaryFiles` nodes cant access. Separate multiple patterns with with semicolon `;`.
    +	 * - `^(.*\/)*\.git(\/.*)*$`
    +	 * Set to empty to not block based on file patterns.
    +	 */
    +	@Env('N8N_BLOCK_FILE_PATTERNS')
    +	blockFilePatterns: string = '^(.*\\/)*\\.git(\\/.*)*$';
    +
     	/**
     	 * In a [security audit](https://docs.n8n.io/hosting/securing/security-audit/), how many days for a workflow to be considered abandoned if not executed.
     	 */
    
  • packages/@n8n/config/test/config.test.ts+1 0 modified
    @@ -322,6 +322,7 @@ describe('GlobalConfig', () => {
     		security: {
     			restrictFileAccessTo: '~/.n8n-files',
     			blockFileAccessToN8nFiles: true,
    +			blockFilePatterns: '^(.*\\/)*\\.git(\\/.*)*$',
     			daysAbandonedWorkflow: 90,
     			contentSecurityPolicy: '{}',
     			contentSecurityPolicyReportOnly: false,
    

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

7

News mentions

0

No linked articles in our index yet.