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

This commit is contained in:
Dimitri Lavrenük
2025-12-19 11:07:29 +00:00
committed by GitHub
parent 538181cbe3
commit e22acaab3d
4 changed files with 75 additions and 0 deletions
@@ -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.
*/
+1
View File
@@ -322,6 +322,7 @@ describe('GlobalConfig', () => {
security: {
restrictFileAccessTo: '~/.n8n-files',
blockFileAccessToN8nFiles: true,
blockFilePatterns: '^(.*\\/)*\\.git(\\/.*)*$',
daysAbandonedWorkflow: 90,
contentSecurityPolicy: '{}',
contentSecurityPolicyReportOnly: false,
@@ -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', () => {
@@ -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));
}