Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Security bug about prototype pollution #805

Merged
merged 1 commit into from
Oct 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were errors just logging [Object] or something like that before this change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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