diff --git a/packages/koa/src/response.ts b/packages/koa/src/response.ts index 4115953f34..e374d222c7 100644 --- a/packages/koa/src/response.ts +++ b/packages/koa/src/response.ts @@ -18,6 +18,29 @@ import type { Application } from './application.ts'; import type { Context } from './context.ts'; import type { Request } from './request.ts'; +function formatRedirectUrl(url: string): string { + if (url.startsWith('https://') || url.startsWith('http://')) { + // formatting url again avoid security escapes + try { + return new URL(url).toString(); + } catch { + return url; + } + } + if (url.startsWith('/\\')) { + return `/%5C${url.slice(2)}`; + } + const firstChar = url[0]; + const secondChar = url[1]; + if ((firstChar === '/' || firstChar === '\\') && (secondChar === '/' || secondChar === '\\')) { + return `/%2F${url.slice(2)}`; + } + if (url.startsWith('\\')) { + return `/%5C${url.slice(1)}`; + } + return url; +} + export class Response { [key: symbol]: unknown; app: Application; @@ -250,10 +273,7 @@ export class Response { if (url === 'back') { url = this._getBackReferrer() || alt || '/'; } - if (url.startsWith('https://') || url.startsWith('http://')) { - // formatting url again avoid security escapes - url = new URL(url).toString(); - } + url = formatRedirectUrl(url); this.set('Location', encodeUrl(url)); // status diff --git a/packages/koa/test/response/redirect.test.ts b/packages/koa/test/response/redirect.test.ts index e1e1dbb012..eee198dfa8 100644 --- a/packages/koa/test/response/redirect.test.ts +++ b/packages/koa/test/response/redirect.test.ts @@ -21,6 +21,41 @@ describe('ctx.redirect(url)', () => { assert.equal(ctx.status, 302); }); + it('should not redirect to protocol-relative URLs', () => { + const ctx = context(); + ctx.redirect('//evil.com'); + assert.equal(ctx.response.header.location, '/%2Fevil.com'); + assert.equal(ctx.status, 302); + }); + + it('should not redirect to backslash-prefixed URLs', () => { + const ctx = context(); + ctx.redirect(String.raw`/\evil.com`); + assert.equal(ctx.response.header.location, '/%5Cevil.com'); + assert.equal(ctx.status, 302); + }); + + it('should not redirect to slash and backslash variants', () => { + const ctx = context(); + ctx.redirect(String.raw`\//evil.com`); + assert.equal(ctx.response.header.location, '/%2F/evil.com'); + assert.equal(ctx.status, 302); + }); + + it('should not redirect to double-backslash URLs', () => { + const ctx = context(); + ctx.redirect(String.raw`\\evil.com`); + assert.equal(ctx.response.header.location, '/%2Fevil.com'); + assert.equal(ctx.status, 302); + }); + + it('should not throw on malformed absolute URLs', () => { + const ctx = context(); + ctx.redirect('http://[invalid'); + assert.equal(ctx.response.header.location, 'http://[invalid'); + assert.equal(ctx.status, 302); + }); + it('should auto fix not encode url', async () => { const app = new Koa();