57

I am using a Nodejs backend with server-side rendering using handlebars. After reading a doc array of objects from handlebars, which contains key "content" and "from". However when I try to use #each to loop through the array of objects, the error "Handlebars: Access has been denied to resolve the property "from" because it is not an "own property" of its parent" appears.

I've tried to console.log() the data that I've fetched in the doc array and everything seems fine.

For some perspective, this is the mongoose query,
I've added the object doc as a key inside the res.render arguments.

Confession.find()
  .sort({date: -1})
  .then(function(doc){
    for(var i=0; i < doc.length; i++){
      //Check whether sender is anonymous
      if (doc[i].from === "" || doc[i].from == null){
        doc[i].from = "Anonymous";
      }

      //Add an extra JSON Field for formatted date
      doc[i].formattedDate = formatTime(doc[i].date);
    }
    res.render('index', {title: 'Confession Box', success:req.session.success, errors: req.session.errors, confession: doc});
    req.session.errors = null;
    req.session.success = null;
  });

This is the portion of .hbs file I am trying to loop through:

 {{#each confession}}
    <div class="uk-card uk-card-default uk-card-body uk-margin uk-align-center uk-width-1-2@m" >
        <div class="uk-text-bold">Message: </div>
        <div>{{this.content}}</div>
        <div>From: {{this.from}}</div>
        <div>Posted: {{this.formattedDate}}</div>
    </div>
    {{/each}}
Jason Novak
  • 133
  • 5
Lee Boon Kong
  • 1,007
  • 1
  • 8
  • 17

16 Answers16

90

If using mongoose, this issue can be solved by using .lean() to get a json object (instead of a mongoose one):

dbName.find({}).lean()
  // execute query
  .exec(function(error, body) {
     //Some code
  });
Billeh
  • 1,257
  • 7
  • 6
  • 9
    god bless you! LIFE SAVER! – Nick Thenick Apr 09 '20 at 19:33
  • 4
    Wow, this was AMAZING! – Alan Daniel Oct 31 '20 at 05:21
  • 4
    Thank you. So much simpler and solved my problem. – Georges Dec 02 '20 at 17:32
  • 2
    Thanks, but you explain us the root of the problem? – Maverick Feb 08 '21 at 17:05
  • 2
    yea...it's working.. Anyone plz explain "lean()" function in detail – Nithin mm May 19 '21 at 15:48
  • 2
    From mongoose.js.com : 'The lean option tells Mongoose to skip hydrating the result documents. This makes queries faster and less memory intensive, but the result documents are plain old JavaScript objects (POJOs), not Mongoose documents.' . . . Essentially, instead of returning a Mongoose document, it returns the data in JSON. In my case at least, it was just this formatting issue. – Billeh Jan 16 '22 at 17:37
  • Yep working fine, ```UserModel.find({}) .lean() .then((userList) => { res.render("pages/user/user-list", { userList: userList, }); });``` – coder618 Apr 01 '22 at 06:09
  • A complementary option, if you are working with a single Mongoose object instead of a list, is to use the `toObject()` method. See https://mongoosejs.com/docs/api.html#document_Document-toObject – Maxime Pacary Oct 29 '22 at 08:51
43

i solve this issue by installing a dev dependency for handlebars

npm i -D handlebars@4.5.0

phemieny7
  • 803
  • 7
  • 21
  • 1
    Wow this worked, Why is this happening though? I am currently using express-handlebars (3.1.0) which I set as a render engine in my express app. – Lee Boon Kong Jan 12 '20 at 14:13
  • I suspect this was happening on newer version of handlebars due to some restrictions, but I do not know how to work on these restrictions. – Lee Boon Kong Jan 12 '20 at 14:30
  • Well, the issue lays between the express plugin that support handlebars, but once the handlebars 4.5.0 is save to use as the main engine of your frontend, kindly let me know by commenting on this. – phemieny7 Jan 13 '20 at 14:04
  • 1
    This is not working. Still get the same issue after I execute npm i -D handlebars@4.5.0 – Deepak Thakur Apr 30 '20 at 12:15
  • 1
    Correct answer is here https://github.com/wycats/handlebars.js/issues/1642 – Deepak Thakur Apr 30 '20 at 12:41
  • Would not suggest either rolling back to former versions of HandlerBars nor installing other dependencies. Since, HandlerBars won't let access prototype methods objects, either use document's toObject() method or Mongoose lean() method. Best solution https://stackoverflow.com/a/7503523/140693 – Satya Kalluri Oct 22 '20 at 18:23
13

"Wow this worked, Why is this happening though? I am currently using express-handlebars (3.1.0) which I set as a render engine in my express app." – Lee Boon Kong Jan 12 at 14:13

"In the past, Handlebars would allow you to access prototype methods and properties of the input object from the template... Multiple security issues have come from this behaviour... In handlebars@^4.6.0. access to the object prototype has been disabled completely. Now, if you use custom classes as input to Handlebars, your code won't work anymore... This package automatically adds runtime options to each template-calls, disabling the security restrictions... If your users are writing templates and you execute them on your server you should NOT use this package, but rather find other ways to solve the problem... I suggest you convert your class-instances to plain JavaScript objects before passing them to the template function. Every property or function you access, must be an "own property" of its parent." – README

More details here: https://www.npmjs.com/package/@handlebars/allow-prototype-access

QUICK AND DIRTY INSECURE METHOD

Usage (express-handlebars and mongoose):

express-handlebars does not allow you to specify runtime-options to pass to the template function. This package can help you disable prototype checks for your models.

"Only do this, if you have full control over the templates that are executed in the server."

Steps:

1 - Install dependency

npm i @handlebars/allow-prototype-access

2 - Use this snippet as an example to rewrite your express server

const express = require('express');
const mongoose = require('mongoose');
const Handlebars = require('handlebars');
const exphbs = require('express-handlebars');

// Import function exported by newly installed node modules.
const { allowInsecurePrototypeAccess } = require('@handlebars/allow-prototype-access');

const PORT = process.env.PORT || 3000;

const app = express();

const routes = require('./routes');

app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static('public'));

// When connecting Handlebars to the Express app...
app.engine('handlebars', exphbs({
    defaultLayout: 'main',
    // ...implement newly added insecure prototype access
    handlebars: allowInsecurePrototypeAccess(Handlebars)
    })
);
app.set('view engine', 'handlebars');

app.use(routes);

const MONGODB_URI = process.env.MONGODB_URI || >'mongodb://localhost/dbName';

mongoose.connect(MONGODB_URI);

app.listen(PORT, function () {
  console.log('Listening on port: ' + PORT);
});

3 - Run the server and do your happy dance.


LONGER MORE SECURE METHOD

Before passing the object returned by your AJAX call to the Handlebars template, map it into a new object with each property or function you need to access in your .hbs file. Below you can see the new object made before passing it to the Handlebars template.

const router = require("express").Router();
const db = require("../../models");

router.get("/", function (req, res) {
    db.Article.find({ saved: false })
        .sort({ date: -1 })
        .then(oldArticleObject => {
            const newArticleObject = {
                articles: oldArticleObject.map(data => {
                    return {
                        headline: data.headline,
                        summary: data.summary,
                        url: data.url,
                        date: data.date,
                        saved: data.saved
                    }
                })
            }
            res.render("home", {
                articles: newArticleObject.articles
            })
        })
        .catch(error => res.status(500).send(error));
});

Your mongoose query

Correct me if I'm wrong but I think this might work for your query...

Confession.find()
    .sort({ date: -1 })
    .then(function (oldDoc) {

        for (var i = 0; i < oldDoc.length; i++) {
            //Check whether sender is anonymous
            if (oldDoc[i].from === "" || oldDoc[i].from == null) {
                oldDoc[i].from = "Anonymous";
            }

            //Add an extra JSON Field for formatted date
            oldDoc[i].formattedDate = formatTime(oldDoc[i].date);
        }

        const newDoc = {
            doc: oldDoc.map(function (data) {
                return {
                    from: data.from,
                    formattedDate: data.formattedDate
                }
            })
        }
        
        res.render('index', { title: 'Confession Box', success: req.session.success, errors: req.session.errors, confession: newDoc.doc });
        req.session.errors = null;
        req.session.success = null;
    });
Dženis H.
  • 7,284
  • 3
  • 25
  • 44
Jason Novak
  • 133
  • 5
11

Today I have the same warning from handlebars and the view is empty. Below is how I fixed that:

//  * USERS PAGE
// @description        users route
// @returns           ../views/users.hbs
router.get('/users', async (req, res) => {
  // get all items from db collection
  const collection = 'User'
  await dbFindAllDocs(collection) // <=> wrapper for Model.find() ...
    .then(documents => {
      // create context Object with 'usersDocuments' key
      const context = {
        usersDocuments: documents.map(document => {
          return {
            name: document.name,
            location: document.location
          }
        })
      }
      // rendering usersDocuments from context Object
      res.render('users', {
        usersDocuments: context.usersDocuments
      })
    })
    .catch(error => res.status(500).send(error))
})

the users.hbs file

<ul>
{{#each usersDocuments}}
<li>name: {{this.name}} location: {{this.location}}</li>
{{/each}}    
</ul>

Creating an entire new Object named context with its own properties then pass in it into the render function will fix the issue...

note:

When we do not create a new Object, it is easy to accidentally expose confidential information, or information that could compromise the security of the projet, mapping the data that's returned from the database and passing only what's needed onto the view could be a good practice...

Drozerah
  • 236
  • 3
  • 9
7

Starting from version 4.6.0 onward, Handlebars forbids accessing prototype properties and methods of the context object by default. This is related to a security issue described here: https://mahmoudsec.blogspot.com/2019/04/handlebars-template-injection-and-rce.html

Refer to https://github.com/wycats/handlebars.js/issues/1642

If you are certain that only developers have access to the templates, it's possible to allow prototype access by installing the following package:

npm i @handlebars/allow-prototype-access

If you are using express-handlebars you should proceed as:

const 
    express = require('express'),
    _handlebars = require('handlebars'),
    expressHandlebars = require('express-handlebars'),
    {allowInsecurePrototypeAccess} = require('@handlebars/allow-prototype-access')

const app = express()

app.engine('handlebars', expressHandlebars({
    handlebars: allowInsecurePrototypeAccess(_handlebars)
}))
app.set('view engine', 'handlebars')
jm4rc05
  • 91
  • 2
  • 6
6

try npm install handlebars version 4.5.3

npm install handlebars@4.5.3

It worked for me

Lajos Arpad
  • 64,414
  • 37
  • 100
  • 175
Xrest
  • 61
  • 1
  • 6
5

A cleaner way to solve this issue is to use the mongoose document .toJSON() method.

let data = dbName.find({})
  .exec(function(error, body) {
     //Some code
  });
    data = data.toJSON()
//use {{data}} on .hbs template
Devmaleeq
  • 541
  • 6
  • 9
3

Add this in app.js (Default Layout path)

app.engine('hbs', hbs.engine({
  extname: 'hbs',
  defaultLayout: 'layouts',
  layoutDir: __dirname + '/views/layouts',
  partialsDir:__dirname+'/views/partials/',
  runtimeOptions:{allowProtoPropertiesByDefault:true,
  allowedProtoMethodsByDefault:true}
}));
2

There was a breaking change in the recent release of Handlebars which has caused this error.

You could simply add the configurations they suggest in their documentation, however be aware, depending on you implementation, this could lead the vulnerability to XXS and RCE attacks.

https://handlebarsjs.com/api-reference/runtime-options.html#options-to-control-prototype-access

Confession.find()
  .sort({date: -1})
  .then(function(doc){
    for(var i=0; i < doc.length; i++){
      //Check whether sender is anonymous
      if (doc[i].from === "" || doc[i].from == null){
        doc[i].from = "Anonymous";
      }

      //Add an extra JSON Field for formatted date
      doc[i].formattedDate = formatTime(doc[i].date);
    }
    res.render('index', {title: 'Confession Box', success:req.session.success, errors: req.session.errors, confession: doc}, {

      // Options to allow access to the properties and methods which as causing the error.

      allowProtoMethodsByDefault: true,
      allowProtoPropertiesByDefault: true

    });

    req.session.errors = null;
    req.session.success = null;
  });
roydukkey
  • 3,149
  • 2
  • 27
  • 43
2

Creating another new Object or Array from data returned by find() will solve the problem . See below a simple illustration

app.get("/",(req,res)=>{

 let com = require('./MODELCOM')    // loading model
 let source=fs.readFileSync(__dirname+"/views/template.hbs","utf-8");

 com.find((err,data)=>{
    // creation new array  using map
   let wanted = data.map(doc=>{
       return {
           name:doc.name,
           _id:doc._id
        }
   })

    let html= handlebar.compile(source);
  fs.writeFileSync(__dirname+"/views/reciever.html",html({communities:wanted}))
    res.sendFile(__dirname+"/views/reciever.html")
});
GNETO DOMINIQUE
  • 628
  • 10
  • 19
  • Very similar to my solution. In my case, search returned result was having the breaking change issue. I did the map transformation as above. The best answer while maintaining the security's objective. – Yazid May 08 '20 at 13:58
2

I am using Angular version 8.0.2 and Node version 10.16.3 While running test cases was facing below issue:

Handlebars: Access has been denied to resolve the property "statements" because it is not an "own property" of its parent.

Handlebars: Access has been denied to resolve the property "functions" because it is not an "own property" of its parent.

While debugging the issue further found that in package.json, "karma-coverage-istanbul-reporter": "2.0.1" is there but "istanbul-lib-report" was missing so did following steps:

  1. In package.json file, under dependencies included "istanbul-lib-report": "3.0.0"
  2. Execute npm install

And it solved my issue :) (Hope this helps someone)

sabin
  • 841
  • 12
  • 15
2

Just add the following code to Fix the Problem..... Before using that install allow Prototype by the following command. If you have any problem comment:...

Install-Module

npm install @handlebars/allow-prototype-access

importing package

const Handlebars = require('handlebars')
const {allowInsecurePrototypeAccess} = require('@handlebars/allow-prototype- 
access')

Set View Engine

app.engine('handlebars', expressHandlebars({
    handlebars: allowInsecurePrototypeAccess(Handlebars)
}));  
app.set('view engine', 'handlebars');
...
0

There is a workaround for this that works in all versions of hbs: do this and sent database to the page. This works without changing the Handlbar template and we can proceed with 0 vulnerabilities finally

var database=[];
for(var i=0;i<foundData.length;i++)
{
 database[i]=foundData[i].toObject();
}
0

I added a map function an it worked for me:

    Confession.find()
      .sort({date: -1})
      .then(function(doc){
        for(var i=0; i < doc.length; i++){
          //Check whether sender is anonymous
          if (doc[i].from === "" || doc[i].from == null){
            doc[i].from = "Anonymous";
          }
    
          //Add an extra JSON Field for formatted date
          doc[i].formattedDate = formatTime(doc[i].date);
        }
        res.render('index', {title: 'Confession Box', success:req.session.success, errors: req.session.errors, confession: **doc.map(doc=>doc.toJSON())}**);
        req.session.errors = null;
        req.session.success = null;
      });

0

I use mongoose and with this function you can solve your problem.

const confession=await Confession.find({}).lean();
res.render('index',{confession});
0

whats really help me and i didnt have to make a huge change in the code was add

runtimeOptions: {
    allowProtoPropertiesByDefault: true,
    allowProtoMethodsByDefault: true
  }

inside of my app.engine, the entire code ->

const handlebars = require('express-handlebars')
app.engine('hbs', handlebars.engine({extname: 'hbs', runtimeOptions: {
    allowProtoPropertiesByDefault: true,
    allowProtoMethodsByDefault: true
  },layoutsDir: __dirname + '/views/Layout', defaultLayout: 'defaultLayout'}))
app.set('view engine', 'hbs')