Skip to content

Commit 0867302

Browse files
FDrag0njonchurch
authored andcommitted
Prevent open redirect allow list bypass due to encodeurl
Co-authored-by: Jon Church <me@jonchurch.com>
1 parent 567c9c6 commit 0867302

File tree

2 files changed

+64
-2
lines changed

2 files changed

+64
-2
lines changed

lib/response.js

+19-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ var extname = path.extname;
3434
var mime = send.mime;
3535
var resolve = path.resolve;
3636
var vary = require('vary');
37+
var urlParse = require('url').parse;
3738

3839
/**
3940
* Response prototype.
@@ -911,8 +912,25 @@ res.location = function location(url) {
911912
loc = this.req.get('Referrer') || '/';
912913
}
913914

915+
var lowerLoc = loc.toLowerCase();
916+
var encodedUrl = encodeUrl(loc);
917+
if (lowerLoc.indexOf('https://') === 0 || lowerLoc.indexOf('http://') === 0) {
918+
try {
919+
var parsedUrl = urlParse(loc);
920+
var parsedEncodedUrl = urlParse(encodedUrl);
921+
// Because this can encode the host, check that we did not change the host
922+
if (parsedUrl.host !== parsedEncodedUrl.host) {
923+
// If the host changes after encodeUrl, return the original url
924+
return this.set('Location', loc);
925+
}
926+
} catch (e) {
927+
// If parse fails, return the original url
928+
return this.set('Location', loc);
929+
}
930+
}
931+
914932
// set location
915-
return this.set('Location', encodeUrl(loc));
933+
return this.set('Location', encodedUrl);
916934
};
917935

918936
/**

test/res.location.js

+45-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
'use strict'
22

33
var express = require('../')
4-
, request = require('supertest');
4+
, request = require('supertest')
5+
, url = require('url');
56

67
describe('res', function(){
78
describe('.location(url)', function(){
89
it('should set the header', function(done){
910
var app = express();
1011

12+
app.use(function(req, res){
13+
res.location('http://google.com/').end();
14+
});
15+
16+
request(app)
17+
.get('/')
18+
.expect('Location', 'http://google.com/')
19+
.expect(200, done)
20+
})
21+
22+
it('should preserve trailing slashes when not present', function(done){
23+
var app = express();
24+
1125
app.use(function(req, res){
1226
res.location('http://google.com').end();
1327
});
@@ -31,6 +45,36 @@ describe('res', function(){
3145
.expect(200, done)
3246
})
3347

48+
it('should not encode bad "url"', function (done) {
49+
var app = express()
50+
51+
app.use(function (req, res) {
52+
// This is here to show a basic check one might do which
53+
// would pass but then the location header would still be bad
54+
if (url.parse(req.query.q).host !== 'google.com') {
55+
res.status(400).end('Bad url');
56+
}
57+
res.location(req.query.q).end();
58+
});
59+
60+
request(app)
61+
.get('/?q=http://google.com\\@apple.com')
62+
.expect(200)
63+
.expect('Location', 'http://google.com\\@apple.com')
64+
.end(function (err) {
65+
if (err) {
66+
throw err;
67+
}
68+
69+
// This ensures that our protocol check is case insensitive
70+
request(app)
71+
.get('/?q=HTTP://google.com\\@apple.com')
72+
.expect(200)
73+
.expect('Location', 'HTTP://google.com\\@apple.com')
74+
.end(done)
75+
});
76+
});
77+
3478
it('should not touch already-encoded sequences in "url"', function (done) {
3579
var app = express()
3680

0 commit comments

Comments
 (0)