Skip to content

Commit 24f13e7

Browse files
committed
Added maxTotalMergeKeys (10000) loader option (v5 backport)
1 parent 9963d36 commit 24f13e7

4 files changed

Lines changed: 75 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88

9+
## 3.15.0 - Unreleased
10+
### Added
11+
- Added `maxTotalMergeKeys` (10000) loader option to limit the total number of
12+
keys processed by YAML merge (`<<`) across one `safeLoad()` / `safeLoadAll()`
13+
call.
14+
15+
916
## [3.14.2] - 2025-11-15
1017
### Security
1118
- Fix prototype pollution in merge (<<).

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ options:
117117
http://yaml.org/type/
118118
- `DEFAULT_FULL_SCHEMA` - all supported YAML types.
119119
- `json` _(default: false)_ - compatibility with JSON.parse behaviour. If true, then duplicate keys in a mapping will override values rather than throwing an error.
120+
- `maxTotalMergeKeys` _(default: 10000)_ - limits the total number of keys
121+
processed by merge (`<<`) across one `safeLoad()` / `safeLoadAll()` call. Set
122+
to `-1` to disable.
120123

121124
NOTE: This function **does not** understand multi-document sources, it throws
122125
exception on those.

lib/js-yaml/loader.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ function State(input, options) {
154154
this.legacy = options['legacy'] || false;
155155
this.json = options['json'] || false;
156156
this.listener = options['listener'] || null;
157+
this.maxTotalMergeKeys = typeof options['maxTotalMergeKeys'] === 'number' ? options['maxTotalMergeKeys'] : 10000;
157158

158159
this.implicitTypes = this.schema.compiledImplicit;
159160
this.typeMap = this.schema.compiledTypeMap;
@@ -163,6 +164,7 @@ function State(input, options) {
163164
this.line = 0;
164165
this.lineStart = 0;
165166
this.lineIndent = 0;
167+
this.totalMergeKeys = 0;
166168

167169
this.documents = [];
168170

@@ -293,6 +295,10 @@ function mergeMappings(state, destination, source, overridableKeys) {
293295
for (index = 0, quantity = sourceKeys.length; index < quantity; index += 1) {
294296
key = sourceKeys[index];
295297

298+
if (state.maxTotalMergeKeys !== -1 && ++state.totalMergeKeys > state.maxTotalMergeKeys) {
299+
throwError(state, 'merge keys exceeded maxTotalMergeKeys (' + state.maxTotalMergeKeys + ')');
300+
}
301+
296302
if (!_hasOwnProperty.call(destination, key)) {
297303
setProperty(destination, key, source[key]);
298304
overridableKeys[key] = true;

test/units/loader-parameters.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
var assert = require('assert');
44
var yaml = require('../..');
55

6+
function createMergeChain(count) {
7+
var lines = [ 'a0: &a0 { k0: 0 }' ];
8+
var i;
9+
10+
for (i = 1; i < count; i++) {
11+
lines.push('a' + i + ': &a' + i + ' { <<: *a' + (i - 1) + ', k' + i + ': ' + i + ' }');
12+
}
13+
14+
lines.push('b: *a' + (count - 1));
15+
return lines.join('\n') + '\n';
16+
}
17+
618
suite('loader parameters', function () {
719
var testStr = 'test: 1 \ntest: 2';
820
var expected = [ { test: 2 } ];
@@ -51,4 +63,51 @@ suite('loader parameters', function () {
5163
}, { json: true });
5264
assert.deepEqual(result, expected);
5365
});
66+
67+
test('maxTotalMergeKeys - caps total merge keys', function () {
68+
function merge(n) {
69+
var anchors = [];
70+
var refs = [];
71+
var i;
72+
73+
for (i = 0; i < n; i++) {
74+
anchors.push('- &x' + i + ' {a' + i + ': ' + i + '}');
75+
refs.push('*x' + i);
76+
}
77+
78+
return anchors.join('\n') + '\n- <<: [' + refs.join(', ') + ']\n';
79+
}
80+
81+
assert.doesNotThrow(function () {
82+
yaml.safeLoad(merge(3), { maxTotalMergeKeys: 5 });
83+
});
84+
assert.throws(function () {
85+
yaml.safeLoad(merge(3), { maxTotalMergeKeys: 2 });
86+
}, /maxTotalMergeKeys/);
87+
assert.doesNotThrow(function () {
88+
yaml.safeLoad(merge(3), { maxTotalMergeKeys: -1 });
89+
});
90+
91+
result = yaml.safeLoad(createMergeChain(150), { maxTotalMergeKeys: -1 });
92+
assert.strictEqual(Object.keys(result.b).length, 150);
93+
});
94+
95+
test('safeLoadAll - maxTotalMergeKeys is shared across all documents', function () {
96+
var src = [
97+
'---',
98+
'a: &a { k1: 1, k2: 2 }',
99+
'b: { <<: *a }',
100+
'---',
101+
'a: &a { k1: 1, k2: 2 }',
102+
'b: { <<: *a }',
103+
''
104+
].join('\n');
105+
106+
assert.doesNotThrow(function () {
107+
yaml.safeLoadAll(src, { maxTotalMergeKeys: 4 });
108+
});
109+
assert.throws(function () {
110+
yaml.safeLoadAll(src, { maxTotalMergeKeys: 3 });
111+
}, /maxTotalMergeKeys/);
112+
});
54113
});

0 commit comments

Comments
 (0)