From 7d61ea01229858d1dc6f75fcdb60cf3ce644adaa Mon Sep 17 00:00:00 2001 From: "n8n-assistant[bot]" <100856346+n8n-assistant[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:06:46 +0000 Subject: [PATCH] fix(core): Import security audit risk reporters with a file extension (backport to release-candidate/2.32.x) (#35267) Co-authored-by: Lorent Lempereur --- .../rules/no-dynamic-import-template.test.ts | 43 +++++++++++++++++++ .../src/rules/no-dynamic-import-template.ts | 23 +++++++++- .../security-audit/security-audit.service.ts | 5 +-- 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 packages/@n8n/eslint-config/src/rules/no-dynamic-import-template.test.ts diff --git a/packages/@n8n/eslint-config/src/rules/no-dynamic-import-template.test.ts b/packages/@n8n/eslint-config/src/rules/no-dynamic-import-template.test.ts new file mode 100644 index 00000000000..e9dcd203e1b --- /dev/null +++ b/packages/@n8n/eslint-config/src/rules/no-dynamic-import-template.test.ts @@ -0,0 +1,43 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import { NoDynamicImportTemplateRule } from './no-dynamic-import-template.js'; + +const ruleTester = new RuleTester(); + +ruleTester.run('no-dynamic-import-template', NoDynamicImportTemplateRule, { + valid: [ + { + code: 'await import(`./reporters/${name}.js`)', + }, + { + code: 'await import(`../reporters/${name}.mjs`)', + }, + { + code: 'await import(`./data/${name}.json`)', + }, + { + code: "await import('./reporters/credentials.js')", + }, + { + code: 'await import(`n8n-nodes-base/${name}`)', + }, + ], + invalid: [ + { + code: 'await import(`@/security-audit/${name}.js`)', + errors: [{ messageId: 'noDynamicImportTemplate' }], + }, + { + code: 'await import(`./reporters/${name}`)', + errors: [{ messageId: 'missingExtension' }], + }, + { + code: 'await import(`../reporters/${name}`)', + errors: [{ messageId: 'missingExtension' }], + }, + { + code: 'await import(`./reporters/${name}.ts`)', + errors: [{ messageId: 'missingExtension' }], + }, + ], +}); diff --git a/packages/@n8n/eslint-config/src/rules/no-dynamic-import-template.ts b/packages/@n8n/eslint-config/src/rules/no-dynamic-import-template.ts index 57f3168caf5..39d74e477c5 100644 --- a/packages/@n8n/eslint-config/src/rules/no-dynamic-import-template.ts +++ b/packages/@n8n/eslint-config/src/rules/no-dynamic-import-template.ts @@ -1,16 +1,20 @@ import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils'; +const MODULE_EXTENSIONS = ['.js', '.mjs', '.cjs', '.json', '.node']; + export const NoDynamicImportTemplateRule = ESLintUtils.RuleCreator.withoutDocs({ meta: { type: 'problem', docs: { description: - 'Disallow non-relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario.', + 'Disallow non-relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario. Also require an explicit file extension, because `module: NodeNext` emits a native `import()` whose specifier Node resolves without extension inference.', }, schema: [], messages: { noDynamicImportTemplate: 'Use relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario.', + missingExtension: + 'End the template string argument to `await import()` with an explicit file extension (e.g. `.js`). Under `module: NodeNext` this compiles to a native `import()`, and Node throws ERR_MODULE_NOT_FOUND for an extensionless relative specifier.', }, }, defaultOptions: [], @@ -26,6 +30,23 @@ export const NoDynamicImportTemplateRule = ESLintUtils.RuleCreator.withoutDocs({ messageId: 'noDynamicImportTemplate', }); }, + + 'AwaitExpression > ImportExpression > TemplateLiteral'(node: TSESTree.TemplateLiteral) { + const head = node.quasis[0].value.cooked; + + if (!head?.startsWith('./') && !head?.startsWith('../')) { + return; + } + + const tail = node.quasis[node.quasis.length - 1].value.cooked ?? ''; + + if (!MODULE_EXTENSIONS.some((extension) => tail.endsWith(extension))) { + context.report({ + node, + messageId: 'missingExtension', + }); + } + }, }; }, }); diff --git a/packages/cli/src/security-audit/security-audit.service.ts b/packages/cli/src/security-audit/security-audit.service.ts index bb983e08e10..bb9c21306fb 100644 --- a/packages/cli/src/security-audit/security-audit.service.ts +++ b/packages/cli/src/security-audit/security-audit.service.ts @@ -63,11 +63,10 @@ export class SecurityAuditService { }; // `@vite-ignore` keeps vite's dynamic-import-vars plugin from analyzing this - // runtime-only import: without a static file extension it otherwise errors, - // which surfaces fatally during v8 coverage's uncovered-file walk. + // runtime-only import, which surfaces fatally during v8 coverage's uncovered-file walk. // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const RiskReporterModule = await import( - /* @vite-ignore */ `./risk-reporters/${toFilename[className]}` + /* @vite-ignore */ `./risk-reporters/${toFilename[className]}.js` ); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access