Skip to content

chore(deps): update dependency js-yaml to v5 [security] - #1961

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate-npm-js-yaml-vulnerability
Open

renovate[bot] wants to merge 1 commit into
mainfrom
renovate-npm-js-yaml-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
js-yaml 4.3.2 → 5.4.1 age confidence

JS-YAML: Quadratic-complexity DoS in merge key handling via repeated aliases

CVE-2026-53550 / GHSA-h67p-54hq-rp68

More information

Details

Summary

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 is tag:yaml.org,2002:merge.
  • For each element, it calls mergeMappings(...).
  • mergeMappings(...) computes Object.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);
  }
}

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


js-yaml: YAML merge-key chains can force quadratic CPU consumption

CVE-2026-59869 / GHSA-52cp-r559-cp3m

More information

Details

Impact

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.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


JS-YAML: Quadratic CPU consumption in !!omap resolution (3.x and 4.x) — CVE-2026-59870 fix not backported

GHSA-5p4m-2wfm-xmqj

More information

Details

Quadratic CPU consumption in !!omap resolution (js-yaml 3.x and 4.x)
Summary

resolveYamlOmap() enforces key uniqueness for !!omap sequences with a linear
scan (objectKeys.indexOf(...)) inside the per-element loop, making resolution
O(n²) in the number of entries. A modestly sized YAML document therefore
consumes disproportionate CPU inside yaml.load(), giving a denial of service
against any consumer that parses untrusted YAML.

!!omap is registered in the default schema
(lib/schema/default.js → require('../type/omap')), so a plain
yaml.load(untrustedInput) with no options is affected — no custom schema or
non-default configuration is required.

This is the same weakness as CVE-2026-59870 / GHSA-724g-mxrg-4qvm, which was
fixed in the 5.x line in 5.2.1. That fix was never backported: both currently
maintained legacy lines still carry the original implementation.

Affected versions
Line Latest tested Status
3.x 3.15.0 Affected — objectKeys.indexOf(pairKey) at lib/type/omap.js:29
4.x 4.3.0 Affected — objectKeys.indexOf(pairKey) at lib/type/omap.js:30
5.x 5.2.2 Not affected — fixed in 5.2.1 (uses a Set)

Both figures are the newest release of each line at the time of writing, so
this is not a "you are on an old version" issue.

Details

lib/type/omap.js (js-yaml 4.3.0):

if (objectKeys.indexOf(pairKey) === -1) objectKeys.push(pairKey)
else return false

objectKeys grows by one element per entry, and Array.prototype.indexOf is a
linear scan, so resolving an n-entry !!omap performs roughly
1 + 2 + … + n comparisons — quadratic in n. The work happens synchronously
inside yaml.load(), blocking the event loop for its whole duration.

The 5.x line already solves exactly this by tracking seen keys in a Set
(src/tag/sequence/omap.ts):

if (carrier.seen.has(key)) return 'duplicate key in ordered map'
carrier.seen.add(key)
Proof of concept
// poc.js  —  node poc.js
const yaml = require('js-yaml');
const doc = n => '!!omap\n' + Array.from({length: n}, (_, i) => `- k${i}: ${i}`).join('\n') + '\n';

for (const n of [10000, 20000, 40000, 80000]) {
  const d = doc(n), t = Date.now();
  yaml.load(d);                      // default schema, no options
  console.log(`n=${n} bytes=${d.length} load=${Date.now() - t}ms`);
}
Measured (node v20.20.2, default heap, no flags)

js-yaml 4.3.0

n=10000  bytes=137787   load=54ms
n=20000  bytes=297787   load=169ms
n=40000  bytes=617787   load=646ms
n=80000  bytes=1257787  load=2607ms

js-yaml 3.15.0

n=10000  bytes=137787   load=53ms
n=20000  bytes=297787   load=166ms
n=40000  bytes=617787   load=641ms
n=80000  bytes=1257787  load=2567ms

Runtime grows by a factor of ~4 for each doubling of n, which is the
signature of O(n²) (linear growth would be ~2×).

Scaling further: a 2.48 MB document with 150,000 entries blocked
yaml.load() for 10.8 seconds.

Impact

Any service that parses attacker-influenced YAML with js-yaml 3.x or 4.x can be
stalled with a small input. Because the loop is synchronous, a single request
blocks the Node.js event loop and stalls every other request in the process —
so the amplification is per-process, not just per-request.

Suggested severity: consistent with CVE-2026-59870 (the same weakness in
5.x), i.e. Availability-only impact, network attack vector, no privileges or
user interaction required.

Suggested fix

Mirror the 5.x fix — replace the linear scan with a Set:

// lib/type/omap.js
const seen = new Set()
// ...
if (seen.has(pairKey)) return false
seen.add(pairKey)

This preserves the existing duplicate-key rejection semantics exactly while
making resolution O(n). A maxOmapLength-style cap would also work, but the
Set matches what 5.x already ships and requires no new option.

References
  • CVE-2026-59870 / GHSA-724g-mxrg-4qvm — same weakness in 5.0.0–5.2.0, fixed in 5.2.1
  • lib/type/omap.js (3.x, 4.x) — the affected resolver
  • lib/schema/default.js — registers !!omap in the default schema
Discovery

Found by an automated static-analysis and executed-proof-of-concept scanner run
against js-yaml 4.2.0, then manually verified against 3.15.0 and 4.3.0 by
executing the proof of concept above. All timings in this report were measured
on the current releases of each line, not on the version originally scanned.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


js-yaml: maxTotalMergeKeys does not limit CPU use for empty merge sources

CVE-2026-84375 / GHSA-2883-xcg3-v3hh

More information

Details

Summary

maxTotalMergeKeys does not count empty mappings. An attacker can repeatedly merge a large sequence of them and consume significant CPU without reaching the configured limit.

Example
arr: &arr [{}, {}, {}, ...] # N empty mappings
targets:
  - <<: *arr                # repeated K times

For every target, the loader iterates all N elements of arr. This results in O(N * K) work while totalMergeKeys remains unchanged.

PoC
import { performance } from 'node:perf_hooks'
import { load, YAML11_SCHEMA } from 'js-yaml'

const n = 20000

const src =
  'arr: &arr [' + '{},'.repeat(n).slice(0, -1) + ']\n' +
  'targets:\n' +
  '  - <<: *arr\n'.repeat(n)

const started = performance.now()

load(src, { schema: YAML11_SCHEMA })

console.log(`${(performance.now() - started).toFixed(1)} ms`)

Observed results:

N YAML size Time
800 ~13 KB ~20 ms
3200 ~50 KB ~180 ms
20000 ~500 KB ~13 s
Impact

An attacker can submit a relatively small YAML document that causes prolonged CPU consumption despite the default maxTotalMergeKeys limit.

Fix

Count each merge-source mapping as one budget unit, in addition to counting its keys.

Difference with v5

In v3 & v4, merge is enabled by default. So, the severity score is higher.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


js-yaml: maxTotalMergeKeys does not limit CPU use for empty merge sources

GHSA-r3ph-w7gj-g6xm

More information

Details

Summary

maxTotalMergeKeys does not count empty mappings. An attacker can repeatedly merge a large sequence of them and consume significant CPU without reaching the configured limit.

Example
arr: &arr [{}, {}, {}, ...] # N empty mappings
targets:
  - <<: *arr                # repeated K times

For every target, the loader iterates all N elements of arr. This results in O(N * K) work while totalMergeKeys remains unchanged.

PoC
import { performance } from 'node:perf_hooks'
import { load, YAML11_SCHEMA } from 'js-yaml'

const n = 20000

const src =
  'arr: &arr [' + '{},'.repeat(n).slice(0, -1) + ']\n' +
  'targets:\n' +
  '  - <<: *arr\n'.repeat(n)

const started = performance.now()

load(src, { schema: YAML11_SCHEMA })

console.log(`${(performance.now() - started).toFixed(1)} ms`)

Observed results:

N YAML size Time
800 ~13 KB ~20 ms
3200 ~50 KB ~180 ms
20000 ~500 KB ~13 s
Impact

When merge keys are enabled, an attacker can submit a relatively small YAML document that causes prolonged CPU consumption despite the default maxTotalMergeKeys limit.

Fix

Count every merge source mapping as one budget unit in addition to counting its keys.

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

nodeca/js-yaml (js-yaml)

v5.4.1

Compare Source

v5.4.0

Compare Source

Added
  • Added the scalarStyleRules dumper option to customize string formatting.
    See Scalar styling for details.
Changed
  • [breaking] Flattened the low-level AST node style representation. Scalar
    and collection nodes now use SCALAR_STYLE and COLLECTION_STYLE values;
    explicit tags use the separate tagged property. Alias nodes now contain
    only kind and anchor. This only affects code that directly constructs or
    edits AST nodes.
  • [breaking] The sortKeys option was rewritten using AST mutation to avoid
    side effects.
  • Reworked scalar style selection. This can change formatting without changing
    loaded values; in particular, whitespace-only strings are now double-quoted.
Fixed
  • Accept a byte order mark at the start of each document in a stream, #​791.
  • Produce valid flow mappings with quoteFlowKeys and flowSkipColonSpace,
    including alias and property-only keys, #​786.
  • Preserve empty scalar items when converting block sequences to flow style.
  • Do not apply the 1024-character simple-key limit to flow mapping keys.
  • Count Unicode code points, rather than UTF-16 code units, for the
    1024-character simple-key limit.
  • Add an explicit document-end marker after keep-chomped block scalars when
    needed to preserve trailing newlines.

v5.3.0

Compare Source

This release focuses on reworking the documentation and making small
architectural improvements before moving forward.

Added
  • Added completely new documentation.
  • Exported DUMP_SCHEMA, the default schema used by the dumper.
  • Added YAMLException.throwAt() for throwing an error at a source position.
Changed
  • Changed flat constant exports to grouped exports: EVENT_ID, SCALAR_STYLE,
    COLLECTION_STYLE, and CHOMPING_MODE, along with their value types. The old
    exports are still preserved, but deprecated.
  • Made identify mandatory for custom tag definitions. Use
    identify: () => false for load-only tags.
Deprecated
  • Deprecated flat constant exports. Use grouped ones instead.
Removed
  • Removed the MERGE_KEY export (not used anymore after last fixes).
Fixed
  • Validate << sequence items at merge time, so aliased merge sources are
    checked too.
  • Resolve << outside of a mapping key as the plain string '<<', matching
    v4, instead of leaking an internal symbol into the result.

v5.2.3

Compare Source

Fixed
  • Prevent prototype fallback when resolving tags and mapping entries, #​782.
  • Resolve !!timestamp years 0000-0099 correctly, #​775.
  • Preserve implicit null mapping values before document markers and reject
    unpaired mapping event streams, #​784.
  • Preserve folded scalar values with tab-indented lines when round-tripping a
    parsed AST through present(); dump() and loading are unaffected, #​780.

v5.2.2

Compare Source

Fixed
  • Quote flow scalars where a colon precedes a flow indicator, #​773.
Security
  • Avoid exponential parsing time for nested flow sequence pairs.

v5.2.1

Compare Source

Fixed
  • Add Map support to !!omap (should work when realMapTag used)
Security
  • Remove quadratic complexity from !!omap addItem. Regression from v5
    (usually not critical, because YAML11_SCHEMA is not default anymore).

v5.2.0

Compare Source

Added
  • Added maxTotalMergeKeys (10000) loader option to limit the total number of
    keys processed by YAML merge (<<) across one load() / loadAll() call.
  • Added maxAliases (-1) loader option to limit the number of YAML aliases per
    document.
Removed
  • maxMergeSeqLength replaced with maxTotalMergeKeys for limiting YAML merge
    processing.
Fixed
  • Round-trip of integers with exponential form (>= 1e21)

v5.1.0

Compare Source

Added
  • Collection tags can finalize an incrementally populated carrier into a
    different result value.
Changed
  • [breaking] quoteStyle now selects the preferred quote style; use the
    restored forceQuotes option to force quoting non-key strings.

v5.0.0

Compare Source

Added
  • Added named exports for schemas, tags, parser events and AST utilities.
  • Reworked JSON_SCHEMA and CORE_SCHEMA with spec-compliant scalar resolution
    rules, and added YAML11_SCHEMA.
  • Added realMapTag for lossless mappings with non-string and complex keys.
    Object-based mappings now reject complex keys instead of stringifying them.
  • Added dump() transform option for changing the generated AST before
    rendering.
  • Added dump() options seqInlineFirst, flowBracketPadding,
    flowSkipCommaSpace, flowSkipColonSpace, quoteFlowKeys, quoteStyle and
    tagBeforeAnchor.
  • Added formal data layers (events and AST) for modular data pipelines.
    • Added low-level parser (to events), presenter and visitor APIs.
  • Added the YAML Test Suite to the
    test set.
Changed
  • See the migration guide for upgrade notes.
  • Rewritten in TypeScript and reorganized the public API around flat named
    exports.
  • Reduced the set of exported schemas:
    • YAML 1.2 schemas: CORE_SCHEMA (loader default), JSON_SCHEMA,
      FAILSAFE_SCHEMA.
    • YAML11_SCHEMA, a combination of all YAML 1.1 tags (YAML 1.1 does not
      specify a schema, only "types").
  • load/dump default behaviour is now specified exactly via schemas:
    • load uses CORE_SCHEMA, without !!merge by default.
    • dump uses YAML11_SCHEMA + CORE_SCHEMA for the quoting check, to
      guarantee backward compatibility by default.
  • !!set is now loaded as a JavaScript Set.
  • Replaced the Type API with a tags API. Similar, but more precise and
    simpler. See examples for details. Tags can be defined via
    defineScalarTag(), defineSequenceTag() and defineMappingTag(), or as a
    spread + override of an existing tag.
  • Renamed Schema.extend() to Schema.withTags().
  • Expanded YAML 1.2 conformance and improved handling of directives, document
    markers, block keys, multiline scalars, tag syntax and other things.
  • load() now throws on empty input instead of returning undefined.
  • Moved browser builds to the js-yaml/browser export.
  • Deprecated the loadAll signature with an iterator (still works, but is a
    candidate for removal).
Removed
  • Removed deprecated safeLoad(), safeLoadAll() and safeDump() exports.
  • Removed DEFAULT_SCHEMA and the nested types export.
  • Removed loader options onWarning, legacy and listener.
  • Removed dumper options styles, replacer, noCompatMode, condenseFlow,
    quotingType and forceQuotes. Renamed noArrayIndent to seqNoIndent.
    Formatting and representation are now configured through presenter options,
    schemas and tag definitions. See migration guide on how to replace.
  • Removed support for importing internal files from lib/.

v4.3.2

Compare Source

v4.3.1

Compare Source

v4.3.0

Compare Source


Configuration

📅 Schedule: (in timezone Europe/Zurich)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@github-actions

Copy link
Copy Markdown

This PR will trigger no release when merged.

@renovate
renovate Bot force-pushed the renovate-npm-js-yaml-vulnerability branch from 8091319 to 6834d03 Compare October 1, 2026 13:34

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants