Skip to content
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,21 @@ core module:
}
```

Wildcard patterns are supported to match multiple modules, using `*` as a wildcard:

```jsonc
// .eslintrc
{
"settings": {
"import/core-modules": [
"electron",
"@my-monorepo/*", // matches @my-monorepo/package-a, @my-monorepo/package-b, etc.
"@my-*/*", // matches @my-org/package, @my-company/package, etc.
],
},
}
```

In Electron's specific case, there is a shared config named `electron`
that specifies this for you.

Expand Down
44 changes: 43 additions & 1 deletion src/core/importType.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isAbsolute as nodeIsAbsolute, relative, resolve as nodeResolve } from 'path';
import isCoreModule from 'is-core-module';
import minimatch from 'minimatch';

import resolve from 'eslint-module-utils/resolve';
import { getContextPackagePath } from './packagePath';
Expand All @@ -23,6 +24,45 @@ function isInternalRegexMatch(name, settings) {
return internalScope && new RegExp(internalScope).test(name);
}

function isDangerousPattern(pattern) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this function at all now that it's just using minimatch?

// Block patterns that would match too broadly
if (pattern === '*') { return true; } // Matches everything
if (pattern === '**') { return true; } // Double wildcard
if (pattern === '*/*') { return true; } // Matches any scoped package
if (pattern === '.*') { return true; } // Regex-style wildcard
if (pattern.startsWith('.*')) { return true; } // Regex wildcards
if (pattern.endsWith('.*')) { return true; } // Regex wildcards

// Block patterns that are too short and broad
if (pattern.length <= 2 && pattern.includes('*')) { return true; }

// Block patterns with multiple wildcards that could be too broad
const wildcardCount = pattern.split('*').length - 1;
if (wildcardCount > 1) {
// Allow valid scoped patterns like @namespace/* or @my-*/*, but block overly broad ones
const validScopedPatterns = [
'@*/*', // @namespace/package
'@*-*/*', // @my-namespace/package
'@*/package-*', // @namespace/package-name
];

if (!validScopedPatterns.some((validPattern) => minimatch(pattern, validPattern))) {
return true;
}
}

return false;
}

function matchesCoreModulePattern(name, pattern) {
// Prevent dangerous patterns that could match too broadly
if (isDangerousPattern(pattern)) {
return false;
}

return minimatch(name, pattern);
}

export function isAbsolute(name) {
return typeof name === 'string' && nodeIsAbsolute(name);
}
Expand All @@ -32,7 +72,9 @@ export function isBuiltIn(name, settings, path) {
if (path || !name) { return false; }
const base = baseModule(name);
const extras = settings && settings['import/core-modules'] || [];
return isCoreModule(base) || extras.indexOf(base) > -1;
return isCoreModule(base)
|| extras.indexOf(base) > -1
|| extras.some((pattern) => pattern.includes('*') && matchesCoreModulePattern(base, pattern));
}

const moduleRegExp = /^\w/;
Expand Down
121 changes: 120 additions & 1 deletion tests/src/core/importType.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from 'chai';
import * as path from 'path';
import isCoreModule from 'is-core-module';

import importType, { isExternalModule, isScoped, isAbsolute } from 'core/importType';
import importType, { isExternalModule, isScoped, isAbsolute, isBuiltIn } from 'core/importType';

import { testContext, testFilePath } from '../utils';

Expand Down Expand Up @@ -139,6 +139,94 @@ describe('importType(name)', function () {
expect(importType('@org/foobar/some/path/to/resource.json', scopedContext)).to.equal('builtin');
});

it("should return 'builtin' for wildcard patterns in core modules", function () {
// Test basic wildcard patterns
const wildcardContext = testContext({ 'import/core-modules': ['@my-monorepo/*'] });
expect(importType('@my-monorepo/package-a', wildcardContext)).to.equal('builtin');
expect(importType('@my-monorepo/package-b', wildcardContext)).to.equal('builtin');
expect(importType('@my-monorepo/some-long-package-name', wildcardContext)).to.equal('builtin');

// Test that non-matching patterns return external
expect(importType('@other-org/package', wildcardContext)).to.equal('external');
expect(importType('regular-package', wildcardContext)).to.equal('external');
expect(importType('@my-monorepo-but-not-scoped/package', wildcardContext)).to.equal('external');
});

it("should return 'builtin' for wildcard patterns with multiple wildcards", function () {
const multiWildcardContext = testContext({ 'import/core-modules': ['@my-*/*'] });
expect(importType('@my-org/package', multiWildcardContext)).to.equal('builtin');
expect(importType('@my-company/package', multiWildcardContext)).to.equal('builtin');
expect(importType('@my-test/package', multiWildcardContext)).to.equal('builtin');

// Should not match different patterns
expect(importType('@other-org/package', multiWildcardContext)).to.equal('external');
expect(importType('my-org/package', multiWildcardContext)).to.equal('external');
});

it("should return 'builtin' for resources inside wildcard core modules", function () {
const wildcardContext = testContext({ 'import/core-modules': ['@my-monorepo/*'] });
expect(importType('@my-monorepo/package-a/some/path/to/resource.json', wildcardContext)).to.equal('builtin');
expect(importType('@my-monorepo/package-b/nested/module', wildcardContext)).to.equal('builtin');
});

it('should support mixing exact matches and wildcards in core modules', function () {
const mixedContext = testContext({ 'import/core-modules': ['electron', '@my-monorepo/*', '@specific/package'] });

// Exact matches should work
expect(importType('electron', mixedContext)).to.equal('builtin');
expect(importType('@specific/package', mixedContext)).to.equal('builtin');

// Wildcard matches should work
expect(importType('@my-monorepo/any-package', mixedContext)).to.equal('builtin');

// Non-matches should be external
expect(importType('@other/package', mixedContext)).to.equal('external');
});

it('should handle dangerous wildcard patterns safely', function () {
// Test various dangerous patterns that should be blocked
const dangerousPatterns = [
'*', // Bare wildcard
'**', // Double wildcard
'*/*', // Any scoped package
'.*', // Regex wildcard
'.*foo', // Regex prefix
'foo.*', // Regex suffix
'a*', // Too short and broad
'*a', // Too short and broad
'*foo*', // Multiple wildcards (too broad)
'foo*bar*', // Multiple wildcards (too broad)
'*/*/*', // Triple wildcards
];

dangerousPatterns.forEach((pattern) => {
const context = testContext({ 'import/core-modules': [pattern] });
expect(importType('react', context)).to.equal('external', `Pattern "${pattern}" should not match anything`);
expect(importType('lodash', context)).to.equal('external', `Pattern "${pattern}" should not match anything`);
expect(importType('@babel/core', context)).to.equal('external', `Pattern "${pattern}" should not match anything`);
});

// Test that valid patterns still work
const validPatterns = [
'@my-org/*', // Valid scoped wildcard
'my-prefix-*', // Valid prefix wildcard
'@namespace/prefix-*', // Valid scoped prefix wildcard
'electron', // Exact match (no wildcard)
];

validPatterns.forEach((pattern) => {
const context = testContext({ 'import/core-modules': [pattern] });
// Should not break the system - external packages should still be external
expect(importType('totally-different-package', context)).to.equal('external', `Pattern "${pattern}" should not break normal operation`);
});

// Test specific valid matches
const validContext = testContext({ 'import/core-modules': ['@my-org/*', 'my-prefix-*'] });
expect(importType('@my-org/package', validContext)).to.equal('builtin');
expect(importType('my-prefix-tool', validContext)).to.equal('builtin');
expect(importType('react', validContext)).to.equal('external');
});

it("should return 'external' for module from 'node_modules' with default config", function () {
expect(importType('resolve', context)).to.equal('external');
});
Expand Down Expand Up @@ -283,4 +371,35 @@ describe('isAbsolute', () => {
expect(() => isAbsolute(0)).not.to.throw();
expect(() => isAbsolute(NaN)).not.to.throw();
});

it('should not use dynamic regex patterns that could cause ReDoS vulnerabilities', function () {
// Test that dangerous patterns are blocked by isDangerousPattern
const dangerousPatterns = [
'*', // Matches everything
'**', // Double wildcard
'*/*', // Any scoped package
'.*', // Regex wildcard
'.+', // Regex plus
'.*foo', // Regex prefix
'foo.*', // Regex suffix
'a*', // Too short
'ab*', // Too short
];

dangerousPatterns.forEach((pattern) => {
const context = testContext({ 'import/core-modules': [pattern] });
// These should all be blocked and not match anything
expect(isBuiltIn('test-module', context.settings, null)).to.equal(false);
expect(isBuiltIn('@test/module', context.settings, null)).to.equal(false);
});
});

it('should use safe glob matching instead of regex construction', function () {
// Verify no dynamic regex patterns like [\\s\\S]*? are created
const context = testContext({ 'import/core-modules': ['@my-monorepo/*'] });
// Valid patterns should work safely without regex construction
expect(isBuiltIn('@my-monorepo/package-a', context.settings, null)).to.equal(true);
expect(isBuiltIn('@my-monorepo/package-b', context.settings, null)).to.equal(true);
expect(isBuiltIn('@other-org/package', context.settings, null)).to.equal(false);
});
});
17 changes: 17 additions & 0 deletions tests/src/rules/no-extraneous-dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,23 @@ ruleTester.run('no-extraneous-dependencies', rule, {
code: 'import "@generated/bar/and/sub/path"',
settings: { 'import/core-modules': ['@generated/bar'] },
}),
// Test wildcard patterns in core-modules
test({
code: 'import "@my-monorepo/package-a"',
settings: { 'import/core-modules': ['@my-monorepo/*'] },
}),
test({
code: 'import "@my-monorepo/package-b/nested/module"',
settings: { 'import/core-modules': ['@my-monorepo/*'] },
}),
test({
code: 'import "@my-org/any-package"',
settings: { 'import/core-modules': ['@my-*/*'] },
}),
test({
code: 'import "@namespace/any-package"',
settings: { 'import/core-modules': ['@namespace/*', 'specific-module'] },
}),
// check if "rxjs" dependency declaration fix the "rxjs/operators subpackage
test({
code: 'import "rxjs/operators"',
Expand Down
Loading