mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-21 12:51:16 +08:00
fix: Limit access to files based on regex pattern (#23413)
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -322,6 +322,7 @@ describe('GlobalConfig', () => {
|
||||
security: {
|
||||
restrictFileAccessTo: '~/.n8n-files',
|
||||
blockFileAccessToN8nFiles: true,
|
||||
blockFilePatterns: '^(.*\\/)*\\.git(\\/.*)*$',
|
||||
daysAbandonedWorkflow: 90,
|
||||
contentSecurityPolicy: '{}',
|
||||
contentSecurityPolicyReportOnly: false,
|
||||
|
||||
+46
@@ -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', () => {
|
||||
|
||||
+20
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user