Skip to content

Commit e18afbf

Browse files
committed
Fix possible code execution in (already unsafe) load()
... when object with executable toString() property is used as a map key
1 parent 9d4ce5e commit e18afbf

File tree

6 files changed

+58
-2
lines changed

6 files changed

+58
-2
lines changed

lib/js-yaml/loader.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ var PATTERN_TAG_HANDLE = /^(?:!|!!|![a-z\-]+!)$/i;
3030
var PATTERN_TAG_URI = /^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i;
3131

3232

33+
function _class(obj) { return Object.prototype.toString.call(obj); }
34+
3335
function is_EOL(c) {
3436
return (c === 0x0A/* LF */) || (c === 0x0D/* CR */);
3537
}
@@ -287,16 +289,29 @@ function storeMappingPair(state, _result, overridableKeys, keyTag, keyNode, valu
287289

288290
// The output is a plain object here, so keys can only be strings.
289291
// We need to convert keyNode to a string, but doing so can hang the process
290-
// (deeply nested arrays that explode exponentially using aliases) or execute
291-
// code via toString.
292+
// (deeply nested arrays that explode exponentially using aliases).
292293
if (Array.isArray(keyNode)) {
294+
keyNode = Array.prototype.slice.call(keyNode);
295+
293296
for (index = 0, quantity = keyNode.length; index < quantity; index += 1) {
294297
if (Array.isArray(keyNode[index])) {
295298
throwError(state, 'nested arrays are not supported inside keys');
296299
}
300+
301+
if (typeof keyNode === 'object' && _class(keyNode[index]) === '[object Object]') {
302+
keyNode[index] = '[object Object]';
303+
}
297304
}
298305
}
299306

307+
// Avoid code execution in load() via toString property
308+
// (still use its own toString for arrays, timestamps,
309+
// and whatever user schema extensions happen to have @@toStringTag)
310+
if (typeof keyNode === 'object' && _class(keyNode) === '[object Object]') {
311+
keyNode = '[object Object]';
312+
}
313+
314+
300315
keyNode = String(keyNode);
301316

302317
if (_result === null) {

test/issues/0480-date.yml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ !<tag:yaml.org,2002:timestamp> '2019-04-05T12:00:43.467Z': 123 }

test/issues/0480-fn-array.yml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
? [
2+
123,
3+
{ toString: !<tag:yaml.org,2002:js/function> 'function (){throw new Error("code execution")}' }
4+
] : key

test/issues/0480-fn.yml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ toString: !<tag:yaml.org,2002:js/function> 'function (){throw new Error("code execution")}' } : key

test/issues/0480-fn2.yml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ __proto__: { toString: !<tag:yaml.org,2002:js/function> 'function(){throw new Error("code execution")}' } } : key

test/issues/0480.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
4+
var assert = require('assert');
5+
var yaml = require('../../');
6+
var readFileSync = require('fs').readFileSync;
7+
8+
9+
test('Should not execute code when object with toString property is used as a key', function () {
10+
var data = yaml.load(readFileSync(require('path').join(__dirname, '/0480-fn.yml'), 'utf8'));
11+
12+
assert.deepEqual(data, { '[object Object]': 'key' });
13+
});
14+
15+
test('Should not execute code when object with __proto__ property is used as a key', function () {
16+
var data = yaml.load(readFileSync(require('path').join(__dirname, '/0480-fn2.yml'), 'utf8'));
17+
18+
assert.deepEqual(data, { '[object Object]': 'key' });
19+
});
20+
21+
test('Should not execute code when object inside array is used as a key', function () {
22+
var data = yaml.load(readFileSync(require('path').join(__dirname, '/0480-fn-array.yml'), 'utf8'));
23+
24+
assert.deepEqual(data, { '123,[object Object]': 'key' });
25+
});
26+
27+
// this test does not guarantee in any way proper handling of date objects,
28+
// it just keeps old behavior whenever possible
29+
test('Should leave non-plain objects as is', function () {
30+
var data = yaml.load(readFileSync(require('path').join(__dirname, '/0480-date.yml'), 'utf8'));
31+
32+
assert.deepEqual(Object.keys(data).length, 1);
33+
assert(/2019/.test(Object.keys(data)[0]));
34+
});

0 commit comments

Comments
 (0)