Security context
Low· 4.7GHSA-jj78-5fmv-mv28 CVE-2024-9266CWE-601Published Oct 3, 2024

Express Open Redirect vulnerability

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

3.4.5 → fixed in 4.0.0-rc1

Details

URL Redirection to Untrusted Site ('Open Redirect') vulnerability in Express. This vulnerability affects the use of the Express Response object. This issue impacts Express: from 3.4.5 before 4.0.0-rc1.

The fix

no semver2 so travis stops crying

Jonathan Ong· Oct 30, 2013, 05:44 AM+1316682a7d7a977
lib/response.js+3 7
@@ -605,8 +605,7 @@ res.cookie = function(name, val, options){
/**
* Set the location header to `url`.
*
- * The given `url` can also be the name of a mapped url, for
- * example by default express supports "back" which redirects
+ * The given `url` can also be "back", which redirects
* to the _Referrer_ or _Referer_ headers or "/".
*
* Examples:
@@ -637,11 +636,8 @@ res.location = function(url){
, req = this.req
, path;
- // setup redirect map
- var map = { back: req.get('Referrer') || '/' };
-
- // perform redirect
- url = map[url] || url;
+ // "back" is an alias for the referrer
+ if(url === 'back') url = req.get('Referrer') || '/';
// relative
if (!~url.indexOf('://') && 0 != url.indexOf('//')) {
lib/response.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
History.md+9 0
@@ -1,3 +1,12 @@
+3.4.5 / 2013-11-27
+==================
+
+ * update connect
+ * res.location: remove leading ./ #1802 @kapouer
+ * res.redirect: fix `res.redirect('toString') #1829 @michaelficarra
+ * res.send: always send ETag when content-length > 0
+ * router: add Router.all() method
+
3.4.4 / 2013-10-29
==================
lib/response.js+1 1
@@ -132,7 +132,7 @@ res.send = function(body){
// ETag support
// TODO: W/ support
- if (app.settings.etag && len > 1024 && 'GET' == req.method) {
+ if (app.settings.etag && len && 'GET' == req.method) {
if (!this.get('ETag')) {
this.set('ETag', etag(body));
}
lib/response.js+2 2
@@ -643,11 +643,11 @@ res.location = function(url){
// relative
if (!~url.indexOf('://') && 0 != url.indexOf('//')) {
- var path
+ var path;
// relative to path
if ('.' == url[0]) {
- path = req.originalUrl.split('?')[0]
+ path = req.originalUrl.split('?')[0];
url = path + ('/' == path[path.length - 1] ? '' : '/') + url;
// relative to mount-point
} else if ('/' != url[0]) {
paths
lib/response.js | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
lib/response.js+5 4
@@ -14,6 +14,7 @@ var http = require('http')
, cookie = require('cookie')
, send = require('send')
, mime = connect.mime
+ , resolve = require('url').resolve
, basename = path.basename
, extname = path.extname;
@@ -633,7 +634,8 @@ res.cookie = function(name, val, options){
res.location = function(url){
var app = this.app
- , req = this.req;
+ , req = this.req
+ , path;
// setup redirect map
var map = { back: req.get('Referrer') || '/' };
@@ -643,12 +645,11 @@ res.location = function(url){
// relative
if (!~url.indexOf('://') && 0 != url.indexOf('//')) {
- var path;
-
// relative to path
if ('.' == url[0]) {
path = req.originalUrl.split('?')[0];
- url = path + ('/' == path[path.length - 1] ? '' : '/') + url;
+ path = path + ('/' == path[path.length - 1] ? '' : '/');
+ url = resolve(path, url);
// relative to mount-point
} else if ('/' != url[0]) {
path = app.path();
test/res.location.js | 4 ++--
test/res.redirect.js | 10 +++++-----
2 files changed, 7 insertions(+), 7 deletions(-)
test/res.redirect.js+5 5
@@ -210,17 +210,17 @@ describe('res', function(){
request(root)
.get('/depth1')
.end(function(err, res){
- res.headers.should.have.property('location', '/depth1/./index');
+ res.headers.should.have.property('location', '/depth1/index');
request(root)
.get('/depth1/depth2')
.end(function(err, res){
- res.headers.should.have.property('location', '/depth1/depth2/./index');
+ res.headers.should.have.property('location', '/depth1/depth2/index');
request(root)
.get('/depth1/depth2/depth3')
.end(function(err, res){
- res.headers.should.have.property('location', '/depth1/depth2/depth3/./index');
+ res.headers.should.have.property('location', '/depth1/depth2/depth3/index');
done();
})
})
@@ -231,11 +231,11 @@ describe('res', function(){
request(root)
.get('/depth2')
.end(function(err, res){
- res.headers.should.have.property('location', '/depth2/./index');
+ res.headers.should.have.property('location', '/depth2/index');
request(root)
.get('/depth3')
.end(function(err, res){
- res.headers.should.have.property('location', '/depth3/./index');
+ res.headers.should.have.property('location', '/depth3/index');
done();
})
})
test/res.location.js | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
lib/response.js+1 1
@@ -637,7 +637,7 @@ res.location = function(url){
, path;
// "back" is an alias for the referrer
- if(url === 'back') url = req.get('Referrer') || '/';
+ if ('back' == url) url = req.get('Referrer') || '/';
// relative
if (!~url.indexOf('://') && 0 != url.indexOf('//')) {
examples/content-negotiation/index.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
package.json+1 1
@@ -43,7 +43,7 @@
"should": "2",
"connect-redis": "*",
"marked": "*",
- "supertest": "0.8.1 - 1"
+ "supertest": ">= 0.8.1 < 1"
},
"keywords": [
"express",
lib/response.js | 2 +-
test/res.send.js | 15 +++++++++++++++
2 files changed, 16 insertions(+), 1 deletion(-)
test/res.send.js+15 0
@@ -321,6 +321,21 @@ describe('res', function(){
describe('"etag" setting', function(){
describe('when enabled', function(){
+ it('should send ETag even when content-length < 1024', function(done){
+ var app = express();
+
+ app.use(function(req, res){
+ res.send('kajdslfkasdf');
+ });
+
+ request(app)
+ .get('/')
+ .end(function(err, res){
+ res.headers.should.have.property('etag');
+ done();
+ });
+ })
+
it('should send ETag ', function(done){
var app = express();
test/res.sendfile.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
test/res.sendfile.js+1 1
@@ -51,7 +51,7 @@ describe('res', function(){
.get('/')
.end(function(err, res){
assert(1 == calls, 'called too many times');
- res.text.should.equal("ENOENT, stat 'test/fixtures/nope.html'");
+ res.text.should.startWith("ENOENT, stat");
res.statusCode.should.equal(200);
done();
});
.travis.yml | 2 ++
package.json | 18 +++++++++---------
2 files changed, 11 insertions(+), 9 deletions(-)
.travis.yml+2 0
@@ -2,3 +2,5 @@ language: node_js
node_js:
- "0.8"
- "0.10"
+before_install:
+ - "npm update -g npm"
package.json+9 9
@@ -32,18 +32,18 @@
"methods": "0.1.0",
"send": "0.1.4",
"cookie-signature": "1.0.1",
- "debug": "*"
+ "debug": "0.7.3 - 1"
},
"devDependencies": {
- "ejs": "*",
- "mocha": "*",
+ "ejs": "0.8.4 - 1",
+ "mocha": "^1.13.0",
"jade": "0.30.0",
- "hjs": "*",
- "stylus": "*",
- "should": "2",
- "connect-redis": "*",
- "marked": "*",
- "supertest": ">= 0.8.1 < 1"
+ "hjs": "0.0.6 - 1",
+ "stylus": "0.39.1 - 1",
+ "should": "^2.0.2",
+ "connect-redis": "^1.4.5",
+ "marked": "0.2.9 - 1",
+ "supertest": "0.8.1 - 1"
},
"keywords": [
"express",
.travis.yml | 4 +---
package.json | 20 ++++++++++----------
2 files changed, 11 insertions(+), 13 deletions(-)
.travis.yml+1 3
@@ -1,6 +1,4 @@
language: node_js
node_js:
- "0.8"
- - "0.10"
-before_install:
- - "npm update -g npm"
+ - "0.10"
package.json+10 10
@@ -32,18 +32,18 @@
"methods": "0.1.0",
"send": "0.1.4",
"cookie-signature": "1.0.1",
- "debug": "0.7.3 - 1"
+ "debug": ">= 0.7.3 < 1"
},
"devDependencies": {
- "ejs": "0.8.4 - 1",
- "mocha": "^1.13.0",
+ "ejs": ">= 0.8.4 < 1",
+ "mocha": ">= 1.13.0 < 2",
"jade": "0.30.0",
- "hjs": "0.0.6 - 1",
- "stylus": "0.39.1 - 1",
- "should": "^2.0.2",
- "connect-redis": "^1.4.5",
- "marked": "0.2.9 - 1",
- "supertest": "0.8.1 - 1"
+ "hjs": ">= 0.0.6 < 1",
+ "stylus": ">= 0.39.1 < 1",
+ "should": ">= 2.0.2 < 3",
+ "connect-redis": ">= 1.4.5 < 2",
+ "marked": ">= 0.2.9 < 1",
+ "supertest": ">= 0.8.1 < 1"
},
"keywords": [
"express",
@@ -66,6 +66,6 @@
"test": "make test"
},
"engines": {
- "node": "*"
+ "node": ">= 0.8.0"
}
}
lib/response.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
test/res.location.js+2 2
@@ -64,7 +64,7 @@ describe('res', function(){
request(app)
.get('/post/1')
.end(function(err, res){
- res.headers.should.have.property('location', '/post/1/./edit');
+ res.headers.should.have.property('location', '/post/1/edit');
done();
})
})
@@ -81,7 +81,7 @@ describe('res', function(){
request(app)
.get('/post/1')
.end(function(err, res){
- res.headers.should.have.property('location', '/post/1/../new');
+ res.headers.should.have.property('location', '/post/new');
done();
})
})
More files changed — see the full commit.

References