mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 13:39:44 +02:00
chore(core): Deprecate internal task runner mode and warn about SSRF default block list changes (no-changelog) (backport to release-candidate/2.38.x) (#38141)
Co-authored-by: Mike Repeć <mike.repec@n8n.io> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
3d2f334c24
commit
16199c6e56
|
|
@ -76,6 +76,13 @@ describe('DeprecationService', () => {
|
|||
['N8N_OUTBOUND_PROXY_MODE', 'main-only', true],
|
||||
['N8N_OUTBOUND_PROXY_MODE', 'all', false],
|
||||
['N8N_OUTBOUND_PROXY_MODE', undefined, false],
|
||||
['N8N_RUNNERS_MODE', 'internal', true],
|
||||
['N8N_RUNNERS_MODE', 'external', false],
|
||||
['N8N_RUNNERS_MODE', undefined, false],
|
||||
['N8N_SSRF_PROTECTION_ENABLED', 'true', true],
|
||||
['N8N_SSRF_PROTECTION_ENABLED', '1', true],
|
||||
['N8N_SSRF_PROTECTION_ENABLED', 'false', false],
|
||||
['N8N_SSRF_PROTECTION_ENABLED', undefined, false],
|
||||
])('should detect when %s is `%s`', (envVar, value, mustWarn) => {
|
||||
toTest(envVar, value, mustWarn);
|
||||
});
|
||||
|
|
@ -199,6 +206,28 @@ describe('DeprecationService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('N8N_SSRF_PROTECTION_ENABLED', () => {
|
||||
beforeEach(() => {
|
||||
process.env.N8N_SSRF_PROTECTION_ENABLED = 'true';
|
||||
});
|
||||
|
||||
test.each([undefined, 'default', 'DEFAULT , 100.64.0.0/10'])(
|
||||
'should warn when N8N_SSRF_BLOCKED_IP_RANGES is `%s`',
|
||||
(ranges) => {
|
||||
if (ranges === undefined) delete process.env.N8N_SSRF_BLOCKED_IP_RANGES;
|
||||
else process.env.N8N_SSRF_BLOCKED_IP_RANGES = ranges;
|
||||
deprecationService.warn();
|
||||
expect(logger.warn.mock.lastCall?.[0] ?? '').toContain('N8N_SSRF_PROTECTION_ENABLED');
|
||||
},
|
||||
);
|
||||
|
||||
test('should not warn when N8N_SSRF_BLOCKED_IP_RANGES lists literal ranges only', () => {
|
||||
process.env.N8N_SSRF_BLOCKED_IP_RANGES = '10.0.0.0/8,192.168.0.0/16';
|
||||
deprecationService.warn();
|
||||
expect(logger.warn.mock.lastCall?.[0] ?? '').not.toContain('N8N_SSRF_PROTECTION_ENABLED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('running outside a container', () => {
|
||||
const message = 'Running n8n outside a container is deprecated';
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,29 @@ export class DeprecationService {
|
|||
'The default for this variable will change to `false` in a future version. Set it to `true` explicitly to keep installing unverified community packages.',
|
||||
checkValue: (value?: string) => value === undefined,
|
||||
},
|
||||
{
|
||||
envVar: 'N8N_RUNNERS_MODE',
|
||||
message:
|
||||
'The `internal` mode is deprecated and will be removed in a future version. Run task runners as a separate process and set this variable to `external`.',
|
||||
checkValue: (value?: string) => value === 'internal',
|
||||
},
|
||||
{
|
||||
envVar: 'N8N_SSRF_PROTECTION_ENABLED',
|
||||
message:
|
||||
"The built-in blocked IP ranges will expand in a future version to include the shared address space (100.64.0.0/10) and IPv6 transition ranges. To keep the current list, set N8N_SSRF_BLOCKED_IP_RANGES to the literal ranges instead of the `default` keyword, which always expands to the running version's built-in list.",
|
||||
checkValue: (value?: string) => ['true', '1'].includes(value?.toLowerCase() ?? ''),
|
||||
// Literal block lists without the `default` keyword do not pick up the expanded built-in list.
|
||||
disableIf: () => {
|
||||
const ranges = process.env.N8N_SSRF_BLOCKED_IP_RANGES;
|
||||
return (
|
||||
ranges !== undefined &&
|
||||
!ranges
|
||||
.toLowerCase()
|
||||
.split(',')
|
||||
.some((r) => r.trim() === 'default')
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
envVar: 'N8N_RUNNERS_TASK_TIMEOUT',
|
||||
message:
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ import './v3/in-memory-binary-data.rule';
|
|||
import './v3/offload-manual-executions.rule';
|
||||
import './v3/removed-nodes.rule';
|
||||
import './v3/removed-nodes-with-replacements.rule';
|
||||
import './v3/ssrf-default-blocked-ranges.rule';
|
||||
import './v3/task-runner-internal-mode.rule';
|
||||
import './v3/task-runner-task-timeout.rule';
|
||||
import './v3/unverified-packages.rule';
|
||||
import './v3/workflow-import-url-removed.rule';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
import type { GlobalConfig } from '@n8n/config';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { SsrfDefaultBlockedRangesRule } from '../ssrf-default-blocked-ranges.rule';
|
||||
|
||||
describe('SsrfDefaultBlockedRangesRule', () => {
|
||||
const createRule = (enabled: boolean) =>
|
||||
new SsrfDefaultBlockedRangesRule(mock<GlobalConfig>({ ssrfProtection: { enabled } }));
|
||||
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
delete process.env.N8N_SSRF_BLOCKED_IP_RANGES;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('detect()', () => {
|
||||
it('should be affected when SSRF protection is enabled', async () => {
|
||||
const result = await createRule(true).detect();
|
||||
|
||||
expect(result.isAffected).toBe(true);
|
||||
expect(result.instanceIssues).toHaveLength(1);
|
||||
expect(result.instanceIssues[0].level).toBe('warning');
|
||||
expect(result.recommendations).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should be affected when the block list keeps the default keyword', async () => {
|
||||
process.env.N8N_SSRF_BLOCKED_IP_RANGES = 'Default, 100.64.0.0/10';
|
||||
|
||||
const result = await createRule(true).detect();
|
||||
|
||||
expect(result.isAffected).toBe(true);
|
||||
});
|
||||
|
||||
it('should not be affected when the block list is literal ranges only', async () => {
|
||||
process.env.N8N_SSRF_BLOCKED_IP_RANGES = '10.0.0.0/8,127.0.0.0/8';
|
||||
|
||||
const result = await createRule(true).detect();
|
||||
|
||||
expect(result.isAffected).toBe(false);
|
||||
});
|
||||
|
||||
it('should not be affected when SSRF protection is disabled', async () => {
|
||||
const result = await createRule(false).detect();
|
||||
|
||||
expect(result.isAffected).toBe(false);
|
||||
expect(result.instanceIssues).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { TaskRunnerInternalModeRule } from '../task-runner-internal-mode.rule';
|
||||
|
||||
describe('TaskRunnerInternalModeRule', () => {
|
||||
const rule = new TaskRunnerInternalModeRule();
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
delete process.env.N8N_RUNNERS_MODE;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('detect()', () => {
|
||||
it('should be affected when the mode is explicitly internal', async () => {
|
||||
process.env.N8N_RUNNERS_MODE = 'internal';
|
||||
|
||||
const result = await rule.detect();
|
||||
|
||||
expect(result.isAffected).toBe(true);
|
||||
expect(result.instanceIssues).toHaveLength(1);
|
||||
expect(result.instanceIssues[0].level).toBe('warning');
|
||||
expect(result.recommendations).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not be affected when the mode is external', async () => {
|
||||
process.env.N8N_RUNNERS_MODE = 'external';
|
||||
|
||||
const result = await rule.detect();
|
||||
|
||||
expect(result.isAffected).toBe(false);
|
||||
});
|
||||
|
||||
it('should not be affected when the mode is not set', async () => {
|
||||
const result = await rule.detect();
|
||||
|
||||
expect(result.isAffected).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { GlobalConfig } from '@n8n/config';
|
||||
import { BreakingChangeRule } from '@n8n/decorators';
|
||||
|
||||
import type {
|
||||
BreakingChangeRuleMetadata,
|
||||
IBreakingChangeInstanceRule,
|
||||
InstanceDetectionReport,
|
||||
} from '../../types';
|
||||
import { BreakingChangeCategory } from '../../types';
|
||||
|
||||
@BreakingChangeRule({ version: 'v3' })
|
||||
export class SsrfDefaultBlockedRangesRule implements IBreakingChangeInstanceRule {
|
||||
constructor(private readonly globalConfig: GlobalConfig) {}
|
||||
|
||||
id: string = 'ssrf-default-blocked-ranges-v3';
|
||||
|
||||
getMetadata(): BreakingChangeRuleMetadata {
|
||||
return {
|
||||
version: 'v3',
|
||||
title: 'SSRF protection blocks more IP ranges by default',
|
||||
description:
|
||||
'The built-in blocked IP ranges expand to include the shared address space (100.64.0.0/10) and IPv6 transition ranges. Requests to hosts in these ranges start failing on instances with SSRF protection enabled.',
|
||||
category: BreakingChangeCategory.environment,
|
||||
severity: 'medium',
|
||||
documentationUrl:
|
||||
'https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables/security',
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async detect(): Promise<InstanceDetectionReport> {
|
||||
// Instances that enumerate literal ranges do not pick up the expanded built-in list.
|
||||
const ranges = process.env.N8N_SSRF_BLOCKED_IP_RANGES;
|
||||
const usesDefaultList =
|
||||
ranges === undefined ||
|
||||
ranges
|
||||
.toLowerCase()
|
||||
.split(',')
|
||||
.some((r) => r.trim() === 'default');
|
||||
|
||||
if (!this.globalConfig.ssrfProtection.enabled || !usesDefaultList) {
|
||||
return { isAffected: false, instanceIssues: [], recommendations: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
isAffected: true,
|
||||
instanceIssues: [
|
||||
{
|
||||
title: 'SSRF protection uses the built-in blocked IP ranges',
|
||||
description:
|
||||
'N8N_SSRF_PROTECTION_ENABLED is true and N8N_SSRF_BLOCKED_IP_RANGES relies on the built-in list, either because it is not set or because it contains the `default` keyword. After the update, that list also covers 100.64.0.0/10 and IPv6 transition ranges, so workflows calling hosts in these ranges fail.',
|
||||
level: 'warning',
|
||||
},
|
||||
],
|
||||
recommendations: [
|
||||
{
|
||||
action: 'Review internal hosts in the newly blocked ranges',
|
||||
description:
|
||||
"Check if any workflow calls hosts in 100.64.0.0/10 or IPv6 transition ranges. Add the ones you still need to N8N_SSRF_ALLOWED_IP_RANGES or N8N_SSRF_ALLOWED_HOSTNAMES. To keep the current list exactly, set N8N_SSRF_BLOCKED_IP_RANGES to the literal ranges; the `default` keyword always expands to the running version's built-in list.",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { BreakingChangeRule } from '@n8n/decorators';
|
||||
|
||||
import type {
|
||||
BreakingChangeRuleMetadata,
|
||||
IBreakingChangeInstanceRule,
|
||||
InstanceDetectionReport,
|
||||
} from '../../types';
|
||||
import { BreakingChangeCategory } from '../../types';
|
||||
|
||||
@BreakingChangeRule({ version: 'v3' })
|
||||
export class TaskRunnerInternalModeRule implements IBreakingChangeInstanceRule {
|
||||
id: string = 'task-runner-internal-mode-v3';
|
||||
|
||||
getMetadata(): BreakingChangeRuleMetadata {
|
||||
return {
|
||||
version: 'v3',
|
||||
title: 'Internal task runner mode is removed',
|
||||
description:
|
||||
'Task runners can no longer run as a child process of n8n. The new version only supports N8N_RUNNERS_MODE=external and fails to start Code node executions when no external task runner connects.',
|
||||
category: BreakingChangeCategory.infrastructure,
|
||||
severity: 'medium',
|
||||
documentationUrl: 'https://docs.n8n.io/deploy/host-n8n/configure-n8n/set-up-task-runners',
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async detect(): Promise<InstanceDetectionReport> {
|
||||
if (process.env.N8N_RUNNERS_MODE !== 'internal') {
|
||||
return { isAffected: false, instanceIssues: [], recommendations: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
isAffected: true,
|
||||
instanceIssues: [
|
||||
{
|
||||
title: 'N8N_RUNNERS_MODE is set to "internal"',
|
||||
description:
|
||||
'This instance runs task runners inside the n8n process. The new version rejects this mode, so Code nodes stop executing until an external task runner is connected.',
|
||||
level: 'warning',
|
||||
},
|
||||
],
|
||||
recommendations: [
|
||||
{
|
||||
action: 'Switch to external task runners before upgrading',
|
||||
description:
|
||||
'Run the task runner launcher as a separate process or container, set N8N_RUNNERS_MODE=external and share N8N_RUNNERS_AUTH_TOKEN between n8n and the launcher.',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user