Tienes un proyecto Node.js que usa minimatch para filtrar archivos. Funciona bien con patrones simples como *.js o src/**/*.ts. Pero un día añades un patrón con llaves como {src,lib}/**/*.{js,ts,jsx,tsx} y notas que el build tarda más de lo esperado. Investigas y descubres que minimatch convierte ese patrón en una expresión regular que el motor de JavaScript ejecuta con backtracking exponencial.
El problema no es solo rendimiento. Minimatch tiene CVE-2022-3517, una vulnerabilidad de denegación de servicio por expresiones regulares. Un atacante puede enviar un patrón malicioso que congela tu servidor durante minutos. La vulnerabilidad lleva años documentada pero sigue presente en millones de proyectos porque minimatch es una dependencia transitiva que nadie revisa.
Ejecutas npm ls minimatch en tu proyecto y ves que lo usan glob, fast-glob, rimraf, del, y otras diez dependencias que ni sabías que tenías. Actualizar minimatch directamente no sirve porque cada paquete trae su propia versión. Y aunque la actualizaras, el rendimiento sigue siendo el mismo.
El problema de las expresiones regulares con backtracking
Minimatch convierte patrones glob a expresiones regulares. El patrón *.js se convierte en algo como ^(?:(?!\.)(?=.)[^/]*?\.js)$. Esta expresión funciona correctamente, pero cuando el patrón tiene alternativas como {a,b,c} o rangos como {1..100}, la expresión regular resultante crece exponencialmente.
El motor de expresiones regulares de JavaScript usa backtracking para encontrar coincidencias. Cuando una rama de la expresión no coincide, el motor retrocede e intenta otra rama. Con expresiones complejas, el número de combinaciones a probar crece de forma exponencial. Un patrón que parece inocente puede generar millones de operaciones de backtracking.
La vulnerabilidad CVE-2022-3517 explota exactamente esto. Un patrón diseñado específicamente puede hacer que el motor de regex entre en un bucle de backtracking que consume CPU durante segundos o minutos. En un servidor web, esto significa que un atacante puede tumbar tu servicio enviando una petición con un patrón malicioso.
La solución: picomatch como motor interno
Picomatch es otro matcher de globs escrito por Jon Schlinkert. Usa un enfoque diferente para generar expresiones regulares que evita el backtracking catastrófico. Además, tiene límites internos en la expansión de llaves para evitar que patrones como {1..1000000} generen un millón de alternativas.
El problema de picomatch es que su API no es compatible con minimatch. Si tu código usa minimatch(path, pattern), no puedes simplemente cambiar a picomatch(pattern)(path). Tendrías que reescribir todas las llamadas, actualizar los tests, y verificar que el comportamiento sea idéntico en todos los casos extremos.
Aquí es donde entra minimatch-fast. Es una capa de compatibilidad que expone exactamente la misma API que minimatch pero usa picomatch internamente. No tienes que cambiar una línea de código. Instalas el paquete, actualizas el import, y todo funciona igual pero más rápido y sin la vulnerabilidad.
Instalación y migración
La instalación es estándar:
npm install minimatch-fast
La migración más simple es cambiar los imports:
// Antes
import { minimatch } from 'minimatch';
// Después
import { minimatch } from 'minimatch-fast';
Si no quieres tocar el código, puedes usar el aliasing de npm. Esto hace que cualquier paquete que importe minimatch reciba minimatch-fast en su lugar:
npm install minimatch@npm:minimatch-fast
Con esta opción, todas las dependencias que usan minimatch automáticamente usan minimatch-fast sin saberlo. Es útil cuando tienes dependencias transitivas que no puedes controlar.
API idéntica a minimatch
La función principal es minimatch(path, pattern, options). Recibe una ruta, un patrón glob, y opciones opcionales. Devuelve true si la ruta coincide con el patrón.
import { minimatch } from 'minimatch-fast';
minimatch('src/index.ts', '**/*.ts'); // true
minimatch('src/index.ts', '**/*.js'); // false
minimatch('.gitignore', '*', { dot: true }); // true
minimatch('SRC/Index.ts', '**/*.ts', { nocase: true }); // true
Para filtrar arrays de rutas, usa minimatch.match():
const files = ['app.js', 'app.ts', 'test.js', 'README.md'];
const jsFiles = minimatch.match(files, '*.js');
// ['app.js', 'test.js']
Si necesitas un filtro reutilizable para Array.filter():
const files = ['app.js', 'app.ts', 'test.js'];
const isTypeScript = minimatch.filter('*.ts');
const tsFiles = files.filter(isTypeScript);
// ['app.ts']
Para obtener la expresión regular compilada:
const regex = minimatch.makeRe('**/*.ts');
// Devuelve un objeto RegExp que puedes usar directamente
regex.test('src/index.ts'); // true
La expansión de llaves convierte un patrón en múltiples patrones:
minimatch.braceExpand('{src,lib}/*.js');
// ['src/*.js', 'lib/*.js']
minimatch.braceExpand('file{1..3}.txt');
// ['file1.txt', 'file2.txt', 'file3.txt']
Para escapar metacaracteres cuando el patrón viene de input de usuario:
const userInput = 'file[1].txt';
const escaped = minimatch.escape(userInput);
// 'file\\[1\\].txt'
minimatch('file[1].txt', escaped); // true (match literal)
La clase Minimatch para patrones reutilizables
Cuando vas a comparar el mismo patrón contra muchas rutas, compilar el patrón una vez es más eficiente:
import { Minimatch } from 'minimatch-fast';
const matcher = new Minimatch('**/*.{js,ts}');
const files = ['app.js', 'lib/utils.ts', 'README.md', 'src/index.tsx'];
const matches = files.filter(file => matcher.match(file));
// ['app.js', 'lib/utils.ts']
La clase expone propiedades útiles:
const m = new Minimatch('!**/*.test.js');
m.pattern; // '!**/*.test.js'
m.negate; // true (el patrón empieza con !)
m.comment; // false (no es un comentario)
m.regexp; // RegExp compilada
El método hasMagic() indica si el patrón tiene metacaracteres glob:
new Minimatch('*.js').hasMagic(); // true
new Minimatch('index.js').hasMagic(); // false
Sintaxis de patrones soportada
Los comodines básicos funcionan como esperas:
*coincide con cualquier carácter excepto/**coincide con cualquier carácter incluyendo/(cruza directorios)?coincide con un solo carácter[abc]coincide con cualquiera de los caracteres listados[a-z]coincide con un rango de caracteres[!abc]coincide con cualquier carácter excepto los listados
Las llaves expanden alternativas:
minimatch('src/app.js', '{src,lib}/*.js'); // true
minimatch('lib/app.js', '{src,lib}/*.js'); // true
minimatch('dist/app.js', '{src,lib}/*.js'); // false
Los rangos numéricos y alfabéticos también funcionan:
minimatch('file3.txt', 'file{1..5}.txt'); // true
minimatch('file7.txt', 'file{1..5}.txt'); // false
minimatch('section-b.md', 'section-{a..d}.md'); // true
Los patrones extglob añaden operadores de expresiones regulares:
@(a|b)coincide exactamente conaob?(a|b)coincide con cero o una ocurrencia deaob*(a|b)coincide con cero o más ocurrencias+(a|b)coincide con una o más ocurrencias!(a|b)coincide con cualquier cosa exceptoaob
minimatch('foo.js', '*.+(js|ts)'); // true
minimatch('foo.jsx', '*.+(js|ts)'); // false
minimatch('test.spec.js', '!(*spec*).js'); // false (contiene spec)
minimatch('app.js', '!(*spec*).js'); // true
Las clases POSIX proporcionan conjuntos de caracteres portables:
minimatch('file1.txt', 'file[[:digit:]].txt'); // true
minimatch('fileA.txt', 'file[[:alpha:]].txt'); // true
minimatch('FILE.txt', '[[:upper:]]*.txt'); // true
La negación invierte el resultado del match:
minimatch('app.js', '!*.test.js'); // true (no es un test)
minimatch('app.test.js', '!*.test.js'); // false (es un test)
Opciones de configuración
Las opciones controlan el comportamiento del matching:
dot: Por defecto, * no coincide con archivos que empiezan por punto. Activa esta opción para incluirlos.
minimatch('.gitignore', '*'); // false
minimatch('.gitignore', '*', { dot: true }); // true
nocase: Matching sin distinguir mayúsculas y minúsculas.
minimatch('README.md', '*.MD'); // false
minimatch('README.md', '*.MD', { nocase: true }); // true
matchBase: Compara el patrón solo contra el nombre del archivo, no la ruta completa.
minimatch('src/lib/utils.js', '*.js'); // false
minimatch('src/lib/utils.js', '*.js', { matchBase: true }); // true
noglobstar: Trata ** como * (no cruza directorios).
minimatch('a/b/c.js', '**/*.js'); // true
minimatch('a/b/c.js', '**/*.js', { noglobstar: true }); // false
nobrace: Desactiva la expansión de llaves.
minimatch('src/a.js', '{src,lib}/a.js'); // true
minimatch('src/a.js', '{src,lib}/a.js', { nobrace: true }); // false
noext: Desactiva los patrones extglob.
minimatch('foo.js', '*.+(js|ts)'); // true
minimatch('foo.js', '*.+(js|ts)', { noext: true }); // false
nonegate: Desactiva la negación con !.
minimatch('app.js', '!*.test.js'); // true
minimatch('app.js', '!*.test.js', { nonegate: true }); // false
Para crear un matcher con opciones predeterminadas:
const mm = minimatch.defaults({ nocase: true, dot: true });
mm('README.MD', '*.md'); // true
mm('.env', '*'); // true
Benchmarks: entre 1.3x y 26x más rápido
Los benchmarks se ejecutaron en Node.js 22 comparando minimatch 10.0.1 contra minimatch-fast 1.0.0. Los resultados varían según el tipo de patrón:
Patrones simples (*.js, **/*.ts): 1.35x más rápido. La mejora es modesta porque estos patrones ya son eficientes en minimatch.
Patrones con negación (!*.test.js): 1.50x más rápido.
Patrones con llaves ({src,lib}/**/*.{js,ts}): 6.5x más rápido. Aquí es donde picomatch brilla porque evita la explosión combinatoria de alternativas.
Patrones con llaves complejas ({a,b,c}/{d,e,f}/**/*.{js,ts,jsx,tsx}): 26.6x más rápido. Minimatch genera una expresión regular gigante mientras que picomatch mantiene el rendimiento lineal.
Clase Minimatch precompilada: 1.16x más rápido en uso repetido del mismo patrón.
En términos prácticos, si tu build tarda 10 segundos procesando globs con patrones complejos, puede bajar a menos de 1 segundo. En patrones simples la mejora es menor pero nunca es más lento que minimatch.
Seguridad: protección contra ReDoS
La vulnerabilidad CVE-2022-3517 permite ataques de denegación de servicio mediante expresiones regulares. Un atacante envía un patrón diseñado para causar backtracking catastrófico y tu servidor se congela.
minimatch-fast mitiga esto de dos formas:
Motor de regex seguro: Picomatch genera expresiones regulares que no sufren backtracking exponencial. Los patrones que congelan minimatch se ejecutan instantáneamente en minimatch-fast.
Límites en expansión de llaves: La expansión de rangos como {1..1000} tiene límites para evitar que un patrón genere millones de alternativas. Minimatch original se congela con {1..100000}. minimatch-fast lo procesa en milisegundos con un límite razonable.
// Esto congela minimatch durante segundos o minutos
// minimatch('file.js', '{1..100000}.js');
// minimatch-fast lo maneja sin problemas
minimatch('file.js', '{1..100000}.js'); // false, procesado en ms
Si tu aplicación recibe patrones de usuarios externos, minimatch-fast es la opción segura.
Compatibilidad verificada con 355 tests
El paquete incluye 355 tests que verifican compatibilidad exacta con minimatch:
- 42 tests unitarios de funcionalidad básica
- 42 tests de casos extremos y edge cases
- 22 tests de seguridad y protección contra patrones maliciosos
- 196 tests de compatibilidad derivados del test suite original de minimatch
- 53 tests de verificación de clases POSIX, Unicode y regex
Todos los tests de minimatch original pasan. Si tu código funciona con minimatch, funciona con minimatch-fast sin cambios.
Soporte TypeScript completo
El paquete incluye definiciones de tipos completas:
import {
minimatch,
Minimatch,
type MinimatchOptions,
} from 'minimatch-fast';
const options: MinimatchOptions = {
nocase: true,
dot: true,
};
const matcher: Minimatch = new Minimatch('**/*.ts', options);
const result: boolean = matcher.match('src/index.ts');
Los tipos son idénticos a los de @types/minimatch, así que el autocompletado y verificación de tipos funcionan igual.
Cuándo usar minimatch-fast
Migración desde minimatch: Si ya usas minimatch y quieres mejor rendimiento o eliminar la vulnerabilidad CVE-2022-3517, minimatch-fast es un cambio de una línea.
Proyectos nuevos: Si vas a usar glob matching, empieza directamente con minimatch-fast en lugar de minimatch.
Patrones complejos: Si usas patrones con muchas llaves o rangos, la mejora de rendimiento es dramática.
Aplicaciones que reciben patrones externos: Si los usuarios pueden enviar patrones glob, minimatch-fast protege contra ataques ReDoS.
Dependencias transitivas: Usa el aliasing de npm para que todas tus dependencias usen minimatch-fast automáticamente.
El código está en GitHub
El paquete está publicado en NPM como minimatch-fast. El código fuente está en github.com/686f6c61/minimatch-fast.
La licencia es MIT, igual que minimatch original. Puedes usarlo en proyectos comerciales sin restricciones.
Si encuentras incompatibilidades con minimatch o tienes sugerencias, abre un issue en GitHub. El objetivo es mantener compatibilidad al 100% mientras mejoramos rendimiento y seguridad.