Skip to content

Commit 6917dcd

Browse files
authored
fix: do not report invalid tlds as valid urls (#1950)
* fix: do not report invalid tlds as valid urls * chore: update yarn locks of example apps * chore: remove http references in tests for security hotspots * fix: failing tests
1 parent e6f700c commit 6917dcd

File tree

8 files changed

+123
-140
lines changed

8 files changed

+123
-140
lines changed

examples/ExpoMessaging/yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4584,6 +4584,13 @@ lines-and-columns@^1.1.6:
45844584
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
45854585
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
45864586

4587+
linkify-it@^4.0.1:
4588+
version "4.0.1"
4589+
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec"
4590+
integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==
4591+
dependencies:
4592+
uc.micro "^1.0.1"
4593+
45874594
loader-utils@^2.0.0:
45884595
version "2.0.4"
45894596
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
@@ -7049,6 +7056,11 @@ ua-parser-js@^0.7.30:
70497056
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.32.tgz#cd8c639cdca949e30fa68c44b7813ef13e36d211"
70507057
integrity sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==
70517058

7059+
uc.micro@^1.0.1:
7060+
version "1.0.6"
7061+
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
7062+
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
7063+
70527064
uglify-es@^3.1.9:
70537065
version "3.3.9"
70547066
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"

examples/SampleApp/yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5322,6 +5322,13 @@ lines-and-columns@^1.1.6:
53225322
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
53235323
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
53245324

5325+
linkify-it@^4.0.1:
5326+
version "4.0.1"
5327+
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec"
5328+
integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==
5329+
dependencies:
5330+
uc.micro "^1.0.1"
5331+
53255332
loader-utils@^2.0.0:
53265333
version "2.0.4"
53275334
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
@@ -8020,6 +8027,11 @@ [email protected]:
80208027
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961"
80218028
integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==
80228029

8030+
uc.micro@^1.0.1:
8031+
version "1.0.6"
8032+
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
8033+
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
8034+
80238035
uglify-es@^3.1.9:
80248036
version "3.3.9"
80258037
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"

examples/TypeScriptMessaging/yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4999,6 +4999,13 @@ lines-and-columns@^1.1.6:
49994999
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
50005000
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
50015001

5002+
linkify-it@^4.0.1:
5003+
version "4.0.1"
5004+
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec"
5005+
integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==
5006+
dependencies:
5007+
uc.micro "^1.0.1"
5008+
50025009
loader-utils@^2.0.0:
50035010
version "2.0.4"
50045011
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
@@ -7580,6 +7587,11 @@ [email protected]:
75807587
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961"
75817588
integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==
75827589

7590+
uc.micro@^1.0.1:
7591+
version "1.0.6"
7592+
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
7593+
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
7594+
75837595
uglify-es@^3.1.9:
75847596
version "3.3.9"
75857597
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"

package/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"dayjs": "1.10.5",
7373
"file-loader": "6.2.0",
7474
"i18next": "20.2.4",
75+
"linkify-it": "^4.0.1",
7576
"lodash-es": "4.17.21",
7677
"metro-react-native-babel-preset": "0.66.2",
7778
"mime-types": "^2.1.34",
@@ -106,6 +107,7 @@
106107
"@types/react": "17.0.5",
107108
"@types/react-native": "0.67.3",
108109
"@types/react-test-renderer": "17.0.1",
110+
"@types/linkify-it": "3.0.2",
109111
"@types/uuid": "^8.3.4",
110112
"@typescript-eslint/eslint-plugin": "^5.7.0",
111113
"@typescript-eslint/parser": "^5.7.0",

package/src/components/Message/MessageSimple/utils/parseLinks.test.ts

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,116 +2,114 @@ import { parseLinksFromText } from './parseLinks';
22

33
describe('parseLinksFromText', () => {
44
it.each([
5-
['www.getstream.io', 'www.getstream.io'],
6-
['getstream.io', 'getstream.io'],
7-
['scrn://team-chat', 'team-chat'],
8-
['https://localhost', 'localhost'],
5+
['https://www.getstream.io', 'https://www.getstream.io'],
6+
['https://getstream.io', 'https://getstream.io'],
7+
['scrn://team-chat', undefined],
8+
['https://localhost', 'https://localhost'],
99
[
1010
'https://localhost/with/path?and=query&multiple=params',
11-
'localhost/with/path?and=query&multiple=params',
11+
'https://localhost/with/path?and=query&multiple=params',
12+
],
13+
[
14+
'https://localhost/with/path?and=query#fragment',
15+
'https://localhost/with/path?and=query#fragment',
1216
],
13-
['https://localhost/with/path?and=query#fragment', 'localhost/with/path?and=query#fragment'],
14-
['reactnative.stream', 'reactnative.stream'],
17+
['reactnative.stream', undefined],
1518
[
1619
'https://zh.wikipedia.org/wiki/挪威牛油危機',
17-
'zh.wikipedia.org/wiki/%E6%8C%AA%E5%A8%81%E7%89%9B%E6%B2%B9%E5%8D%B1%E6%A9%9F',
20+
'https://zh.wikipedia.org/wiki/%E6%8C%AA%E5%A8%81%E7%89%9B%E6%B2%B9%E5%8D%B1%E6%A9%9F',
1821
],
1922
[
2023
'https://getstream.io/chat/docs/react-native/?language=javascript',
21-
'getstream.io/chat/docs/react-native/?language=javascript',
24+
'https://getstream.io/chat/docs/react-native/?language=javascript',
2225
],
23-
['127.0.0.1/local_(development)_server', '127.0.0.1/local_(development)_server'],
24-
['https://a.co:8999/ab.php?p=12', 'a.co:8999/ab.php?p=12'],
26+
['127.0.0.1/local_(development)_server', undefined],
27+
['https://a.co:8999/ab.php?p=12', 'https://a.co:8999/ab.php?p=12'],
2528
[
26-
'http://help.apple.com/xcode/mac/current/#/devba7f53ad4',
27-
'help.apple.com/xcode/mac/current/#/devba7f53ad4',
29+
'https://help.apple.com/xcode/mac/current/#/devba7f53ad4',
30+
'https://help.apple.com/xcode/mac/current/#/devba7f53ad4',
2831
],
2932
])('Returns the encoded value of %p as %p', (link, expected) => {
3033
const result = parseLinksFromText(link);
31-
32-
expect(result[0].encoded).toBe(expected);
34+
expect(result[0]?.encodedUrl).toBe(expected);
3335
});
34-
3536
it('parses fqdn', () => {
3637
const input = `We have put the apim bol,
37-
temporarily so :sj: we can later put the monitors on my grasp
38-
on reality right now is
39-
https://www.contrived-example.com:8080/sub/page.php?p1=1🇳🇴&p2=2#fragment-identifier
40-
:)`;
41-
38+
temporarily so :sj: we can later put the monitors on my grasp
39+
on reality right now is
40+
https://www.contrived-example.com:8080/sub/page.php?p1=1🇳🇴&p2=2#fragment-identifier
41+
:)`;
4242
const result = parseLinksFromText(input);
43-
4443
expect(result).toEqual([
4544
{
46-
encoded:
47-
'www.contrived-example.com:8080/sub/page.php?p1=1%F0%9F%87%B3%F0%9F%87%B4&p2=2#fragment-identifier',
45+
encodedUrl:
46+
'https://www.contrived-example.com:8080/sub/page.php?p1=1%F0%9F%87%B3%F0%9F%87%B4&p2=2#fragment-identifier',
4847
raw: 'https://www.contrived-example.com:8080/sub/page.php?p1=1🇳🇴&p2=2#fragment-identifier',
49-
scheme: 'https://',
5048
},
5149
]);
5250
});
53-
5451
it.each([
5552
5653
5754
5855
5956
])('Can parse the email address %p', (email) => {
6057
const result = parseLinksFromText(email);
61-
62-
expect(result[0].encoded).toBe(email);
63-
expect(result[0].scheme).toBe('mailto:');
58+
expect(result[0].encodedUrl).toBe('mailto:' + email);
59+
expect(result[0].raw).toBe(email);
6460
});
65-
6661
it("doesn't double the mailto prefix", () => {
6762
const input = 'mailto:[email protected]';
68-
6963
const result = parseLinksFromText(input);
70-
7164
expect(result[0]).toEqual({
72-
encoded: '[email protected]',
65+
encodedUrl: input,
7366
raw: input,
74-
scheme: 'mailto:',
7567
});
7668
});
77-
7869
it('Does not falsely parse LINKs from text content', () => {
7970
const input = `#This string exists to test that we don't produce false positives
80-
81-
Existing links:
82-
[already a parsed link](https://getstream.io/blog/react-native-how-to-build-bidirectional-infinite-scroll/)
83-
[even a bogus one]( should not match )
84-
85-
## These should, however, be parsed:
86-
www.getstream.io
87-
getstream.io
88-
`;
71+
Existing links:
72+
[already a parsed link](https://getstream.io/blog/react-native-how-to-build-bidirectional-infinite-scroll/)
73+
[even a bogus one]( should not match )
74+
## These should, however, be parsed:
75+
www.getstream.io
76+
getstream.io
77+
`;
8978
const result = parseLinksFromText(input);
90-
79+
console.log({ result });
9180
expect(result).toHaveLength(2);
9281
});
93-
9482
it('Encodes incomplete emoji unicode', () => {
9583
const input = 'https://getstream.io/�';
9684
const result = parseLinksFromText(input);
97-
9885
expect(result[0]).toEqual({
99-
encoded: 'getstream.io/%EF%BF%BD',
100-
raw: 'https://getstream.io/�',
101-
scheme: 'https://',
86+
encodedUrl: 'https://getstream.io/%EF%BF%BD',
87+
raw: input,
10288
});
10389
});
104-
90+
it('doest not report invalid tlds as urls', () => {
91+
const input = `
92+
%
93+
% Not links
94+
%
95+
example.invalid
96+
example.invalid/
97+
http://.example.com
98+
http://-example.com
99+
hppt://example.com
100+
example.coma
101+
-example.coma
102+
`;
103+
const result = parseLinksFromText(input);
104+
expect(result).toHaveLength(0);
105+
});
105106
it('does not parse a decimal number as a URL', () => {
106107
const input = '123.456';
107108
const result = parseLinksFromText(input);
108-
109109
expect(result).toHaveLength(0);
110110
});
111-
112111
it.each([['@user'], ['@user.name']])('does not parse %p as a URL', (input) => {
113112
const result = parseLinksFromText(input);
114-
115113
expect(result).toHaveLength(0);
116114
});
117115
});
Lines changed: 11 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,8 @@
1-
/**
2-
* 📣 Note: Do not use named capture groups, as
3-
* it is not yet available in Hermes.
4-
*
5-
* These helpers indend to make it easier to skim read
6-
* the patterns below, but note that some are optional,
7-
* specified in the patterns these are added to.
8-
* */
9-
const emailUserName = '[\\w+\\.~$_-]+';
10-
const schema = `(\\w{2,15}:\\/\\/)`;
11-
// something.tld OR 123.123.123.123
12-
const domain = `((?:\\w+\\.[a-zA-Z]+)+(?:[^:\\/\\s]+)|(?:\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}))`;
13-
const port = `(:[0-9]{1,5})`;
14-
const path = `((?:\\/)?[^?\\s]+)`;
15-
const queryString = `(\\?[^#\\s]+)`;
16-
const fragment = `(#[\\w_-]+)`;
17-
18-
/**
19-
* Match any email address, with and without `mailto:`
20-
*/
21-
const emailPattern = `(mailto:)?((?:${emailUserName})@(?:${domain}))`;
22-
23-
/**
24-
* Match any string starting with something that seems like it's a schema.
25-
* */
26-
const schemePrefixedLinkPattern = `${schema}(\\S+)`;
1+
import Linkify from 'linkify-it';
272

28-
/**
29-
* Match as much as possible of a fqdn, at least with a domain name formed as
30-
* something.tld
31-
*/
32-
const fqdnLinkPattern = `${schema}?${domain}${port}?${path}?${queryString}?${fragment}?`;
33-
34-
interface Link {
35-
encoded: string;
3+
interface LinkInfo {
4+
encodedUrl: string;
365
raw: string;
37-
scheme: string;
386
}
397

408
/**
@@ -49,57 +17,19 @@ const removeMarkdownLinksFromText = (input: string) => input.replace(/\[[\w\s]+\
4917
* */
5018
const removeUserNamesFromText = (input: string) => input.replace(/^@\w+\.?\w/, '');
5119

52-
export const parseLinksFromText = (input: string): Link[] => {
53-
let matches;
54-
20+
export const parseLinksFromText = (input: string): LinkInfo[] => {
5521
const strippedInput = [removeMarkdownLinksFromText, removeUserNamesFromText].reduce(
5622
(acc, fn) => fn(acc),
5723
input,
5824
);
5925

60-
const results: Link[] = [];
61-
62-
const emailRegExp = new RegExp(emailPattern, 'gi');
63-
while ((matches = emailRegExp.exec(input)) !== null) {
64-
const [raw, scheme = 'mailto:', displayValue] = matches;
65-
results.push({ encoded: encodeURI(displayValue), raw, scheme });
66-
}
67-
68-
/**
69-
* The two link patterns are checked with an "or" (`|`)
70-
* to avoid overlapping matches being duplicated in the output.
71-
* */
72-
const linkRegex = new RegExp(`${fqdnLinkPattern}|${schemePrefixedLinkPattern}`, 'gi');
73-
while ((matches = linkRegex.exec(strippedInput)) !== null) {
74-
const [
75-
raw,
76-
fqdnScheme = '',
77-
fqdnDomainName = '',
78-
fqdnPort = '',
79-
fqdnPath = '',
80-
fqdnQueryStraing = '',
81-
fqdnFragment = '',
82-
schemePrefixedScheme = '',
83-
schemePrefixedPath = '',
84-
] = matches;
85-
86-
if (schemePrefixedScheme !== '') {
87-
results.push({
88-
encoded: encodeURI(schemePrefixedPath),
89-
raw,
90-
scheme: schemePrefixedScheme,
91-
});
92-
continue;
93-
}
26+
const linkify = Linkify();
27+
const matches = linkify.match(strippedInput) ?? [];
9428

95-
results.push({
96-
encoded: encodeURI(
97-
[fqdnDomainName, fqdnPort, fqdnPath, fqdnQueryStraing, fqdnFragment].join(''),
98-
),
99-
raw,
100-
scheme: fqdnScheme,
101-
});
102-
}
29+
const result: LinkInfo[] = matches.map((match) => {
30+
const { raw, url } = match;
31+
return { encodedUrl: encodeURI(url), raw };
32+
});
10333

104-
return results;
34+
return result;
10535
};

package/src/components/Message/MessageSimple/utils/renderText.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,15 @@ export const renderText = <
109109
if (!text) return null;
110110

111111
let newText = text.trim();
112-
const urls = parseLinksFromText(newText);
112+
const linkInfos = parseLinksFromText(newText);
113113

114-
for (const urlInfo of urls) {
115-
const displayLink = truncate(urlInfo.encoded, {
114+
for (const linkInfo of linkInfos) {
115+
const displayLink = truncate(linkInfo.raw, {
116116
length: 200,
117117
omission: '...',
118118
});
119-
const markdown = `[${displayLink}](${urlInfo.scheme}${urlInfo.encoded})`;
120-
newText = newText.replace(urlInfo.raw, markdown);
119+
const markdown = `[${displayLink}](${linkInfo.encodedUrl})`;
120+
newText = newText.replace(linkInfo.raw, markdown);
121121
}
122122

123123
newText = newText.replace(/[<&"'>]/g, '\\$&');

0 commit comments

Comments
 (0)