Introduction
A flaw in Babel's SystemJS module transform plugin allows specially crafted JavaScript source code to produce compiled output containing arbitrary executable code, which turns the build process itself into an attack vector. With roughly 367 million annual npm downloads for babel-core and a four-year window of affected versions, this vulnerability falls squarely into the supply-chain risk category that security teams have been watching more and more closely.
CVE-2026-44728 carries a CVSS 8.2 (HIGH) score and affects @babel/plugin-transform-modules-systemjs from version 7.12.0 through 7.29.3, as well as the 8.x alpha line from 8.0.0-alpha.0 through 8.0.0-alpha.12. It is classified under both CWE-94 (Code Injection) and CWE-843 (Type Confusion), two distinct weakness mechanisms that lead to the same outcome.
Technical Information
Vulnerable Component
The vulnerable component is @babel/plugin-transform-modules-systemjs, a Babel plugin that transforms ECMAScript module syntax (import/export statements and import() expressions) into SystemJS-compatible format. @babel/preset-env also includes this plugin when configured with the modules: "systemjs" option, so projects using preset-env with that setting are transitively affected. No other plugins under the @babel/ namespace are affected by this vulnerability.
Root Cause
The root cause is in the constructExportCall function in packages/babel-plugin-transform-modules-systemjs/src/index.ts. When Babel encounters export names that are string specifiers (a feature introduced in ES2022, e.g., export {foo as "default exports"}), the vulnerable code path unconditionally used t.identifier(exportName) to build MemberExpression AST nodes.
t.identifier() does not sanitize or validate the string it receives. An identifier node in the AST is meant to hold a valid JavaScript identifier, not an arbitrary string, so a carefully crafted string export name passed through t.identifier() can break out of the intended AST structure entirely. The resulting compiled output would contain the attacker's string as raw JavaScript code rather than as a quoted property name.
For example, take an export like export {foo as "malicious;payload()//"}. The vulnerable code path places this string into an identifier node and renders it as dot notation in the output: _exportObj.malicious;payload()//. The semicolon terminates the intended statement, payload() executes as a function call, and // comments out the rest of the line. The compiled output now contains executable code that was never present in the original source.
That is why the vulnerability carries both CWE-94 (Code Injection) and CWE-843 (Type Confusion). The type confusion happens when a string specifier, which should be treated as a string literal, is incorrectly processed as an identifier. The code injection follows from it, because attacker-controlled data then influences the structure of the generated output code.
CVSS Vector Breakdown
The CVSS vector CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H breaks down as follows:
| Component | Value | Meaning |
|---|---|---|
| Attack Vector | Local (AV:L) | Attacker must provide code to the Babel compiler locally or through a pipeline |
| Attack Complexity | Low (AC:L) | No special conditions required beyond submitting crafted code |
| Privileges Required | Low (PR:L) | Minimal access needed to submit source for compilation |
| User Interaction | Required (UI:R) | A user or automated process must compile the malicious code |
| Scope | Changed (S:C) | Impact extends beyond the vulnerable component |
| Confidentiality | High (C:H) | Total information disclosure possible |
| Integrity | High (I:H) | Total system integrity compromise possible |
| Availability | High (A:H) | Total availability disruption possible |
The "Scope: Changed" designation means a compromise of the build toolchain can cascade into production environments. Code injected during compilation would be present in the final bundled output that is served to users or executed on servers.
Attack Flow
-
The attacker creates a JavaScript file containing an export statement with a carefully constructed string specifier, such as
export {foo as "crafted_payload"}, where the payload string is designed to produce valid, executable JavaScript when incorrectly placed into an identifier position. -
The attacker submits this file to a system that compiles it using Babel with the SystemJS module transform enabled. This could be a CI/CD pipeline processing pull requests, a build server handling dependency updates, or any workflow that compiles third-party code.
-
During the AST transformation phase,
@babel/plugin-transform-modules-systemjsencounters the string export specifier and passes it tot.identifier()instead oft.stringLiteral(). -
The compiled output contains the attacker's payload as executable JavaScript code. When this output is later loaded or executed (in a browser, Node.js runtime, or test environment), the injected code runs with the privileges of the execution context.
Key Constraint
The advisory explicitly states that this vulnerability only affects users who compile untrusted or third-party code. Projects that strictly control their input source code and never compile external contributions face significantly lower risk, and that condition should drive how teams assess and prioritize the fix.
Patch Information
The Babel team fixed CVE-2026-44728 in commit 5abf7bb (PR #17973), authored by Huáng Jùnliàng (@JLHwung) and reviewed by Nicolò Ribaudo (@nicolo-ribaudo). The fix was published on May 5, 2026, and the advisory (GHSA-fv7c-fp4j-7gwp) was formally released on May 8, 2026.
The fix targets the root cause directly. It checks whether the export name is a string specifier and, if so, wraps the name in t.stringLiteral() (which escapes the string properly) and marks the MemberExpression as computed. Here is the relevant portion of the diff:
// In constructExportCall — the member expression branch: for (let i = 0; i < exportNames.length; i++) { const exportName = exportNames[i]; + const computed = stringSpecifiers.has(exportName); + const exportNameNode = computed + ? t.stringLiteral(exportName) + : t.identifier(exportName); const exportValue = exportValues[i]; statements.push( @@ -140,7 +140,8 @@ function constructExportCall( "=", t.memberExpression( t.identifier(exportObj), - t.identifier(exportName), + exportNameNode, + computed, ), exportValue, ),
The generated code now uses bracket notation with a properly escaped string literal (_exportObj["default exports"]) instead of the unsafe dot notation (_exportObj.default exports), which would have caused the injected string to be interpreted as executable code.
The same commit applies a parallel fix to the object-property construction path, so both code-generation branches handle string specifiers the same safe way:
for (let i = 0; i < exportNames.length; i++) { const exportName = exportNames[i]; + const exportNameNode = stringSpecifiers.has(exportName) + ? t.stringLiteral(exportName) + : t.identifier(exportName); const exportValue = exportValues[i]; - objectProperties.push( - t.objectProperty( - stringSpecifiers.has(exportName) - ? t.stringLiteral(exportName) - : t.identifier(exportName), - exportValue, - ), - ); + objectProperties.push(t.objectProperty(exportNameNode, exportValue));
The commit also adds new test fixtures that check the output for string-name exports combined with export * re-exports, such as export {foo as "default exports", bar} from "./other.mjs" alongside export * from "./other.mjs".
Fixed Versions
| Package | Fixed Version |
|---|---|
@babel/plugin-transform-modules-systemjs | 7.29.4 (for 7.x) |
@babel/plugin-transform-modules-systemjs | 8.0.0-alpha.13 (for 8.x alpha) |
@babel/preset-env | 7.29.5 (pulls in the patched plugin) |
To upgrade, run npm install @babel/[email protected] or npm audit fix.
Ecosystem Response
Several high-profile projects bumped the dependency within days of the advisory:
| Project | Action | Date |
|---|---|---|
| axios | Bumped plugin to 7.29.4 | May 9, 2026 |
| MetaMask Snaps | Dependency bump to 7.29.4 | May 9, 2026 |
| Apache (mailing list PR) | Bumped from 7.18.6 to 7.29.4 | May 9, 2026 |
| OpenGeoscience/geojs | Bumped from 7.29.0 to 7.29.4 | Post-advisory |
Affected Systems and Versions
The following packages and version ranges are affected:
| Package | Affected Range | Fixed Version |
|---|---|---|
@babel/plugin-transform-modules-systemjs | >= 7.12.0 and < 7.29.4 | 7.29.4 |
@babel/plugin-transform-modules-systemjs | >= 8.0.0-alpha.0 and < 8.0.0-alpha.13 | 8.0.0-alpha.13 |
@babel/preset-env (with modules: "systemjs") | Same ranges (transitive) | 7.29.5 |
The vulnerability was introduced in version 7.12.0, which added support for ES2022 string-name specifiers, so the exposure window spans approximately four years of releases across the v7 line.
Vulnerable configurations. A project is affected if it meets both of the following conditions:
- It uses
@babel/plugin-transform-modules-systemjsdirectly, or uses@babel/preset-envwith themodules: "systemjs"option. - It compiles untrusted or third-party source code through Babel.
Projects using CommonJS, AMD, or native ES module output are not affected by this vulnerability. Projects that compile only their own trusted source code are at significantly lower risk.
To identify affected configurations, audit Babel configuration files (.babelrc, babel.config.js, babel.config.json) for the modules: "systemjs" setting or direct references to @babel/plugin-transform-modules-systemjs.
Vendor Security History
CVE-2026-44728 is the second arbitrary code execution vulnerability discovered in Babel in recent years. The prior one, CVE-2023-45133, affected @babel/traverse, a core component of the Babel compilation pipeline, and was exploitable through the path.evaluate() function when Babel processed crafted input.
| CVE ID | Component | CWE | Fixed Version |
|---|---|---|---|
| CVE-2023-45133 | @babel/traverse | CWE-697, CWE-184 | 7.23.2, 8.0.0-alpha.4 |
| CVE-2026-44728 | @babel/plugin-transform-modules-systemjs | CWE-94, CWE-843 | 7.29.4, 8.0.0-alpha.13 |
Both vulnerabilities enable arbitrary code execution when Babel compiles specially crafted input, but they sit in different components and have different root causes. CVE-2023-45133 affected the core traversal engine used by many plugins, while CVE-2026-44728 is isolated to the SystemJS module transform. The CWE classifications differ as well (improper comparison and incomplete input filtering vs. code injection and type confusion), which points to distinct weaknesses arriving at the same outcome.
Babel is maintained by a small group of volunteers rather than a company with dedicated security staff. The original maintainer described working on Babel as a side project alongside his role as a JavaScript Engineer at Adobe. The project shipped fixes for CVE-2026-44728 promptly in both the v7 and v8 alpha channels, but the four-year exposure window shows how hard it is to find this kind of vulnerability ahead of time in a volunteer-maintained codebase. The Shenlong CVE Platform lists 4 CVEs in total for Babel.
Organizations that depend on Babel should expect that more compilation-time vulnerabilities may be discovered, and should build defense in depth around their build toolchains.
References
- GitHub Advisory GHSA-fv7c-fp4j-7gwp
- Babel Security Advisory GHSA-fv7c-fp4j-7gwp
- GitHub Advisory API
- Fix Commit 5abf7bb
- GitLab Advisory for CVE-2026-44728
- Feedly CVE Tracking: CVE-2026-44728
- NVD: CVE-2023-45133 (prior Babel vulnerability)
- axios Issue #10870: Security bump for CVE-2026-44728
- MetaMask Snaps Dependency Bump
- Apache Mailing List: Dependency Bump PR
- OpenGeoscience/geojs Dependency Bump
- Babel Official Documentation: plugin-transform-modules-systemjs
- Babel GitHub Repository
- Babel Security Policy
- OpenSearch Dashboards Issue #11930



