Skip to content

Commit cdca5a9

Browse files
authored
Merge pull request #1564 from rocket-admin/backend_security_report
add URL host validation for action URLs
2 parents 4395b28 + f1d2250 commit cdca5a9

File tree

5 files changed

+1567
-1502
lines changed

5 files changed

+1567
-1502
lines changed

backend/src/entities/table-actions/table-action-rules-module/use-cases/create-action-rule.use.case.ts

Lines changed: 114 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -14,118 +14,130 @@ import { buildTableActionWithRule } from '../../table-actions-module/utils/build
1414
import { buildActionEventWithRule } from '../../table-action-events-module/utils/build-action-event-with-rule.util.js';
1515
import { buildFoundActionRulesWithActionsAndEventsDTO } from '../utils/build-found-action-rules-with-actions-and-events-dto.util.js';
1616
import { validateStringWithEnum } from '../../../../helpers/validators/validate-string-with-enum.js';
17+
import { isActionUrlHostAllowed } from '../../../../helpers/validators/is-action-url-host-allowed.js';
1718

1819
@Injectable({ scope: Scope.REQUEST })
1920
export class CreateActionRuleUseCase
20-
extends AbstractUseCase<CreateActionRuleDS, FoundActionRulesWithActionsAndEventsDTO>
21-
implements ICreateActionRule
21+
extends AbstractUseCase<CreateActionRuleDS, FoundActionRulesWithActionsAndEventsDTO>
22+
implements ICreateActionRule
2223
{
23-
constructor(
24-
@Inject(BaseType.GLOBAL_DB_CONTEXT)
25-
protected _dbContext: IGlobalDatabaseContext,
26-
) {
27-
super();
28-
}
24+
constructor(
25+
@Inject(BaseType.GLOBAL_DB_CONTEXT)
26+
protected _dbContext: IGlobalDatabaseContext,
27+
) {
28+
super();
29+
}
2930

30-
protected async implementation(inputData: CreateActionRuleDS): Promise<FoundActionRulesWithActionsAndEventsDTO> {
31-
const { connection_data, action_events_data, rule_data, table_actions_data } = inputData;
32-
const { connectionId, masterPwd, userId } = connection_data;
33-
const { table_name } = rule_data;
31+
protected async implementation(inputData: CreateActionRuleDS): Promise<FoundActionRulesWithActionsAndEventsDTO> {
32+
const { connection_data, action_events_data, rule_data, table_actions_data } = inputData;
33+
const { connectionId, masterPwd, userId } = connection_data;
34+
const { table_name } = rule_data;
3435

35-
await this.validateActionEmailsOrThrowException(table_actions_data, userId);
36-
await this.validateTableNameOrThrowException(table_name, connectionId, masterPwd);
37-
await this.validateTableActionDataOrThrowException(table_actions_data);
36+
await this.validateActionEmailsOrThrowException(table_actions_data, userId);
37+
await this.validateTableNameOrThrowException(table_name, connectionId, masterPwd);
38+
await this.validateTableActionDataOrThrowException(table_actions_data);
3839

39-
const foundConnection = await this._dbContext.connectionRepository.findOne({ where: { id: connectionId } });
40-
const newActionRule = buildEmptyActionRule(rule_data, foundConnection);
41-
const savedActionRule = await this._dbContext.actionRulesRepository.saveNewOrUpdatedActionRule(newActionRule);
40+
const foundConnection = await this._dbContext.connectionRepository.findOne({ where: { id: connectionId } });
41+
const newActionRule = buildEmptyActionRule(rule_data, foundConnection);
42+
const savedActionRule = await this._dbContext.actionRulesRepository.saveNewOrUpdatedActionRule(newActionRule);
4243

43-
const savedTableActions = await Promise.all(
44-
table_actions_data.map((tableAction) => {
45-
const newTableAction = buildTableActionWithRule(tableAction, savedActionRule);
46-
return this._dbContext.tableActionRepository.saveNewOrUpdatedTableAction(newTableAction);
47-
}),
48-
);
44+
const savedTableActions = await Promise.all(
45+
table_actions_data.map((tableAction) => {
46+
const newTableAction = buildTableActionWithRule(tableAction, savedActionRule);
47+
return this._dbContext.tableActionRepository.saveNewOrUpdatedTableAction(newTableAction);
48+
}),
49+
);
4950

50-
const savedActionEvents = await Promise.all(
51-
action_events_data.map((actionEvent) => {
52-
const newActionEvent = buildActionEventWithRule(actionEvent, savedActionRule);
53-
return this._dbContext.actionEventsRepository.saveNewOrUpdatedActionEvent(newActionEvent);
54-
}),
55-
);
56-
savedActionRule.action_events = savedActionEvents;
57-
savedActionRule.table_actions = savedTableActions;
58-
return buildFoundActionRulesWithActionsAndEventsDTO(savedActionRule);
59-
}
51+
const savedActionEvents = await Promise.all(
52+
action_events_data.map((actionEvent) => {
53+
const newActionEvent = buildActionEventWithRule(actionEvent, savedActionRule);
54+
return this._dbContext.actionEventsRepository.saveNewOrUpdatedActionEvent(newActionEvent);
55+
}),
56+
);
57+
savedActionRule.action_events = savedActionEvents;
58+
savedActionRule.table_actions = savedTableActions;
59+
return buildFoundActionRulesWithActionsAndEventsDTO(savedActionRule);
60+
}
6061

61-
private async validateActionEmailsOrThrowException(
62-
table_actions_data: Array<CreateTableActionData>,
63-
userId: string,
64-
): Promise<void> {
65-
const companyWithUsers = await this._dbContext.companyInfoRepository.findUserCompanyWithUsers(userId);
66-
const usersInCompanyEmails = companyWithUsers.users.map((user) => user.email);
67-
const emailsFromEmailActions: Array<string> = table_actions_data.reduce((acc, table_action) => {
68-
if (table_action.action_method === TableActionMethodEnum.EMAIL) {
69-
return acc.concat(table_action.action_emails);
70-
}
71-
return acc;
72-
}, []);
73-
const emailsNotInCompany = emailsFromEmailActions.filter((email) => !usersInCompanyEmails.includes(email));
74-
if (emailsNotInCompany.length > 0) {
75-
throw new BadRequestException(Messages.EMAILS_NOT_IN_COMPANY(emailsNotInCompany));
76-
}
77-
const emailsNotVerified = emailsFromEmailActions.filter((email) => {
78-
const foundUser = companyWithUsers.users.find((user) => user.email === email);
79-
if (foundUser.id === userId) {
80-
return false;
81-
}
82-
return !foundUser.isActive;
83-
});
84-
if (emailsNotVerified.length > 0) {
85-
throw new BadRequestException(Messages.USERS_NOT_VERIFIED(emailsNotVerified));
86-
}
87-
}
62+
private async validateActionEmailsOrThrowException(
63+
table_actions_data: Array<CreateTableActionData>,
64+
userId: string,
65+
): Promise<void> {
66+
const companyWithUsers = await this._dbContext.companyInfoRepository.findUserCompanyWithUsers(userId);
67+
const usersInCompanyEmails = companyWithUsers.users.map((user) => user.email);
68+
const emailsFromEmailActions: Array<string> = table_actions_data.reduce((acc, table_action) => {
69+
if (table_action.action_method === TableActionMethodEnum.EMAIL) {
70+
return acc.concat(table_action.action_emails);
71+
}
72+
return acc;
73+
}, []);
74+
const emailsNotInCompany = emailsFromEmailActions.filter((email) => !usersInCompanyEmails.includes(email));
75+
if (emailsNotInCompany.length > 0) {
76+
throw new BadRequestException(Messages.EMAILS_NOT_IN_COMPANY(emailsNotInCompany));
77+
}
78+
const emailsNotVerified = emailsFromEmailActions.filter((email) => {
79+
const foundUser = companyWithUsers.users.find((user) => user.email === email);
80+
if (foundUser.id === userId) {
81+
return false;
82+
}
83+
return !foundUser.isActive;
84+
});
85+
if (emailsNotVerified.length > 0) {
86+
throw new BadRequestException(Messages.USERS_NOT_VERIFIED(emailsNotVerified));
87+
}
88+
}
8889

89-
private async validateTableNameOrThrowException(
90-
tableName: string,
91-
connectionId: string,
92-
masterPwd: string,
93-
): Promise<void> {
94-
const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection(
95-
connectionId,
96-
masterPwd,
97-
);
98-
const dao = getDataAccessObject(foundConnection);
99-
const tablesInConnection = await dao.getTablesFromDB();
100-
const tableNamesInConnection = tablesInConnection.map((table) => table.tableName);
101-
if (!tableNamesInConnection.includes(tableName)) {
102-
throw new BadRequestException(Messages.TABLE_WITH_NAME_NOT_EXISTS(tableName));
103-
}
104-
}
90+
private async validateTableNameOrThrowException(
91+
tableName: string,
92+
connectionId: string,
93+
masterPwd: string,
94+
): Promise<void> {
95+
const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection(
96+
connectionId,
97+
masterPwd,
98+
);
99+
const dao = getDataAccessObject(foundConnection);
100+
const tablesInConnection = await dao.getTablesFromDB();
101+
const tableNamesInConnection = tablesInConnection.map((table) => table.tableName);
102+
if (!tableNamesInConnection.includes(tableName)) {
103+
throw new BadRequestException(Messages.TABLE_WITH_NAME_NOT_EXISTS(tableName));
104+
}
105+
}
105106

106-
private async validateTableActionDataOrThrowException(tableActions: Array<CreateTableActionData>): Promise<void> {
107-
for (const action of tableActions) {
108-
if (action.action_method === TableActionMethodEnum.EMAIL) {
109-
if (!action.action_emails || action.action_emails.length === 0) {
110-
throw new BadRequestException(Messages.EMAILS_REQUIRED_FOR_EMAIL_ACTION);
111-
}
112-
}
113-
if (action.action_method === TableActionMethodEnum.SLACK) {
114-
if (!action.action_slack_url) {
115-
throw new BadRequestException(Messages.SLACK_URL_MISSING);
116-
}
117-
}
118-
if (!validateStringWithEnum(action.action_method, TableActionMethodEnum)) {
119-
throw new BadRequestException(Messages.INVALID_ACTION_METHOD(action.action_method));
120-
}
121-
if (action.action_method === TableActionMethodEnum.URL) {
122-
if (process.env.NODE_ENV === 'test') {
123-
return;
124-
}
125-
if (!action.action_url || !ValidationHelper.isValidUrl(action.action_url)) {
126-
throw new BadRequestException(Messages.URL_INVALID);
127-
}
128-
}
129-
}
130-
}
107+
private async validateTableActionDataOrThrowException(tableActions: Array<CreateTableActionData>): Promise<void> {
108+
for (const action of tableActions) {
109+
if (action.action_method === TableActionMethodEnum.EMAIL) {
110+
if (!action.action_emails || action.action_emails.length === 0) {
111+
throw new BadRequestException(Messages.EMAILS_REQUIRED_FOR_EMAIL_ACTION);
112+
}
113+
}
114+
if (action.action_method === TableActionMethodEnum.SLACK) {
115+
if (!action.action_slack_url) {
116+
throw new BadRequestException(Messages.SLACK_URL_MISSING);
117+
}
118+
if (process.env.NODE_ENV !== 'test' && !ValidationHelper.isValidUrl(action.action_slack_url)) {
119+
throw new BadRequestException(Messages.URL_INVALID);
120+
}
121+
const isSlackUrlAllowed = await isActionUrlHostAllowed(action.action_slack_url);
122+
if (!isSlackUrlAllowed) {
123+
throw new BadRequestException(Messages.ACTION_URL_HOST_NOT_ALLOWED);
124+
}
125+
}
126+
if (!validateStringWithEnum(action.action_method, TableActionMethodEnum)) {
127+
throw new BadRequestException(Messages.INVALID_ACTION_METHOD(action.action_method));
128+
}
129+
if (action.action_method === TableActionMethodEnum.URL) {
130+
if (
131+
process.env.NODE_ENV !== 'test' &&
132+
(!action.action_url || !ValidationHelper.isValidUrl(action.action_url))
133+
) {
134+
throw new BadRequestException(Messages.URL_INVALID);
135+
}
136+
const isUrlAllowed = await isActionUrlHostAllowed(action.action_url);
137+
if (!isUrlAllowed) {
138+
throw new BadRequestException(Messages.ACTION_URL_HOST_NOT_ALLOWED);
139+
}
140+
}
141+
}
142+
}
131143
}

0 commit comments

Comments
 (0)