expressjs / expressjs/express

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

Open
#7,140 5 comments 0 reactions 0 assignees View on GitHub
enhancement require-triage security
Dominant language
JavaScript
Stars
69.5k
Forks
25k
Avg merge
4d 20h
Merged PRs (30d)
9

Description

## 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

```javascript
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);
```

```bash
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:
```javascript
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

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

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.