Skip to content

Commit db6d8b9

Browse files
authored
Merge pull request #805 from sumeetkakkar/fix/proto-pollution
fix: Security bug about prototype pollution
2 parents e0e25f7 + ddb6523 commit db6d8b9

9 files changed

+907
-741
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ tmp
66
examples/*/views/**/*.js
77
examples/*/lib/*
88
!examples/*/lib/main.js
9+
.vscode
10+
package-lock.json

.travis.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
language: node_js
22
node_js:
3-
- "0.12"
4-
- "5"
5-
- "6"
3+
- "12"
4+
- "14"
5+
- "16"
66
matrix:
77
include:
8-
- node_js: "0.10"
8+
- node_js: "12"
99
env:
1010
- TEST="all"
1111
- CXX=g++-4.8
@@ -14,7 +14,7 @@ notifications:
1414
1515
1616
before_install:
17-
- npm install -g npm@3
17+
- npm install -g npm@6
1818
before_script:
1919
- npm update
2020
sudo: false

dist/dust-core.js

+99-37
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*! dustjs-linkedin - v2.7.2
22
* http://dustjs.com/
3-
* Copyright (c) 2015 Aleksander Williams; Released under the MIT License */
3+
* Copyright (c) 2021 Aleksander Williams; Released under the MIT License */
44
(function (root, factory) {
55
if (typeof define === 'function' && define.amd && define.amd.dust === true) {
66
define('dust.core', [], factory);
@@ -70,6 +70,9 @@
7070
type = type || INFO;
7171
if (loggingLevels[type] >= loggingLevels[dust.debugLevel]) {
7272
log('[DUST:' + type + ']', message);
73+
if (type === ERROR && dust.debugLevel === DEBUG && message instanceof Error && message.stack) {
74+
log('[DUST:' + type + ']', message.stack);
75+
}
7376
}
7477
};
7578

@@ -248,16 +251,25 @@
248251
};
249252

250253
/**
251-
* Decide somewhat-naively if something is a Thenable.
254+
* Decide somewhat-naively if something is a Thenable. Matches Promises A+ Spec, section 1.2 “thenable” is an object or function that defines a then method."
252255
* @param elem {*} object to inspect
253256
* @return {Boolean} is `elem` a Thenable?
254257
*/
255258
dust.isThenable = function(elem) {
256-
return elem &&
257-
typeof elem === 'object' &&
259+
return elem && /* Beware: `typeof null` is `object` */
260+
(typeof elem === 'object' || typeof elem === 'function') &&
258261
typeof elem.then === 'function';
259262
};
260263

264+
/**
265+
* Decide if an element is a function but not Thenable; it is prefereable to resolve a thenable function by its `.then` method.
266+
* @param elem {*} target of inspection
267+
* @return {Boolean} is `elem` a function without a `.then` property?
268+
*/
269+
dust.isNonThenableFunction = function(elem) {
270+
return typeof elem === 'function' && !dust.isThenable(elem);
271+
};
272+
261273
/**
262274
* Decide very naively if something is a Stream.
263275
* @param elem {*} object to inspect
@@ -319,12 +331,17 @@
319331
this.options = options;
320332
this.blocks = blocks;
321333
this.templateName = templateName;
334+
this._isContext = true;
322335
}
323336

324337
dust.makeBase = dust.context = function(global, options) {
325338
return new Context(undefined, global, options);
326339
};
327340

341+
dust.isContext = function(obj) {
342+
return typeof obj === "object" && obj._isContext === true;
343+
};
344+
328345
/**
329346
* Factory function that creates a closure scope around a Thenable-callback.
330347
* Returns a function that can be passed to a Thenable that will resume a
@@ -338,7 +355,8 @@
338355
}
339356

340357
Context.wrap = function(context, name) {
341-
if (context instanceof Context) {
358+
if (dust.isContext(context)) {
359+
context.templateName = name;
342360
return context;
343361
}
344362
return new Context(context, {}, {}, null, name);
@@ -429,7 +447,7 @@
429447
}
430448
}
431449

432-
if (typeof ctx === 'function') {
450+
if (dust.isNonThenableFunction(ctx)) {
433451
fn = function() {
434452
try {
435453
return ctx.apply(ctxThis, arguments);
@@ -710,6 +728,11 @@
710728
return this;
711729
};
712730

731+
/**
732+
* Inserts a new chunk that can be used to asynchronously render or write to it
733+
* @param callback {Function} The function that will be called with the new chunk
734+
* @returns {Chunk} A copy of this chunk instance in order to further chain function calls on the chunk
735+
*/
713736
Chunk.prototype.map = function(callback) {
714737
var cursor = new Chunk(this.root, this.next, this.taps),
715738
branch = new Chunk(this.root, cursor, this.taps);
@@ -725,6 +748,35 @@
725748
return cursor;
726749
};
727750

751+
/**
752+
* Like Chunk#map but additionally resolves a thenable. If the thenable succeeds the callback is invoked with
753+
* a new chunk that can be used to asynchronously render or write to it, otherwise if the thenable is rejected
754+
* then the error body is rendered if available, an error is logged, and the callback is never invoked.
755+
* @param {Chunk} The current chunk to insert a new chunk
756+
* @param thenable {Thenable} the target thenable to await
757+
* @param context {Context} context to use to render the deferred chunk
758+
* @param bodies {Object} may optionally contain an "error" for when the thenable is rejected
759+
* @param callback {Function} The function that will be called with the new chunk
760+
* @returns {Chunk} A copy of this chunk instance in order to further chain function calls on the chunk
761+
*/
762+
function mapThenable(chunk, thenable, context, bodies, callback) {
763+
return chunk.map(function(asyncChunk) {
764+
thenable.then(function(data) {
765+
try {
766+
callback(asyncChunk, data);
767+
} catch (err) {
768+
// handle errors the same way Chunk#map would. This logic is only here since the thenable defers
769+
// logic such that the try / catch in Chunk#map would not capture it.
770+
dust.log(err, ERROR);
771+
asyncChunk.setError(err);
772+
}
773+
}, function(err) {
774+
dust.log('Unhandled promise rejection in `' + context.getTemplateName() + '`', INFO);
775+
asyncChunk.renderError(err, context, bodies).end();
776+
});
777+
});
778+
}
779+
728780
Chunk.prototype.tap = function(tap) {
729781
var taps = this.taps;
730782

@@ -746,7 +798,7 @@
746798
};
747799

748800
Chunk.prototype.reference = function(elem, context, auto, filters) {
749-
if (typeof elem === 'function') {
801+
if (dust.isNonThenableFunction(elem)) {
750802
elem = elem.apply(context.current(), [this, context, null, {auto: auto, filters: filters}]);
751803
if (elem instanceof Chunk) {
752804
return elem;
@@ -771,7 +823,7 @@
771823
chunk = this,
772824
i, len, head;
773825

774-
if (typeof elem === 'function' && !dust.isTemplateFn(elem)) {
826+
if (dust.isNonThenableFunction(elem) && !dust.isTemplateFn(elem)) {
775827
try {
776828
elem = elem.apply(context.current(), [this, context, bodies, params]);
777829
} catch(err) {
@@ -846,6 +898,12 @@
846898
var body = bodies.block,
847899
skip = bodies['else'];
848900

901+
if (dust.isThenable(elem)) {
902+
return mapThenable(this, elem, context, bodies, function(chunk, data) {
903+
chunk.exists(data, context, bodies).end();
904+
});
905+
}
906+
849907
if (!dust.isEmpty(elem)) {
850908
if (body) {
851909
return body(this, context);
@@ -861,6 +919,12 @@
861919
var body = bodies.block,
862920
skip = bodies['else'];
863921

922+
if (dust.isThenable(elem)) {
923+
return mapThenable(this, elem, context, bodies, function(chunk, data) {
924+
chunk.notexists(data, context, bodies).end();
925+
});
926+
}
927+
864928
if (dust.isEmpty(elem)) {
865929
if (body) {
866930
return body(this, context);
@@ -901,11 +965,9 @@
901965
// The eventual result of evaluating `elem` is a partial name
902966
// Load the partial after getting its name and end the async chunk
903967
return this.capture(elem, context, function(name, chunk) {
904-
partialContext.templateName = name;
905968
load(name, chunk, partialContext).end();
906969
});
907970
} else {
908-
partialContext.templateName = elem;
909971
return load(elem, this, partialContext);
910972
}
911973
};
@@ -957,27 +1019,31 @@
9571019
* @return {Chunk}
9581020
*/
9591021
Chunk.prototype.await = function(thenable, context, bodies, auto, filters) {
960-
return this.map(function(chunk) {
961-
thenable.then(function(data) {
962-
if (bodies) {
963-
chunk = chunk.section(data, context, bodies);
964-
} else {
965-
// Actually a reference. Self-closing sections don't render
966-
chunk = chunk.reference(data, context, auto, filters);
967-
}
968-
chunk.end();
969-
}, function(err) {
970-
var errorBody = bodies && bodies.error;
971-
if(errorBody) {
972-
chunk.render(errorBody, context.push(err)).end();
973-
} else {
974-
dust.log('Unhandled promise rejection in `' + context.getTemplateName() + '`', INFO);
975-
chunk.end();
976-
}
977-
});
1022+
return mapThenable(this, thenable, context, bodies, function(chunk, data) {
1023+
if (bodies) {
1024+
chunk.section(data, context, bodies).end();
1025+
} else {
1026+
// Actually a reference. Self-closing sections don't render
1027+
chunk.reference(data, context, auto, filters).end();
1028+
}
9781029
});
9791030
};
9801031

1032+
/**
1033+
* Render an error body if available
1034+
* @param err {Error} error that occurred
1035+
* @param context {Context} context to use to render the error
1036+
* @param bodies {Object} may optionally contain an "error" which will be rendered
1037+
* @return {Chunk}
1038+
*/
1039+
Chunk.prototype.renderError = function(err, context, bodies) {
1040+
var errorBody = bodies && bodies.error;
1041+
if (errorBody) {
1042+
return this.render(errorBody, context.push(err));
1043+
}
1044+
return this;
1045+
};
1046+
9811047
/**
9821048
* Reserve a chunk to be evaluated with the contents of a streamable.
9831049
* Currently an error event will bomb out the stream. Once an error
@@ -989,8 +1055,7 @@
9891055
* @return {Chunk}
9901056
*/
9911057
Chunk.prototype.stream = function(stream, context, bodies, auto, filters) {
992-
var body = bodies && bodies.block,
993-
errorBody = bodies && bodies.error;
1058+
var body = bodies && bodies.block;
9941059
return this.map(function(chunk) {
9951060
var ended = false;
9961061
stream
@@ -1012,11 +1077,8 @@
10121077
if(ended) {
10131078
return;
10141079
}
1015-
if(errorBody) {
1016-
chunk.render(errorBody, context.push(err));
1017-
} else {
1018-
dust.log('Unhandled stream error in `' + context.getTemplateName() + '`', INFO);
1019-
}
1080+
chunk.renderError(err, context, bodies);
1081+
dust.log('Unhandled stream error in `' + context.getTemplateName() + '`', INFO);
10201082
if(!ended) {
10211083
ended = true;
10221084
chunk.end();
@@ -1143,8 +1205,8 @@
11431205
if (typeof define === "function" && define.amd && define.amd.dust === true) {
11441206
define(["require", "dust.core"], function(require, dust) {
11451207
dust.onLoad = function(name, cb) {
1146-
require([name], function() {
1147-
cb();
1208+
require([name], function(tmpl) {
1209+
cb(null, tmpl);
11481210
});
11491211
};
11501212
return dust;

0 commit comments

Comments
 (0)