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

2.0.0 breaks Express.js res.render views configuration #215

Closed
SamuelGaona opened this issue Jan 29, 2023 · 6 comments
Closed

2.0.0 breaks Express.js res.render views configuration #215

SamuelGaona opened this issue Jan 29, 2023 · 6 comments

Comments

@SamuelGaona
Copy link

The configuration for the new version 2.0.0 of eta breaks res.render views configuration.

To Reproduce
Steps to reproduce the behavior:

  1. Use the proposed configuration
var eta = require("eta")
eta.configure({ views: "./OTHER-VIEWS-FOLDER", cache: true })
app.engine("eta", eta.renderFile)
app.set("view engine", "eta")
  1. Use res.render(templateName);
  2. It looks over the default views folder of express.js, './views'

Expected behavior
res.render should render as if it was configured with app.set("views", "./OTHER-VIEWS-FOLDER")

renderFile works just fine.

@Ching367436
Copy link

Summary

Continue using app.set("views", "./OTHER-VIEWS-FOLDER") would be fine.

Detail

When we visit / in the following code, express.js will call renderFile(this.path, options, callback).
Eta.js will then set the renderConfig.filename to this.path, so renderConfig.filename will be fine.

this.path is resolved by express.js by using the setting from app.set("views", "./OTHER-VIEWS-FOLDER").
If we use eta.configure({ views: "./views", cache: true }) instead, express.js will then use its default value to resolve this.path (since it only tells eta.js where to resolve the file, not express.js).

var express = require("express")
var app = express()
var eta = require("eta")

var eta = require("eta")
app.engine("eta", eta.renderFile)
app.set("views", "./OTHER-VIEWS-FOLDER")
app.set("view cache", true)
app.set("view engine", "eta")

app.get("/", function (req, res) {
  res.render("template", 
  req.query
  )
})

app.listen(8000, function () {
  console.log("listening to requests on port 8000")
})

@Ching367436
Copy link

There is another thing worth mentioning about the cache option. (I think its relevent to this issue.)

The following code set the cache option of eta.js, which caches templateFunc.

eta.configure({ views: "./OTHER-VIEWS-FOLDER", cache: true })

The following code set the cache option of express.js, which caches view.

app.set("view cache", true)

THEY ARE DIFFERENT.

@gkentr
Copy link

gkentr commented Jan 30, 2023

@Ching367436 app.set("views", "...") appears to be fine when configured like so but if you set up multiple directories, it seems that it won't attempt to resolve partials using all of them. This works fine in 1.14.2 but fails in 2.0.0 (Stackblitz):

index.js

const express = require('express');
const app = express();
const eta = require('eta');

app.engine('eta', eta.renderFile);

app.set('view engine', 'eta');

app.set('views', ['./views1', './views2']);

app.get('/', function (req, res) {
  res.render('template', {
    favorite: 'Eta',
    name: 'Ben',
    reasons: ['fast', 'lightweight', 'simple'],
  });
});

app.listen(3000);

views1/template.eta

My favorite template engine is <%= it.favorite %> because it is: <%= it.reasons.join(', ') %>

<%~ includeFile('./footer', it) %>

views2/footer.eta

<footer>Signed: <%= it.name %> </footer>

The error:

Eta Error: Could not find the template "./footer". Paths tried: /home/projects/node-n8sqsu/views1/footer.eta
    at EtaErr (file:///home/projects/node-n8sqsu/node_modules/eta/dist/eta.umd.js:48:17)
    at getPath (file:///home/projects/node-n8sqsu/node_modules/eta/dist/eta.umd.js:679:15)
    at includeFile (file:///home/projects/node-n8sqsu/node_modules/eta/dist/eta.umd.js:799:17)
    at Object.includeFileHelper [as includeFile] (file:///home/projects/node-n8sqsu/node_modules/eta/dist/eta.umd.js:851:31)
    at eval (eval at _0x579686 (https://noden8sqsu-el15-d7et1jf1.w-credentialless.staticblitz.com/blitz.685d3c93544a5e80be2c9e6906d68c0f4bd854cc.js:15:338547), <anonymous>:10:5)
    at tryHandleCache (file:///home/projects/node-n8sqsu/node_modules/eta/dist/eta.umd.js:759:9)
    at View.renderFile [as engine] (file:///home/projects/node-n8sqsu/node_modules/eta/dist/eta.umd.js:834:12)
    at View.render (file:///home/projects/node-n8sqsu/node_modules/express/lib/view.js:135:8)
    at tryRender (file:///home/projects/node-n8sqsu/node_modules/express/lib/application.js:640:10)
    at Function.render (file:///home/projects/node-n8sqsu/node_modules/express/lib/application.js:592:3)

@nebrelbug
Copy link
Collaborator

@SamuelGaona @Ching367436 @gkentr thanks for giving details on this issue -- it's one that I didn't expect.

It seems to me that the above problems could be solved in two ways:

  1. Within Eta, merge the values of data.settings.views and data.settings["view cache"] with config.views and config["view cache"]. This is the behavior of 1.14.2.
    @Ching367436 you're right that Eta's cache option and Express's "view cache" option are different. Perhaps we should only merge data.settings.views and config.views, then instruct users to run both eta.configure({ cache: true }) and app.set("view cache", true).

However, if a developer passes in req.query as template data, there is a chance that malicious users could change the views option and gain access to another folder on the user's machine.

  1. Instruct users to set both Express.js config values and Eta.js config values:
app.engine("eta", eta.renderFile)
eta.configure({  views: "./views-folder", cache: true })
app.set("views", "./views-folder")
app.set("view cache", true)
app.set("view engine", "eta")

This is slightly annoying, but may be safer.

What do you all think?

@Ching367436
Copy link

Ching367436 commented Jan 31, 2023

The second option would be better.

If we choose the first option, an attacker could do more than just gain access to another folder on the user's machine under certain circumstances.
For example:
If an attacker could control files on the server (like file upload functionality), the attacker could possibly perform server-side template injection.
In @gkentr's example above, if we change app.get('/', .) to the following and assume that the app has file upload functionality, the attacker could upload footer.eta to THE_UPLOAD_FOLDER.
Then when visiting /?reasons[0]=&settings[views][0]=THE_UPLOAD_FOLDER, THE_UPLOAD_FOLDER/footer.eta will be included (server-side template injection!).

app.get('/', function (req, res) {
  res.render('template', 
    req.query
  );
});

Server-side template injection could sometime lead to remote code execution.
I haven't tested out if server-side template injection in Eta could lead to remote code execution yet, maybe it could.


Update: Server-side template injection in Eta could lead to remote code execution.

@nebrelbug
Copy link
Collaborator

@Ching367436 I think you're right. I'll update the docs to update recommended ExpressJS usage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants