Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/eight-games-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/server-fetch': minor
'@rocket.chat/meteor': minor
---

Refactors checkUrlForSsrf as checkForSsrf adding better coverage and DNS rebinding protection
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const getSupportedVersionsFromCloud = async () => {
fetch(releaseEndpoint, {
headers,
timeout: 5000,
ignoreSsrfValidation: true,
}),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ class RocketChatIntegrationHandler {
headers: opts.headers,
...(opts.timeout && { timeout: opts.timeout }),
...(opts.data && { body: opts.data }),
ignoreSsrfValidation: true,
},
settings.get('Allow_Invalid_SelfSigned_Certs'),
)
Expand Down
100 changes: 0 additions & 100 deletions apps/meteor/app/lib/server/functions/checkUrlForSsrf.ts

This file was deleted.

11 changes: 1 addition & 10 deletions apps/meteor/app/lib/server/functions/setUserAvatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { Meteor } from 'meteor/meteor';
import type { ClientSession } from 'mongodb';

import { checkUrlForSsrf } from './checkUrlForSsrf';
import { onceTransactionCommitedSuccessfully } from '../../../../server/database/utils';
import { SystemLogger } from '../../../../server/lib/logger/system';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
Expand Down Expand Up @@ -103,16 +102,8 @@ export async function setUserAvatar(
if (service === 'url' && typeof dataURI === 'string') {
let response: Response;

const isSsrfSafe = await checkUrlForSsrf(dataURI);
if (!isSsrfSafe) {
throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${encodeURI(dataURI)}`, {
function: 'setUserAvatar',
url: dataURI,
});
}

try {
response = await fetch(dataURI, { redirect: 'error' });
response = await fetch(dataURI);
} catch (e) {
SystemLogger.info(`Not a valid response, from the avatar url: ${encodeURI(dataURI)}`);
throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${encodeURI(dataURI)}`, {
Expand Down
8 changes: 1 addition & 7 deletions apps/meteor/app/livechat/imports/server/rest/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { Meteor } from 'meteor/meteor';
import { getFileExtension } from '../../../../../lib/utils/getFileExtension';
import { API } from '../../../../api/server';
import { FileUpload } from '../../../../file-upload/server';
import { checkUrlForSsrf } from '../../../../lib/server/functions/checkUrlForSsrf';
import { settings } from '../../../../settings/server';
import { setCustomField } from '../../../server/api/lib/customFields';
import type { ILivechatMessage } from '../../../server/lib/localTypes';
Expand All @@ -28,12 +27,7 @@ import { createRoom } from '../../../server/lib/rooms';
const logger = new Logger('SMS');

const getUploadFile = async (details: Omit<IUpload, '_id'>, fileUrl: string) => {
const isSsrfSafe = await checkUrlForSsrf(fileUrl);
if (!isSsrfSafe) {
throw new Meteor.Error('error-invalid-url', 'Invalid URL');
}

const response = await fetch(fileUrl, { redirect: 'error' });
const response = await fetch(fileUrl);

const content = Buffer.from(await response.arrayBuffer());

Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/livechat/server/api/v1/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ API.v1.addRoute(
'Accept': 'application/json',
},
body: sampleData,
ignoreSsrfValidation: true,
};

const webhookUrl = settings.get<string>('Livechat_webhookUrl');
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/livechat/server/lib/routing/External.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class ExternalQueue implements IRoutingMethod {
...(department && { departmentId: department }),
...(ignoreAgentId && { ignoreAgentId }),
},
ignoreSsrfValidation: true,
});
const result = (await request.json()) as { username?: string };

Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/livechat/server/lib/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export async function sendRequest(
},
body: postData,
timeout,
ignoreSsrfValidation: true,
});

if (result.status === 200) {
Expand Down
32 changes: 23 additions & 9 deletions apps/meteor/ee/server/apps/communication/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export class AppsRestApi {

if (this.bodyParams.url) {
try {
const response = await fetch(this.bodyParams.url);
const response = await fetch(this.bodyParams.url, { ignoreSsrfValidation: true });

if (response.status !== 200 || response.headers.get('content-type') !== 'application/zip') {
return API.v1.failure({
Expand All @@ -282,6 +282,7 @@ export class AppsRestApi {
Apps.getMarketplaceClient()
.fetch(`v2/apps/${this.bodyParams.appId}/download/${this.bodyParams.version}?token=${downloadToken}`, {
headers,
ignoreSsrfValidation: true,
})
.catch((cause) => {
throw new Error('App package download failed', { cause });
Expand All @@ -292,6 +293,7 @@ export class AppsRestApi {
Authorization: `Bearer ${marketplaceToken}`,
...headers,
},
ignoreSsrfValidation: true,
})
.catch((cause) => {
throw new Error('App metadata download failed', { cause });
Expand Down Expand Up @@ -532,7 +534,9 @@ export class AppsRestApi {

let result;
try {
const request = await orchestrator.getMarketplaceClient().fetch(`v1/bundles/${this.urlParams.id}/apps`, { headers });
const request = await orchestrator
.getMarketplaceClient()
.fetch(`v1/bundles/${this.urlParams.id}/apps`, { headers, ignoreSsrfValidation: true });
if (request.status !== 200) {
orchestrator.getRocketChatLogger().error("Error getting the Bundle's Apps from the Marketplace:", await request.json());
return API.v1.failure();
Expand Down Expand Up @@ -561,7 +565,7 @@ export class AppsRestApi {

let result;
try {
const request = await orchestrator.getMarketplaceClient().fetch(`v1/featured-apps`, { headers });
const request = await orchestrator.getMarketplaceClient().fetch(`v1/featured-apps`, { headers, ignoreSsrfValidation: true });
if (request.status !== 200) {
orchestrator.getRocketChatLogger().error('Error getting the Featured Apps from the Marketplace:', await request.json());
return API.v1.failure();
Expand Down Expand Up @@ -594,6 +598,7 @@ export class AppsRestApi {
.getMarketplaceClient()
.fetch(`v1/app-request?appId=${appId}&q=${q}&sort=${sort}&limit=${limit}&offset=${offset}`, {
headers,
ignoreSsrfValidation: true,
});
const result = await request.json();

Expand Down Expand Up @@ -623,7 +628,9 @@ export class AppsRestApi {
}

try {
const request = await orchestrator.getMarketplaceClient().fetch(`v1/app-request/stats`, { headers });
const request = await orchestrator
.getMarketplaceClient()
.fetch(`v1/app-request/stats`, { headers, ignoreSsrfValidation: true });
const result = await request.json();
if (!request.ok) {
throw new Error(result.error);
Expand Down Expand Up @@ -657,6 +664,7 @@ export class AppsRestApi {
method: 'POST',
headers,
body: { ids: unseenRequests },
ignoreSsrfValidation: true,
});
const result = await request.json();

Expand Down Expand Up @@ -727,7 +735,7 @@ export class AppsRestApi {
try {
const request = await orchestrator
.getMarketplaceClient()
.fetch(`v1/apps/${this.urlParams.id}?appVersion=${this.queryParams.version}`, { headers });
.fetch(`v1/apps/${this.urlParams.id}?appVersion=${this.queryParams.version}`, { headers, ignoreSsrfValidation: true });
if (request.status !== 200) {
orchestrator.getRocketChatLogger().error('Error getting the App information from the Marketplace:', await request.json());
return API.v1.failure();
Expand All @@ -753,6 +761,7 @@ export class AppsRestApi {
.getMarketplaceClient()
.fetch(`v1/apps/${this.urlParams.id}/latest?appVersion=${this.queryParams.appVersion}`, {
headers,
ignoreSsrfValidation: true,
});
if (request.status !== 200) {
orchestrator.getRocketChatLogger().error('Error getting the App update info from the Marketplace:', await request.json());
Expand Down Expand Up @@ -780,7 +789,7 @@ export class AppsRestApi {
let isPrivateAppUpload = false;

if (this.bodyParams.url) {
const response = await fetch(this.bodyParams.url);
const response = await fetch(this.bodyParams.url, { ignoreSsrfValidation: true });

if (response.status !== 200 || response.headers.get('content-type') !== 'application/zip') {
return API.v1.failure({
Expand All @@ -798,6 +807,7 @@ export class AppsRestApi {
.getMarketplaceClient()
.fetch(`v2/apps/${this.bodyParams.appId}/download/${this.bodyParams.version}?token=${token}`, {
headers,
ignoreSsrfValidation: true,
});

if (response.status !== 200) {
Expand Down Expand Up @@ -934,7 +944,9 @@ export class AppsRestApi {
let result;
let statusCode;
try {
const request = await orchestrator.getMarketplaceClient().fetch(`v1/apps/${this.urlParams.id}`, { headers });
const request = await orchestrator
.getMarketplaceClient()
.fetch(`v1/apps/${this.urlParams.id}`, { headers, ignoreSsrfValidation: true });
statusCode = request.status;
result = await request.json();

Expand Down Expand Up @@ -976,7 +988,7 @@ export class AppsRestApi {
try {
const request = await orchestrator
.getMarketplaceClient()
.fetch(`v1/workspaces/${workspaceIdSetting.value}/apps/${this.urlParams.id}`, { headers });
.fetch(`v1/workspaces/${workspaceIdSetting.value}/apps/${this.urlParams.id}`, { headers, ignoreSsrfValidation: true });

statusCode = request.status;
result = await request.json();
Expand Down Expand Up @@ -1041,7 +1053,9 @@ export class AppsRestApi {
const headers = getDefaultHeaders();

try {
const request = await orchestrator.getMarketplaceClient().fetch(`v1/apps/${appId}/screenshots`, { headers });
const request = await orchestrator
.getMarketplaceClient()
.fetch(`v1/apps/${appId}/screenshots`, { headers, ignoreSsrfValidation: true });
const data = await request.json();

return API.v1.success({
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const getAndCreateNpsSurvey = async function getNpsSurvey(npsId: string)
headers: {
Authorization: `Bearer ${token}`,
},
ignoreSsrfValidation: true,
});

if (!result.ok) {
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/server/services/nps/sendNpsResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const sendNpsResults = async function sendNpsResults(npsId: string, data:
Authorization: `Bearer ${token}`,
},
body: data,
ignoreSsrfValidation: true,
})
).json();
} catch (e) {
Expand Down
Loading
Loading