Skip to content

Commit 4c2e444

Browse files
Copilotggazzoclaude
authored
chore: Update coerceTypes to false and modify mocharc settings (RocketChat#39559)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3e753c9 commit 4c2e444

88 files changed

Lines changed: 466 additions & 281 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/strict-ajv-coercion.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@rocket.chat/rest-typings": minor
3+
"@rocket.chat/meteor": patch
4+
---
5+
6+
Splits the single AJV validator instance into two: `ajv` (coerceTypes: false) for request **body** validation and `ajvQuery` (coerceTypes: true) for **query parameter** validation.
7+
8+
**Why this matters:** Previously, a single AJV instance with `coerceTypes: true` was used everywhere. This silently accepted values with wrong types — for example, sending `{ "rid": 12345 }` (number) where a string was expected would pass validation because `12345` was coerced to `"12345"`. With this change, body validation is now strict: the server will reject payloads with incorrect types instead of silently coercing them.
9+
10+
**What may break for API consumers:**
11+
12+
- **Numeric values sent as strings in POST/PUT/PATCH bodies** (e.g., `{ "count": "10" }` instead of `{ "count": 10 }`) will now be rejected. Ensure JSON bodies use proper types.
13+
- **Boolean values sent as strings in bodies** (e.g., `{ "readThreads": "true" }` instead of `{ "readThreads": true }`) will now be rejected.
14+
- **`null` values where a string is expected** (e.g., `{ "name": null }` for a `type: 'string'` field without `nullable: true`) will no longer be coerced to `""`.
15+
16+
**No change for query parameters:** GET query params (e.g., `?count=10&offset=0`) continue to be coerced via `ajvQuery`, since HTTP query strings are always strings.

apps/meteor/.mocharc.api.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
module.exports = /** @satisfies {import('mocha').MochaOptions} */ ({
88
...require('./.mocharc.base.json'), // see https://github.com/mochajs/mocha/issues/3916
99
timeout: 10000,
10-
bail: true,
10+
bail: false,
1111
retries: 0,
1212
file: 'tests/end-to-end/teardown.ts',
1313
spec: ['tests/end-to-end/api/*.ts', 'tests/end-to-end/api/helpers/**/*', 'tests/end-to-end/api/methods/**/*', 'tests/end-to-end/apps/*'],

apps/meteor/app/api/server/ajv.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { schemas } from '@rocket.chat/core-typings';
2-
import { ajv } from '@rocket.chat/rest-typings';
2+
import { ajv, ajvQuery } from '@rocket.chat/rest-typings';
33

44
const components = schemas.components?.schemas;
55
if (components) {
66
for (const key in components) {
77
if (Object.prototype.hasOwnProperty.call(components, key)) {
8-
ajv.addSchema(components[key], `#/components/schemas/${key}`);
8+
const uri = `#/components/schemas/${key}`;
9+
ajv.addSchema(components[key], uri);
10+
ajvQuery.addSchema(components[key], uri);
911
}
1012
}
1113
}

apps/meteor/app/api/server/v1/call-history.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CallHistory, MediaCalls } from '@rocket.chat/models';
33
import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings';
44
import {
55
ajv,
6+
ajvQuery,
67
validateNotFoundErrorResponse,
78
validateBadRequestErrorResponse,
89
validateUnauthorizedErrorResponse,
@@ -61,7 +62,7 @@ const CallHistoryListSchema = {
6162
additionalProperties: false,
6263
};
6364

64-
export const isCallHistoryListProps = ajv.compile<CallHistoryList>(CallHistoryListSchema);
65+
export const isCallHistoryListProps = ajvQuery.compile<CallHistoryList>(CallHistoryListSchema);
6566

6667
const callHistoryListEndpoints = API.v1.get(
6768
'call-history.list',
@@ -185,7 +186,7 @@ const CallHistoryInfoSchema = {
185186
],
186187
};
187188

188-
export const isCallHistoryInfoProps = ajv.compile<CallHistoryInfo>(CallHistoryInfoSchema);
189+
export const isCallHistoryInfoProps = ajvQuery.compile<CallHistoryInfo>(CallHistoryInfoSchema);
189190

190191
const callHistoryInfoEndpoints = API.v1.get(
191192
'call-history.info',

apps/meteor/app/api/server/v1/commands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Apps } from '@rocket.chat/apps';
22
import type { SlashCommand } from '@rocket.chat/core-typings';
33
import { Messages } from '@rocket.chat/models';
44
import { Random } from '@rocket.chat/random';
5-
import { ajv, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings';
5+
import { ajv, ajvQuery, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings';
66
import objectPath from 'object-path';
77

88
import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom';
@@ -24,7 +24,7 @@ const CommandsGetParamsSchema = {
2424
additionalProperties: false,
2525
};
2626

27-
const isCommandsGetParams = ajv.compile<CommandsGetParams>(CommandsGetParamsSchema);
27+
const isCommandsGetParams = ajvQuery.compile<CommandsGetParams>(CommandsGetParamsSchema);
2828

2929
const commandsEndpoints = API.v1.get(
3030
'commands.get',

apps/meteor/app/api/server/v1/custom-user-status.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ICustomUserStatus } from '@rocket.chat/core-typings';
22
import { CustomUserStatus } from '@rocket.chat/models';
3-
import { ajv, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings';
3+
import { ajv, ajvQuery, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings';
44
import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings';
55
import { escapeRegExp } from '@rocket.chat/string-helpers';
66
import { Match, check } from 'meteor/check';
@@ -46,7 +46,7 @@ const CustomUserStatusListSchema = {
4646
additionalProperties: false,
4747
};
4848

49-
const isCustomUserStatusListProps = ajv.compile<CustomUserStatusListProps>(CustomUserStatusListSchema);
49+
const isCustomUserStatusListProps = ajvQuery.compile<CustomUserStatusListProps>(CustomUserStatusListSchema);
5050

5151
const customUserStatusEndpoints = API.v1.get(
5252
'custom-user-status.list',

apps/meteor/app/api/server/v1/e2e.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { IRoom, ISubscription, IUser } from '@rocket.chat/core-typings';
22
import { Subscriptions, Users } from '@rocket.chat/models';
33
import {
44
ajv,
5+
ajvQuery,
56
validateUnauthorizedErrorResponse,
67
validateBadRequestErrorResponse,
78
validateForbiddenErrorResponse,
@@ -164,7 +165,9 @@ const ise2eGetUsersOfRoomWithoutKeyParamsGET = ajv.compile<e2eGetUsersOfRoomWith
164165

165166
const ise2eUpdateGroupKeyParamsPOST = ajv.compile<e2eUpdateGroupKeyParamsPOST>(e2eUpdateGroupKeyParamsPOSTSchema);
166167

167-
const isE2EFetchUsersWaitingForGroupKeyProps = ajv.compile<E2EFetchUsersWaitingForGroupKeyProps>(E2EFetchUsersWaitingForGroupKeySchema);
168+
const isE2EFetchUsersWaitingForGroupKeyProps = ajvQuery.compile<E2EFetchUsersWaitingForGroupKeyProps>(
169+
E2EFetchUsersWaitingForGroupKeySchema,
170+
);
168171

169172
const isE2EProvideUsersGroupKeyProps = ajv.compile<E2EProvideUsersGroupKeyProps>(E2EProvideUsersGroupKeySchema);
170173

@@ -457,7 +460,6 @@ const e2eEndpoints = API.v1
457460
},
458461
},
459462
async function action() {
460-
// eslint-disable-next-line @typescript-eslint/naming-convention
461463
const { public_key, private_key, force } = this.bodyParams;
462464

463465
await setUserPublicAndPrivateKeysMethod(this.userId, {

apps/meteor/app/api/server/v1/misc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ const spotlightResponseSchema = ajv.compile<{
348348
statusText: { type: 'string' },
349349
avatarETag: { type: 'string' },
350350
},
351-
required: ['_id', 'name', 'username', 'status', 'statusText'],
351+
required: ['_id', 'name', 'username', 'status'],
352352
additionalProperties: true,
353353
},
354354
},

apps/meteor/app/api/server/v1/oauthapps.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { IOAuthApps } from '@rocket.chat/core-typings';
22
import { OAuthApps } from '@rocket.chat/models';
33
import {
44
ajv,
5+
ajvQuery,
56
validateUnauthorizedErrorResponse,
67
validateBadRequestErrorResponse,
78
validateForbiddenErrorResponse,
@@ -114,14 +115,14 @@ const oauthAppsGetParamsSchema = {
114115
],
115116
};
116117

117-
const isOauthAppsGetParams = ajv.compile<OauthAppsGetParams>(oauthAppsGetParamsSchema);
118+
const isOauthAppsGetParams = ajvQuery.compile<OauthAppsGetParams>(oauthAppsGetParamsSchema);
118119

119120
const oauthAppsEndpoints = API.v1
120121
.get(
121122
'oauth-apps.list',
122123
{
123124
authRequired: true,
124-
query: ajv.compile<{ uid?: string }>({
125+
query: ajvQuery.compile<{ uid?: string }>({
125126
type: 'object',
126127
properties: {
127128
uid: {

apps/meteor/app/api/server/v1/permissions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { IPermission } from '@rocket.chat/core-typings';
22
import { Permissions, Roles } from '@rocket.chat/models';
33
import {
44
ajv,
5+
ajvQuery,
56
validateUnauthorizedErrorResponse,
67
validateBadRequestErrorResponse,
78
validateForbiddenErrorResponse,
@@ -57,7 +58,7 @@ const permissionUpdatePropsSchema = {
5758
additionalProperties: false,
5859
};
5960

60-
const isPermissionsListAll = ajv.compile<PermissionsListAllProps>(permissionListAllSchema);
61+
const isPermissionsListAll = ajvQuery.compile<PermissionsListAllProps>(permissionListAllSchema);
6162

6263
const isBodyParamsValidPermissionUpdate = ajv.compile<PermissionsUpdateProps>(permissionUpdatePropsSchema);
6364

0 commit comments

Comments
 (0)