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:
n8n-assistant[bot] 2026-09-08 17:34:02 +00:00 committed by GitHub
parent 3d2f334c24
commit 16199c6e56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 265 additions and 0 deletions

View File

@ -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';

View File

@ -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:

View File

@ -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';

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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.",
},
],
};
}
}

View File

@ -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.',
},
],
};
}
}