Skip to content

Commit 3f8b9ac

Browse files
committed
prevent token from being send in header for cross origin url
1 parent 1a4527e commit 3f8b9ac

7 files changed

Lines changed: 123 additions & 3 deletions

File tree

app/containers/Avatar/Avatar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { Image } from 'expo-image';
44

55
import Emoji from '../markdown/components/emoji/Emoji';
66
import { getAvatarURL } from '../../lib/methods/helpers/getAvatarUrl';
7+
import { getAuthHeaders } from '../../lib/methods/helpers/getAuthHeaders';
78
import { SubscriptionType } from '../../definitions';
89
import { type IAvatar } from './interfaces';
910
import I18n from '../../i18n';
1011
import Touch from '../Touch';
11-
import sdk from '../../lib/services/sdk';
1212

1313
const Avatar = memo(
1414
({
@@ -81,7 +81,7 @@ const Avatar = memo(
8181
style={avatarStyle}
8282
source={{
8383
uri,
84-
headers: sdk.getHeaders()
84+
headers: uri ? getAuthHeaders(uri) : undefined
8585
}}
8686
priority='high'
8787
/>

app/lib/methods/helpers/fetch.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
jest.mock('../../services/sdk', () => ({
33
__esModule: true,
44
default: {
5+
server: 'https://open.rocket.chat',
56
getHeaders: jest.fn(),
67
setBasicAuth: jest.fn()
78
}
@@ -65,6 +66,24 @@ describe('helpers/fetch', () => {
6566
fetchMock.mockResolvedValueOnce(expected);
6667
await expect(customFetch('/api/v1/test')).resolves.toBe(expected);
6768
});
69+
70+
it('attaches session headers (X-Auth-Token/X-User-Id) for same-origin requests', async () => {
71+
(sdk.getHeaders as jest.Mock).mockReturnValue({ 'X-Auth-Token': 'tok', 'X-User-Id': 'uid' });
72+
await customFetch('https://open.rocket.chat/api/v1/settings');
73+
const call = fetchMock.mock.calls[0][1];
74+
expect(call.headers['X-Auth-Token']).toBe('tok');
75+
expect(call.headers['X-User-Id']).toBe('uid');
76+
});
77+
78+
it('omits session headers but keeps default headers for cross-origin requests', async () => {
79+
(sdk.getHeaders as jest.Mock).mockReturnValue({ 'User-Agent': 'RC Mobile', 'X-Auth-Token': 'tok', 'X-User-Id': 'uid' });
80+
await customFetch('https://releases.rocket.chat/v2/server/supportedVersions');
81+
const call = fetchMock.mock.calls[0][1];
82+
expect(call.headers['X-Auth-Token']).toBeUndefined();
83+
expect(call.headers['X-User-Id']).toBeUndefined();
84+
expect(typeof call.headers['User-Agent']).toBe('string');
85+
expect(call.headers['User-Agent']).toMatch(/RC Mobile/);
86+
});
6887
});
6988

7089
describe('helpers/fetch setBasicAuth', () => {

app/lib/methods/helpers/fetch.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import sdk from '../../services/sdk';
22
import { type DefaultHeaders, headers } from './defaultHeaders';
3+
import { getAuthHeaders } from './getAuthHeaders';
34

45
export type TMethods = 'POST' | 'GET' | 'DELETE' | 'PUT' | 'post' | 'get' | 'delete' | 'put';
56

@@ -19,6 +20,7 @@ export const setBasicAuth = (basicAuth: string | null): void => {
1920
export const BASIC_AUTH_KEY = 'BASIC_AUTH_KEY';
2021

2122
export default (url: string, options: IOptions = {}): Promise<Response> => {
22-
const customOptions = { ...options, headers: { ...sdk.getHeaders(), ...(options.headers || {}) } };
23+
const authHeaders = getAuthHeaders(url);
24+
const customOptions = { ...options, headers: { ...authHeaders, ...(options.headers || {}) } };
2325
return fetch(url, customOptions);
2426
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/* eslint-disable import/first */
2+
jest.mock('../../services/sdk', () => ({
3+
__esModule: true,
4+
default: {
5+
server: 'https://open.rocket.chat',
6+
getHeaders: jest.fn()
7+
}
8+
}));
9+
10+
import { getAuthHeaders } from './getAuthHeaders';
11+
import sdk from '../../services/sdk';
12+
13+
describe('getAuthHeaders', () => {
14+
beforeEach(() => {
15+
(sdk.getHeaders as jest.Mock).mockReset();
16+
});
17+
18+
it('returns full headers including the session token for same-origin URLs', () => {
19+
(sdk.getHeaders as jest.Mock).mockReturnValue({ 'User-Agent': 'RC Mobile', 'X-Auth-Token': 'tok', 'X-User-Id': 'uid' });
20+
const headers = getAuthHeaders('https://open.rocket.chat/api/v1/settings');
21+
expect(headers['X-Auth-Token']).toBe('tok');
22+
expect(headers['X-User-Id']).toBe('uid');
23+
expect(headers['User-Agent']).toBe('RC Mobile');
24+
});
25+
26+
it('strips only the session token for cross-origin URLs, keeping other headers', () => {
27+
(sdk.getHeaders as jest.Mock).mockReturnValue({
28+
'User-Agent': 'RC Mobile',
29+
Authorization: 'Basic abc',
30+
'X-Auth-Token': 'tok',
31+
'X-User-Id': 'uid'
32+
});
33+
const headers = getAuthHeaders('https://releases.rocket.chat/v2/server/supportedVersions');
34+
expect(headers['X-Auth-Token']).toBeUndefined();
35+
expect(headers['X-User-Id']).toBeUndefined();
36+
expect(headers['User-Agent']).toBe('RC Mobile');
37+
expect(headers.Authorization).toBe('Basic abc');
38+
});
39+
40+
it('treats relative URLs as same-origin', () => {
41+
(sdk.getHeaders as jest.Mock).mockReturnValue({ 'X-Auth-Token': 'tok' });
42+
const headers = getAuthHeaders('/api/v1/x');
43+
expect(headers['X-Auth-Token']).toBe('tok');
44+
});
45+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import sdk from '../../services/sdk';
2+
import { isSameOrigin } from './isSameOrigin';
3+
4+
/**
5+
* Returns the request headers for a given URL.
6+
* On same-origin requests the full header set is sent (including the session token).
7+
* On cross-origin requests only the session token (X-Auth-Token/X-User-Id) is stripped,
8+
* keeping non-sensitive headers such as User-Agent (and Basic auth) — matching the
9+
* pre-migration behavior where only User-Agent/Basic auth were ever sent off-origin.
10+
*/
11+
export const getAuthHeaders = (url: string): Record<string, string> => {
12+
if (isSameOrigin(url, sdk.server)) {
13+
return sdk.getHeaders();
14+
}
15+
const headers = { ...sdk.getHeaders() };
16+
delete headers['X-Auth-Token'];
17+
delete headers['X-User-Id'];
18+
return headers;
19+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { isSameOrigin } from './isSameOrigin';
2+
3+
describe('isSameOrigin', () => {
4+
test('returns true for same-origin absolute URLs', () => {
5+
expect(isSameOrigin('https://open.rocket.chat/api/v1/x', 'https://open.rocket.chat')).toBe(true);
6+
});
7+
8+
test('returns false for cross-origin absolute URLs', () => {
9+
expect(isSameOrigin('https://releases.rocket.chat/v2/server/supportedVersions', 'https://open.rocket.chat')).toBe(false);
10+
expect(isSameOrigin('https://external.provider.com/avatar/user', 'https://open.rocket.chat')).toBe(false);
11+
});
12+
13+
test('returns false when scheme differs', () => {
14+
expect(isSameOrigin('http://open.rocket.chat/avatar/x', 'https://open.rocket.chat')).toBe(false);
15+
});
16+
17+
test('returns true for relative URLs', () => {
18+
expect(isSameOrigin('/api/v1/x', 'https://open.rocket.chat')).toBe(true);
19+
});
20+
21+
test('returns true when origin is undefined', () => {
22+
expect(isSameOrigin('https://releases.rocket.chat/x')).toBe(true);
23+
});
24+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const isSameOrigin = (url: string, origin?: string): boolean => {
2+
if (!origin) {
3+
return true;
4+
}
5+
try {
6+
return new URL(url).origin === new URL(origin).origin;
7+
} catch {
8+
// Relative/internal URL — safe to attach auth headers.
9+
return true;
10+
}
11+
};

0 commit comments

Comments
 (0)