Skip to content

Commit

Permalink
Merge pull request #805 from sumeetkakkar/fix/proto-pollution
Browse files Browse the repository at this point in the history
fix: Security bug about prototype pollution
  • Loading branch information
sumeetkakkar authored Oct 13, 2021
2 parents e0e25f7 + ddb6523 commit db6d8b9
Show file tree
Hide file tree
Showing 9 changed files with 907 additions and 741 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ tmp
examples/*/views/**/*.js
examples/*/lib/*
!examples/*/lib/main.js
.vscode
package-lock.json
10 changes: 5 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
language: node_js
node_js:
- "0.12"
- "5"
- "6"
- "12"
- "14"
- "16"
matrix:
include:
- node_js: "0.10"
- node_js: "12"
env:
- TEST="all"
- CXX=g++-4.8
Expand All @@ -14,7 +14,7 @@ notifications:
- jchan@linkedin.com
- skinast@linkedin.com
before_install:
- npm install -g npm@3
- npm install -g npm@6
before_script:
- npm update
sudo: false
Expand Down
136 changes: 99 additions & 37 deletions dist/dust-core.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*! dustjs-linkedin - v2.7.2
* http://dustjs.com/
* Copyright (c) 2015 Aleksander Williams; Released under the MIT License */
* Copyright (c) 2021 Aleksander Williams; Released under the MIT License */
(function (root, factory) {
if (typeof define === 'function' && define.amd && define.amd.dust === true) {
define('dust.core', [], factory);
Expand Down Expand Up @@ -70,6 +70,9 @@
type = type || INFO;
if (loggingLevels[type] >= loggingLevels[dust.debugLevel]) {
log('[DUST:' + type + ']', message);
if (type === ERROR && dust.debugLevel === DEBUG && message instanceof Error && message.stack) {
log('[DUST:' + type + ']', message.stack);
}
}
};

Expand Down Expand Up @@ -248,16 +251,25 @@
};

/**
* Decide somewhat-naively if something is a Thenable.
* 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."
* @param elem {*} object to inspect
* @return {Boolean} is `elem` a Thenable?
*/
dust.isThenable = function(elem) {
return elem &&
typeof elem === 'object' &&
return elem && /* Beware: `typeof null` is `object` */
(typeof elem === 'object' || typeof elem === 'function') &&
typeof elem.then === 'function';
};

/**
* Decide if an element is a function but not Thenable; it is prefereable to resolve a thenable function by its `.then` method.
* @param elem {*} target of inspection
* @return {Boolean} is `elem` a function without a `.then` property?
*/
dust.isNonThenableFunction = function(elem) {
return typeof elem === 'function' && !dust.isThenable(elem);
};

/**
* Decide very naively if something is a Stream.
* @param elem {*} object to inspect
Expand Down Expand Up @@ -319,12 +331,17 @@
this.options = options;
this.blocks = blocks;
this.templateName = templateName;
this._isContext = true;
}

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

dust.isContext = function(obj) {
return typeof obj === "object" && obj._isContext === true;
};

/**
* Factory function that creates a closure scope around a Thenable-callback.
* Returns a function that can be passed to a Thenable that will resume a
Expand All @@ -338,7 +355,8 @@
}

Context.wrap = function(context, name) {
if (context instanceof Context) {
if (dust.isContext(context)) {
context.templateName = name;
return context;
}
return new Context(context, {}, {}, null, name);
Expand Down Expand Up @@ -429,7 +447,7 @@
}
}

if (typeof ctx === 'function') {
if (dust.isNonThenableFunction(ctx)) {
fn = function() {
try {
return ctx.apply(ctxThis, arguments);
Expand Down Expand Up @@ -710,6 +728,11 @@
return this;
};

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

/**
* Like Chunk#map but additionally resolves a thenable. If the thenable succeeds the callback is invoked with
* a new chunk that can be used to asynchronously render or write to it, otherwise if the thenable is rejected
* then the error body is rendered if available, an error is logged, and the callback is never invoked.
* @param {Chunk} The current chunk to insert a new chunk
* @param thenable {Thenable} the target thenable to await
* @param context {Context} context to use to render the deferred chunk
* @param bodies {Object} may optionally contain an "error" for when the thenable is rejected
* @param callback {Function} The function that will be called with the new chunk
* @returns {Chunk} A copy of this chunk instance in order to further chain function calls on the chunk
*/
function mapThenable(chunk, thenable, context, bodies, callback) {
return chunk.map(function(asyncChunk) {
thenable.then(function(data) {
try {
callback(asyncChunk, data);
} catch (err) {
// handle errors the same way Chunk#map would. This logic is only here since the thenable defers
// logic such that the try / catch in Chunk#map would not capture it.
dust.log(err, ERROR);
asyncChunk.setError(err);
}
}, function(err) {
dust.log('Unhandled promise rejection in `' + context.getTemplateName() + '`', INFO);
asyncChunk.renderError(err, context, bodies).end();
});
});
}

Chunk.prototype.tap = function(tap) {
var taps = this.taps;

Expand All @@ -746,7 +798,7 @@
};

Chunk.prototype.reference = function(elem, context, auto, filters) {
if (typeof elem === 'function') {
if (dust.isNonThenableFunction(elem)) {
elem = elem.apply(context.current(), [this, context, null, {auto: auto, filters: filters}]);
if (elem instanceof Chunk) {
return elem;
Expand All @@ -771,7 +823,7 @@
chunk = this,
i, len, head;

if (typeof elem === 'function' && !dust.isTemplateFn(elem)) {
if (dust.isNonThenableFunction(elem) && !dust.isTemplateFn(elem)) {
try {
elem = elem.apply(context.current(), [this, context, bodies, params]);
} catch(err) {
Expand Down Expand Up @@ -846,6 +898,12 @@
var body = bodies.block,
skip = bodies['else'];

if (dust.isThenable(elem)) {
return mapThenable(this, elem, context, bodies, function(chunk, data) {
chunk.exists(data, context, bodies).end();
});
}

if (!dust.isEmpty(elem)) {
if (body) {
return body(this, context);
Expand All @@ -861,6 +919,12 @@
var body = bodies.block,
skip = bodies['else'];

if (dust.isThenable(elem)) {
return mapThenable(this, elem, context, bodies, function(chunk, data) {
chunk.notexists(data, context, bodies).end();
});
}

if (dust.isEmpty(elem)) {
if (body) {
return body(this, context);
Expand Down Expand Up @@ -901,11 +965,9 @@
// The eventual result of evaluating `elem` is a partial name
// Load the partial after getting its name and end the async chunk
return this.capture(elem, context, function(name, chunk) {
partialContext.templateName = name;
load(name, chunk, partialContext).end();
});
} else {
partialContext.templateName = elem;
return load(elem, this, partialContext);
}
};
Expand Down Expand Up @@ -957,27 +1019,31 @@
* @return {Chunk}
*/
Chunk.prototype.await = function(thenable, context, bodies, auto, filters) {
return this.map(function(chunk) {
thenable.then(function(data) {
if (bodies) {
chunk = chunk.section(data, context, bodies);
} else {
// Actually a reference. Self-closing sections don't render
chunk = chunk.reference(data, context, auto, filters);
}
chunk.end();
}, function(err) {
var errorBody = bodies && bodies.error;
if(errorBody) {
chunk.render(errorBody, context.push(err)).end();
} else {
dust.log('Unhandled promise rejection in `' + context.getTemplateName() + '`', INFO);
chunk.end();
}
});
return mapThenable(this, thenable, context, bodies, function(chunk, data) {
if (bodies) {
chunk.section(data, context, bodies).end();
} else {
// Actually a reference. Self-closing sections don't render
chunk.reference(data, context, auto, filters).end();
}
});
};

/**
* Render an error body if available
* @param err {Error} error that occurred
* @param context {Context} context to use to render the error
* @param bodies {Object} may optionally contain an "error" which will be rendered
* @return {Chunk}
*/
Chunk.prototype.renderError = function(err, context, bodies) {
var errorBody = bodies && bodies.error;
if (errorBody) {
return this.render(errorBody, context.push(err));
}
return this;
};

/**
* Reserve a chunk to be evaluated with the contents of a streamable.
* Currently an error event will bomb out the stream. Once an error
Expand All @@ -989,8 +1055,7 @@
* @return {Chunk}
*/
Chunk.prototype.stream = function(stream, context, bodies, auto, filters) {
var body = bodies && bodies.block,
errorBody = bodies && bodies.error;
var body = bodies && bodies.block;
return this.map(function(chunk) {
var ended = false;
stream
Expand All @@ -1012,11 +1077,8 @@
if(ended) {
return;
}
if(errorBody) {
chunk.render(errorBody, context.push(err));
} else {
dust.log('Unhandled stream error in `' + context.getTemplateName() + '`', INFO);
}
chunk.renderError(err, context, bodies);
dust.log('Unhandled stream error in `' + context.getTemplateName() + '`', INFO);
if(!ended) {
ended = true;
chunk.end();
Expand Down Expand Up @@ -1143,8 +1205,8 @@
if (typeof define === "function" && define.amd && define.amd.dust === true) {
define(["require", "dust.core"], function(require, dust) {
dust.onLoad = function(name, cb) {
require([name], function() {
cb();
require([name], function(tmpl) {
cb(null, tmpl);
});
};
return dust;
Expand Down
Loading

0 comments on commit db6d8b9

Please sign in to comment.