diff --git a/packages/@n8n/config/src/configs/security.config.ts b/packages/@n8n/config/src/configs/security.config.ts index f1a1d8dbd2e..80668d25807 100644 --- a/packages/@n8n/config/src/configs/security.config.ts +++ b/packages/@n8n/config/src/configs/security.config.ts @@ -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. */ diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index a8bb6f0df05..fa07bd2708e 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -322,6 +322,7 @@ describe('GlobalConfig', () => { security: { restrictFileAccessTo: '~/.n8n-files', blockFileAccessToN8nFiles: true, + blockFilePatterns: '^(.*\\/)*\\.git(\\/.*)*$', daysAbandonedWorkflow: 90, contentSecurityPolicy: '{}', contentSecurityPolicyReportOnly: false, diff --git a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/file-system-helper-functions.test.ts b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/file-system-helper-functions.test.ts index 6e1385e1987..c7fcec61ee6 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/file-system-helper-functions.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/file-system-helper-functions.test.ts @@ -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', () => { diff --git a/packages/core/src/execution-engine/node-execution-context/utils/file-system-helper-functions.ts b/packages/core/src/execution-engine/node-execution-context/utils/file-system-helper-functions.ts index b4ad30bcdd7..5103a603705 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/file-system-helper-functions.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/file-system-helper-functions.ts @@ -47,6 +47,22 @@ async function resolvePath(path: PathLike): Promise { } } +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)); }