Skip to content

View.prototype.lookup() lacks path containment check (unlike send library for res.sendFile) #7140

Description

@raajheshkannaa

Description

View.prototype.lookup() in lib/view.js:104-123 uses path.resolve(root, name) without validating that the resolved path stays within the configured views directory. Combined with decodeURIComponent() in route parameter decoding, applications that pass user input to res.render() are exposed to path traversal.

res.sendFile() has path traversal protection via the send library's root containment check. The view system has no equivalent protection.

Reproduction

const express = require('express');
const app = express();
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.get('/page/:name', (req, res) => res.render(req.params.name));
app.listen(3000);
curl http://localhost:3000/page/..%2f..%2f..%2fetc%2fpasswd

Route param decodes to ../../../../etc/passwd. View lookup resolves outside views directory.

Runtime verification on Express 5.2.1:

layer.match('/page/..%2f..%2fetc%2fpasswd')
// => true, params.name = "../../etc/passwd"

Context

This is not a critical vulnerability since it requires the developer to pass unsanitized user input to res.render(). However, given that res.sendFile() already has containment via the send library, adding equivalent protection in View.prototype.lookup() would provide defense-in-depth consistency.

Suggested improvement

// lib/view.js - View.prototype.lookup()
var loc = resolve(root, name);
if (!loc.startsWith(resolve(root) + sep)) continue;

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions