Skip to content

Commit 2510b0b

Browse files
authored
fix: formatting redirect url on http(s) protocol url (#1803)
avoid security escapes
1 parent a08b386 commit 2510b0b

File tree

3 files changed

+21
-10
lines changed

3 files changed

+21
-10
lines changed

.github/workflows/node.js.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313

1414
strategy:
1515
matrix:
16-
node-version: [12.x, 14.x, 16.x, 18.x]
16+
node-version: [12.x, 14.x, 16.x, 18.x, 20.x, 21.x]
1717

1818
steps:
1919
- uses: actions/checkout@v2

__tests__/response/redirect.js

+16-9
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ describe('ctx.redirect(url)', () => {
1010
it('should redirect to the given url', () => {
1111
const ctx = context();
1212
ctx.redirect('http://google.com');
13-
assert.strictEqual(ctx.response.header.location, 'http://google.com');
13+
assert.strictEqual(ctx.response.header.location, 'http://google.com/');
14+
assert.strictEqual(ctx.status, 302);
15+
});
16+
17+
it('should formatting url before redirect', () => {
18+
const ctx = context();
19+
ctx.redirect('http://google.com\\@apple.com');
20+
assert.strictEqual(ctx.response.header.location, 'http://google.com/@apple.com');
1421
assert.strictEqual(ctx.status, 302);
1522
});
1623

@@ -66,7 +73,7 @@ describe('ctx.redirect(url)', () => {
6673
ctx.header.accept = 'text/html';
6774
ctx.redirect(url);
6875
assert.strictEqual(ctx.response.header['content-type'], 'text/html; charset=utf-8');
69-
assert.strictEqual(ctx.body, `Redirecting to <a href="${url}">${url}</a>.`);
76+
assert.strictEqual(ctx.body, `Redirecting to <a href="${url}/">${url}/</a>.`);
7077
});
7178

7279
it('should escape the url', () => {
@@ -86,17 +93,17 @@ describe('ctx.redirect(url)', () => {
8693
const url = 'http://google.com';
8794
ctx.header.accept = 'text/plain';
8895
ctx.redirect(url);
89-
assert.strictEqual(ctx.body, `Redirecting to ${url}.`);
96+
assert.strictEqual(ctx.body, `Redirecting to ${url}/.`);
9097
});
9198
});
9299

93100
describe('when status is 301', () => {
94101
it('should not change the status code', () => {
95102
const ctx = context();
96-
const url = 'http://google.com';
103+
const url = 'http://google.com/';
97104
ctx.status = 301;
98105
ctx.header.accept = 'text/plain';
99-
ctx.redirect('http://google.com');
106+
ctx.redirect(url);
100107
assert.strictEqual(ctx.status, 301);
101108
assert.strictEqual(ctx.body, `Redirecting to ${url}.`);
102109
});
@@ -108,19 +115,19 @@ describe('ctx.redirect(url)', () => {
108115
const url = 'http://google.com';
109116
ctx.status = 304;
110117
ctx.header.accept = 'text/plain';
111-
ctx.redirect('http://google.com');
118+
ctx.redirect(url);
112119
assert.strictEqual(ctx.status, 302);
113-
assert.strictEqual(ctx.body, `Redirecting to ${url}.`);
120+
assert.strictEqual(ctx.body, `Redirecting to ${url}/.`);
114121
});
115122
});
116123

117124
describe('when content-type was present', () => {
118125
it('should overwrite content-type', () => {
119126
const ctx = context();
120127
ctx.body = {};
121-
const url = 'http://google.com';
128+
const url = 'http://google.com/foo/bar';
122129
ctx.header.accept = 'text/plain';
123-
ctx.redirect('http://google.com');
130+
ctx.redirect(url);
124131
assert.strictEqual(ctx.status, 302);
125132
assert.strictEqual(ctx.body, `Redirecting to ${url}.`);
126133
assert.strictEqual(ctx.type, 'text/plain');

lib/response.js

+4
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,10 @@ module.exports = {
261261
redirect(url, alt) {
262262
// location
263263
if ('back' === url) url = this.ctx.get('Referrer') || alt || '/';
264+
if (url.startsWith('https://') || url.startsWith('http://')) {
265+
// formatting url again avoid security escapes
266+
url = new URL(url).toString();
267+
}
264268
this.set('Location', encodeUrl(url));
265269

266270
// status

0 commit comments

Comments
 (0)