Skip to content

Commit 4498d59

Browse files
authored
fix: it should handle already encoded parts in the URL when redirecting (#3293)
1 parent a93d801 commit 4498d59

File tree

3 files changed

+70
-1
lines changed

3 files changed

+70
-1
lines changed

__tests__/redirector.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,22 @@ describe('GET /r/:postId', () => {
105105
.expect(302)
106106
.expect('Location', 'http://localhost:5002/posts/p1-p1');
107107
});
108+
109+
it('should not escape already encoded URL', async () => {
110+
await con
111+
.getRepository(ArticlePost)
112+
.update(
113+
{ id: 'p1' },
114+
{ url: 'http://p1.com/hello%20world/%f0%9f%9a%80-to-the-🌔' },
115+
);
116+
return request(app.server)
117+
.get('/r/p1')
118+
.expect(302)
119+
.expect(
120+
'Location',
121+
'http://p1.com/hello%20world/%f0%9f%9a%80-to-the-%F0%9F%8C%94?ref=dailydev',
122+
);
123+
});
108124
});
109125

110126
describe('GET /:id/profile-image', () => {

src/common/encodeurl.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*!
2+
* encodeurl
3+
* Copyright(c) 2016 Douglas Christopher Wilson
4+
* MIT Licensed
5+
*/
6+
7+
/**
8+
* RegExp to match non-URL code points, *after* encoding (i.e. not including "%")
9+
* and including invalid escape sequences.
10+
* @private
11+
*/
12+
13+
const ENCODE_CHARS_REGEXP =
14+
/(?:[^\x21\x23-\x3B\x3D\x3F-\x5F\x61-\x7A\x7C\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g;
15+
16+
/**
17+
* RegExp to match unmatched surrogate pair.
18+
* @private
19+
*/
20+
21+
const UNMATCHED_SURROGATE_PAIR_REGEXP =
22+
/(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)/g;
23+
24+
/**
25+
* String to replace unmatched surrogate pair with.
26+
* @private
27+
*/
28+
29+
const UNMATCHED_SURROGATE_PAIR_REPLACE = '$1\uFFFD$2';
30+
31+
/**
32+
* Encode a URL to a percent-encoded form, excluding already-encoded sequences.
33+
*
34+
* This function will take an already-encoded URL and encode all the non-URL
35+
* code points. This function will not encode the "%" character unless it is
36+
* not part of a valid sequence (`%20` will be left as-is, but `%foo` will
37+
* be encoded as `%25foo`).
38+
*
39+
* This encode is meant to be "safe" and does not throw errors. It will try as
40+
* hard as it can to properly encode the given URL, including replacing any raw,
41+
* unpaired surrogate pairs with the Unicode replacement character prior to
42+
* encoding.
43+
*
44+
* @param {string} url
45+
* @return {string}
46+
* @public
47+
*/
48+
49+
export const encodeUrl = (url: string): string =>
50+
String(url)
51+
.replace(UNMATCHED_SURROGATE_PAIR_REGEXP, UNMATCHED_SURROGATE_PAIR_REPLACE)
52+
.replace(ENCODE_CHARS_REGEXP, encodeURI);

src/routes/redirector.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FastifyInstance } from 'fastify';
33
import { ArticlePost, Post } from '../entity';
44
import { getDiscussionLink, notifyView } from '../common';
55
import createOrGetConnection from '../db';
6+
import { encodeUrl } from '../common/encodeurl';
67

78
export default async function (fastify: FastifyInstance): Promise<void> {
89
fastify.get<{ Params: { postId: string }; Querystring: { a?: string } }>(
@@ -35,7 +36,7 @@ export default async function (fastify: FastifyInstance): Promise<void> {
3536
}
3637
const url = new URL(post.url);
3738
url.searchParams.append('ref', 'dailydev');
38-
const encodedUri = encodeURI(url.href);
39+
const encodedUri = encodeUrl(url.href);
3940
if (req.isBot) {
4041
return res.status(302).redirect(encodedUri);
4142
}

0 commit comments

Comments
 (0)