Skip to content

Commit 6e6ba2c

Browse files
committed
fix(core): Set default for allowed file access dirs
1 parent 83ea8e1 commit 6e6ba2c

File tree

4 files changed

+34
-28
lines changed

4 files changed

+34
-28
lines changed

packages/@n8n/config/src/configs/security.config.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@ import { Config, Env } from '../decorators';
33
@Config
44
export class SecurityConfig {
55
/**
6-
* Which directories to limit n8n's access to. Separate multiple dirs with semicolon `;`.
6+
* Dirs that the `ReadWriteFile` and `ReadBinaryFiles` nodes are allowed to access. Separate multiple dirs with semicolon `;`.
7+
* Set to an empty string to disable restrictions (insecure, not recommended for production).
78
*
8-
* @example N8N_RESTRICT_FILE_ACCESS_TO=/home/user/.n8n;/home/user/n8n-data
9+
* @example N8N_RESTRICT_FILE_ACCESS_TO=/home/john/my-n8n-files
910
*/
1011
@Env('N8N_RESTRICT_FILE_ACCESS_TO')
11-
restrictFileAccessTo: string = '';
12+
restrictFileAccessTo: string = '~/.n8n-files';
1213

1314
/**
14-
* Whether to block access to all files at:
15-
* - the ".n8n" directory,
16-
* - the static cache dir at ~/.cache/n8n/public, and
17-
* - user-defined config files.
15+
* Whether to block nodes from accessing files at dirs internally used by n8n:
16+
* - `~/.n8n`
17+
* - `~/.cache/n8n/public`
18+
* - any dirs specified by `N8N_CONFIG_FILES`, `N8N_CUSTOM_EXTENSIONS`, `N8N_BINARY_DATA_STORAGE_PATH`, `N8N_UM_EMAIL_TEMPLATES_INVITE`, and `UM_EMAIL_TEMPLATES_PWRESET`.
1819
*/
1920
@Env('N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES')
2021
blockFileAccessToN8nFiles: boolean = true;

packages/@n8n/config/test/config.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ describe('GlobalConfig', () => {
317317
cert: '',
318318
},
319319
security: {
320-
restrictFileAccessTo: '',
320+
restrictFileAccessTo: '~/.n8n-files',
321321
blockFileAccessToN8nFiles: true,
322322
daysAbandonedWorkflow: 90,
323323
contentSecurityPolicy: '{}',

packages/core/src/execution-engine/node-execution-context/utils/__tests__/file-system-helper-functions.test.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SecurityConfig } from '@n8n/config';
12
import { Container } from '@n8n/di';
23
import type { INode } from 'n8n-workflow';
34
import { createReadStream } from 'node:fs';
@@ -9,7 +10,6 @@ import {
910
BLOCK_FILE_ACCESS_TO_N8N_FILES,
1011
CONFIG_FILES,
1112
CUSTOM_EXTENSION_ENV,
12-
RESTRICT_FILE_ACCESS_TO,
1313
UM_EMAIL_TEMPLATES_INVITE,
1414
UM_EMAIL_TEMPLATES_PWRESET,
1515
} from '@/constants';
@@ -23,6 +23,7 @@ jest.mock('node:fs/promises');
2323
const originalProcessEnv = { ...process.env };
2424

2525
let instanceSettings: InstanceSettings;
26+
let securityConfig: SecurityConfig;
2627
beforeEach(() => {
2728
process.env = { ...originalProcessEnv };
2829

@@ -33,6 +34,8 @@ beforeEach(() => {
3334
(fsRealpath as jest.Mock).mockImplementation((path: string) => path);
3435

3536
instanceSettings = Container.get(InstanceSettings);
37+
securityConfig = Container.get(SecurityConfig);
38+
securityConfig.restrictFileAccessTo = '';
3639
});
3740

3841
describe('isFilePathBlocked', () => {
@@ -51,25 +54,25 @@ describe('isFilePathBlocked', () => {
5154
});
5255

5356
it('should handle empty allowed paths', async () => {
54-
delete process.env[RESTRICT_FILE_ACCESS_TO];
57+
securityConfig.restrictFileAccessTo = '';
5558
const result = await isFilePathBlocked('/some/random/path');
5659
expect(result).toBe(false);
5760
});
5861

5962
it('should handle multiple allowed paths', async () => {
60-
process.env[RESTRICT_FILE_ACCESS_TO] = '/path1;/path2;/path3';
63+
securityConfig.restrictFileAccessTo = '/path1;/path2;/path3';
6164
const allowedPath = '/path2/somefile';
6265
expect(await isFilePathBlocked(allowedPath)).toBe(false);
6366
});
6467

6568
it('should handle empty strings in allowed paths', async () => {
66-
process.env[RESTRICT_FILE_ACCESS_TO] = '/path1;;/path2';
69+
securityConfig.restrictFileAccessTo = '/path1;;/path2';
6770
const allowedPath = '/path2/somefile';
6871
expect(await isFilePathBlocked(allowedPath)).toBe(false);
6972
});
7073

7174
it('should trim whitespace in allowed paths', async () => {
72-
process.env[RESTRICT_FILE_ACCESS_TO] = ' /path1 ; /path2 ; /path3 ';
75+
securityConfig.restrictFileAccessTo = ' /path1 ; /path2 ; /path3 ';
7376
const allowedPath = '/path2/somefile';
7477
expect(await isFilePathBlocked(allowedPath)).toBe(false);
7578
});
@@ -81,14 +84,14 @@ describe('isFilePathBlocked', () => {
8184
});
8285

8386
it('should return true when path is in allowed paths but still restricted', async () => {
84-
process.env[RESTRICT_FILE_ACCESS_TO] = '/some/allowed/path';
87+
securityConfig.restrictFileAccessTo = '/some/allowed/path';
8588
const restrictedPath = instanceSettings.n8nFolder;
8689
expect(await isFilePathBlocked(restrictedPath)).toBe(true);
8790
});
8891

8992
it('should return false when path is in allowed paths', async () => {
9093
const allowedPath = '/some/allowed/path';
91-
process.env[RESTRICT_FILE_ACCESS_TO] = allowedPath;
94+
securityConfig.restrictFileAccessTo = allowedPath;
9295
expect(await isFilePathBlocked(allowedPath)).toBe(false);
9396
});
9497

@@ -125,7 +128,7 @@ describe('isFilePathBlocked', () => {
125128
const homeVarName = process.platform === 'win32' ? 'USERPROFILE' : 'HOME';
126129
const userHome = process.env.N8N_USER_FOLDER ?? process.env[homeVarName] ?? process.cwd();
127130

128-
process.env[RESTRICT_FILE_ACCESS_TO] = userHome;
131+
securityConfig.restrictFileAccessTo = userHome;
129132
process.env[BLOCK_FILE_ACCESS_TO_N8N_FILES] = 'true';
130133
const restrictedPath = instanceSettings.n8nFolder;
131134
expect(await isFilePathBlocked(restrictedPath)).toBe(true);
@@ -135,7 +138,7 @@ describe('isFilePathBlocked', () => {
135138
const homeVarName = process.platform === 'win32' ? 'USERPROFILE' : 'HOME';
136139
const userHome = process.env.N8N_USER_FOLDER ?? process.env[homeVarName] ?? process.cwd();
137140

138-
process.env[RESTRICT_FILE_ACCESS_TO] = userHome;
141+
securityConfig.restrictFileAccessTo = userHome;
139142
process.env[BLOCK_FILE_ACCESS_TO_N8N_FILES] = 'true';
140143
const restrictedPath = join(userHome, 'somefile.txt');
141144
expect(await isFilePathBlocked(restrictedPath)).toBe(false);
@@ -145,14 +148,14 @@ describe('isFilePathBlocked', () => {
145148
const homeVarName = process.platform === 'win32' ? 'USERPROFILE' : 'HOME';
146149
const userHome = process.env.N8N_USER_FOLDER ?? process.env[homeVarName] ?? process.cwd();
147150

148-
process.env[RESTRICT_FILE_ACCESS_TO] = userHome;
151+
securityConfig.restrictFileAccessTo = userHome;
149152
process.env[BLOCK_FILE_ACCESS_TO_N8N_FILES] = 'true';
150153
const restrictedPath = join(userHome, '.n8n_x');
151154
expect(await isFilePathBlocked(restrictedPath)).toBe(false);
152155
});
153156

154157
it('should return true for a symlink in a allowed path to a restricted path', async () => {
155-
process.env[RESTRICT_FILE_ACCESS_TO] = '/path1';
158+
securityConfig.restrictFileAccessTo = '/path1';
156159
const allowedPath = '/path1/symlink';
157160
const actualPath = '/path2/realfile';
158161
(fsRealpath as jest.Mock).mockImplementation((path: string) =>
@@ -173,7 +176,7 @@ describe('isFilePathBlocked', () => {
173176
it('should handle non-existent file when it is not allowed', async () => {
174177
const filePath = '/non/existent/file';
175178
const allowedPath = '/some/allowed/path';
176-
process.env[RESTRICT_FILE_ACCESS_TO] = allowedPath;
179+
securityConfig.restrictFileAccessTo = allowedPath;
177180
const error = new Error('ENOENT');
178181
// @ts-expect-error undefined property
179182
error.code = 'ENOENT';
@@ -216,15 +219,15 @@ describe('getFileSystemHelperFunctions', () => {
216219
});
217220

218221
it('should throw when file access is blocked', async () => {
219-
process.env[RESTRICT_FILE_ACCESS_TO] = '/allowed/path';
222+
securityConfig.restrictFileAccessTo = '/allowed/path';
220223
(fsAccess as jest.Mock).mockResolvedValueOnce({});
221224
await expect(helperFunctions.createReadStream('/blocked/path')).rejects.toThrow(
222225
'Access to the file is not allowed',
223226
);
224227
});
225228

226229
it('should not reveal if file exists if it is within restricted path', async () => {
227-
process.env[RESTRICT_FILE_ACCESS_TO] = '/allowed/path';
230+
securityConfig.restrictFileAccessTo = '/allowed/path';
228231

229232
const error = new Error('ENOENT');
230233
// @ts-expect-error undefined property

packages/core/src/execution-engine/node-execution-context/utils/file-system-helper-functions.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isContainedWithin, safeJoinPath } from '@n8n/backend-common';
2+
import { SecurityConfig } from '@n8n/config';
23
import { Container } from '@n8n/di';
34
import type { FileSystemHelperFunctions, INode } from 'n8n-workflow';
45
import { NodeOperationError } from 'n8n-workflow';
@@ -8,28 +9,29 @@ import {
89
writeFile as fsWriteFile,
910
realpath as fsRealpath,
1011
} from 'node:fs/promises';
12+
import { homedir } from 'node:os';
1113
import { resolve } from 'node:path';
1214

1315
import {
1416
BINARY_DATA_STORAGE_PATH,
1517
BLOCK_FILE_ACCESS_TO_N8N_FILES,
1618
CONFIG_FILES,
1719
CUSTOM_EXTENSION_ENV,
18-
RESTRICT_FILE_ACCESS_TO,
1920
UM_EMAIL_TEMPLATES_INVITE,
2021
UM_EMAIL_TEMPLATES_PWRESET,
2122
} from '@/constants';
2223
import { InstanceSettings } from '@/instance-settings';
2324

2425
const getAllowedPaths = () => {
25-
const restrictFileAccessTo = process.env[RESTRICT_FILE_ACCESS_TO];
26-
if (!restrictFileAccessTo) {
27-
return [];
28-
}
26+
const { restrictFileAccessTo } = Container.get(SecurityConfig);
27+
if (restrictFileAccessTo === '') return [];
28+
2929
const allowedPaths = restrictFileAccessTo
3030
.split(';')
3131
.map((path) => path.trim())
32-
.filter((path) => path);
32+
.filter((path) => path)
33+
.map((path) => (path.startsWith('~') ? path.replace('~', homedir()) : path));
34+
3335
return allowedPaths;
3436
};
3537

0 commit comments

Comments
 (0)