Skip to content

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

Open

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

  1. added 2 commits that reference this issue on Mar 31, 2026
    2d0f088
    36830df
  2. som14062005 commented on Mar 31, 2026

    @som14062005

    I've submitted a fix for this in PR #7142. Happy to make any changes
    if the approach needs adjustment.

  3. added a commit that references this issue on Mar 31, 2026
    84d809d
  4. krzysdz commented on Mar 31, 2026

    @krzysdz
    Contributor

    This is a known behaviour (#4445), but I agree that changing this is a good idea. I can't think of a reason why anyone would try to render something outside of the views directory, but that doesn't mean that nobody does this.

    This probably requires a major release (6.x?) and documentation changes.

  5. som14062005 commented on Mar 31, 2026

    @som14062005

    Thanks for the context on #4445 i understand this could be a
    breaking change for users rendering files outside the views directory.

    happy to:

    • Retarget this to 6.x if that's preferred
    • Add an opt-in setting for 5.x instead
    • Include documentation changes

    Please let me know how you'd like to proceed.

  6. IamLizu commented on Apr 4, 2026

    @IamLizu
    Member

    Thanks for the detailed report.

    After reviewing lib/view.js, the res.render() docs, and prior discussion in #4445, this looks like working as designed rather than a new security bug.

    • View.prototype.lookup() resolves from the configured views path but does not enforce containment, and res.render() intentionally supports absolute view paths.
    • This behavior is covered by tests (test/res.render.js and test/app.render.js include absolute path rendering).
    • The API docs already warn that the view argument should not contain end-user input because it performs filesystem operations.

    res.sendFile() is different by design: it uses send, which applies traversal protections (especially when root is set) because that API is commonly used with user-influenced file paths.

    I’d triage this as a security hardening enhancement (defense in depth), not a vulnerability/regression. A default containment change would likely be breaking, so 6.x (or an opt-in strict mode in 5.x) plus docs updates seems like the safest path.

  7. janderik commented on Jul 22, 2026

    @janderik

    Path Containment Check for View.prototype.lookup()

    Summary: The current implementation lacks path containment validation, which could allow path traversal attacks when resolving view templates.

    Recommendation: Add a root containment check using path.relative() to ensure the resolved path stays within the root directory:

    `javascript
    // In lib/view.js around line 104-123
    View.prototype.lookup = function lookup(name) {
    var opts = this.options;
    var root = opts.root;
    var loc = opts.dirname;
    var paths = [loc];

    // ... existing path resolution logic ...

    // ADD THIS CONTAINMENT CHECK:
    var resolvedPath = path.resolve(root, filePath);
    var relativePath = path.relative(root, resolvedPath);
    if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
    throw new Error('View path traversal detected: ' + filePath);
    }
    return resolvedPath;
    };
    `

    Reference: The send library implements similar protection via send.mime.lookup() path sanitization. The vulnerability exists because
    ame can contain ../ sequences that escape the intended root directory, allowing arbitrary file reads.

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