Skip to content

Commit 4202744

Browse files
chrisbobbegnprice
authored andcommitted
url: Consistently parse URLs for isUrlOnRealm (CVE-2022-35962)
The ad-hoc parsing we'd been doing here had the consequence that it could return true for some URL strings that in reality resolve to URLs that aren't on the Zulip realm. If another (authenticated) user sends a message containing an image link crafted to trigger this bug, and the receiving user taps on the image in order to expand it in the lightbox, this could cause the user's login credentials to be disclosed. This is CVE-2022-35962. To fix the issue, use `new URL` to parse the URL, the same way we're going to do at each of its two call sites. This still isn't a great structure, because it'd be easy for a call site to change (or a new one to be added) so that the URL gets used in a way other than by passing to our `new URL`, or with a different base URL. Add a TODO to refactor accordingly, and mark the function as deprecated to discourage new callers.
1 parent d3781b2 commit 4202744

File tree

2 files changed

+32
-10
lines changed

2 files changed

+32
-10
lines changed

src/utils/__tests__/url-test.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,22 @@ describe('isUrlOnRealm', () => {
118118

119119
test('when link is on realm, return true', () => {
120120
expect(isUrlOnRealm('/#narrow/stream/jest', realm)).toBe(true);
121-
122121
expect(isUrlOnRealm('https://example.com/#narrow/stream/jest', realm)).toBe(true);
123-
124122
expect(isUrlOnRealm('#narrow/#near/1', realm)).toBe(true);
125-
});
126123

127-
test('when link is on not realm, return false', () => {
128-
expect(isUrlOnRealm('https://demo.example.com', realm)).toBe(false);
124+
// Absolutizes to https://example.com/www.google.com
125+
expect(isUrlOnRealm('www.google.com', realm)).toBeTrue();
126+
127+
expect(isUrlOnRealm('https://example.com', realm)).toBeTrue();
128+
expect(isUrlOnRealm('https://example.com/', realm)).toBeTrue();
129+
expect(isUrlOnRealm('https://example.com/foo/bar.baz', realm)).toBeTrue();
130+
});
129131

130-
expect(isUrlOnRealm('www.google.com', realm)).toBe(false);
132+
test('when link is not on realm, return false', () => {
133+
expect(isUrlOnRealm('https://demo.example.com/', realm)).toBeFalse();
134+
expect(isUrlOnRealm('https://demo.example/', realm)).toBeFalse();
135+
expect(isUrlOnRealm('//demo.example/', realm)).toBeFalse();
136+
expect(isUrlOnRealm(' https://demo.example/', realm)).toBeFalse();
131137
});
132138
});
133139

src/utils/url.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,26 @@ export const tryParseUrl = (url: string, base?: string | URL): URL | void => {
9191
}
9292
};
9393

94-
// TODO: Work out what this does, write a jsdoc for its interface, and
95-
// reimplement using URL object (not just for the realm)
96-
export const isUrlOnRealm = (url: string, realm: URL): boolean =>
97-
url.startsWith('/') || url.startsWith(realm.toString()) || !/^(http|www.)/i.test(url);
94+
/**
95+
* Test if the given URL string resolves on the realm, with realm as base.
96+
*
97+
* DEPRECATED because URL strings are complicated. Callers should construct
98+
* a URL object before this point, and use the same URL object both for
99+
* asking any questions like this one and for subsequently making any
100+
* network requests.
101+
*
102+
* This returns true just if `new URL(url, realm)` gives a URL that is
103+
* within the same Zulip realm.
104+
*
105+
* This performs a call to `new URL` and therefore may take a fraction of a
106+
* millisecond. Avoid using in a context where it might be called more than
107+
* 10 or 100 times per user action.
108+
*/
109+
// TODO: Take a URL object instead of a string, and remove warnings in jsdoc.
110+
export const isUrlOnRealm = (url: string, realm: URL): boolean => {
111+
const parsedUrl = tryParseUrl(url, realm);
112+
return parsedUrl ? parsedUrl.origin === realm.origin : false;
113+
};
98114

99115
const getResourceWithAuth = (uri: string, auth: Auth) => ({
100116
uri: new URL(uri, auth.realm).toString(),

0 commit comments

Comments
 (0)