Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 4904da0459 fix(rest): validate unknown query parameters and improve pagination parameter discoverability
https://sonarly.com/issue/30901?type=bug

The REST API pagination parameter is `starting_after` (not `cursor`), but the API silently ignores unrecognized query parameters, causing users who guess the wrong parameter name to get an infinite loop of duplicate records.

Fix: Added detection of commonly-misused pagination parameter names in both cursor parsers. When a user passes `cursor`, `after`, `lastCursor`, `last_cursor`, `startingAfter`, or `page_token` instead of `starting_after` (or `before`, `endingBefore`, `ending` instead of `ending_before`), the API now returns a 400 Bad Request with a clear error message like: "Unknown pagination parameter 'cursor'. Use 'starting_after' instead".

Changes:
1. `parse-starting-after-rest-request.util.ts` — Added a check for 6 common wrong parameter names. When `starting_after` is absent but one of these is present, throws `RestInputRequestParserException` with the new `INVALID_CURSOR_QUERY_PARAM` code. When `starting_after` IS present, wrong names are ignored (no false positives).
2. `parse-ending-before-rest-request.util.ts` — Same pattern for 3 common wrong names for the ending_before parameter.
3. `rest-input-request-parser.exception.ts` — Added `INVALID_CURSOR_QUERY_PARAM` enum value and its user-friendly message in the switch statement. The existing `assertUnreachable` default ensures compile-time exhaustiveness.
4. Both test files updated with cases for wrong parameter detection and a case confirming no false positive when the correct parameter is also present.

The existing exception handler already maps `RestInputRequestParserException` → `BadRequestException` (HTTP 400), so no handler changes were needed.
2026-04-25 04:47:27 +00:00
5 changed files with 97 additions and 0 deletions
@@ -1,4 +1,5 @@
import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util';
import { RestInputRequestParserExceptionCode } from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
describe('parseEndingBeforeRestRequest', () => {
it('should return default if ending_before missing', () => {
@@ -12,4 +13,26 @@ describe('parseEndingBeforeRestRequest', () => {
expect(parseEndingBeforeRestRequest(request)).toEqual('uuid');
});
it('should throw when using wrong pagination parameter name', () => {
const wrongNames = ['before', 'endingBefore', 'ending'];
for (const wrongName of wrongNames) {
const request: any = { query: { [wrongName]: 'some-cursor' } };
expect(() => parseEndingBeforeRestRequest(request)).toThrow(
expect.objectContaining({
code: RestInputRequestParserExceptionCode.INVALID_CURSOR_QUERY_PARAM,
}),
);
}
});
it('should not throw for wrong names when ending_before is also provided', () => {
const request: any = {
query: { ending_before: 'uuid', before: 'ignored' },
};
expect(parseEndingBeforeRestRequest(request)).toEqual('uuid');
});
});
@@ -1,12 +1,31 @@
import {
RestInputRequestParserException,
RestInputRequestParserExceptionCode,
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
const ENDING_BEFORE_WRONG_PARAM_NAMES = [
'before',
'endingBefore',
'ending',
];
export const parseEndingBeforeRestRequest = (
request: AuthenticatedRequest | RequestContext,
): string | undefined => {
const cursorQuery = request.query?.ending_before;
if (typeof cursorQuery !== 'string') {
for (const wrongName of ENDING_BEFORE_WRONG_PARAM_NAMES) {
if (typeof request.query?.[wrongName] === 'string') {
throw new RestInputRequestParserException(
`Unknown pagination parameter '${wrongName}'. Use 'ending_before' instead`,
RestInputRequestParserExceptionCode.INVALID_CURSOR_QUERY_PARAM,
);
}
}
return undefined;
}
@@ -12,6 +12,7 @@ export enum RestInputRequestParserExceptionCode {
INVALID_DEPTH_QUERY_PARAM = 'INVALID_DEPTH_QUERY_PARAM',
INVALID_LIMIT_QUERY_PARAM = 'INVALID_LIMIT_QUERY_PARAM',
INVALID_FILTER_QUERY_PARAM = 'INVALID_FILTER_QUERY_PARAM',
INVALID_CURSOR_QUERY_PARAM = 'INVALID_CURSOR_QUERY_PARAM',
}
const getRestInputRequestParserExceptionUserFriendlyMessage = (
@@ -32,6 +33,8 @@ const getRestInputRequestParserExceptionUserFriendlyMessage = (
return msg`Invalid limit parameter.`;
case RestInputRequestParserExceptionCode.INVALID_FILTER_QUERY_PARAM:
return msg`Invalid filter parameter.`;
case RestInputRequestParserExceptionCode.INVALID_CURSOR_QUERY_PARAM:
return msg`Invalid cursor parameter.`;
default:
assertUnreachable(code);
}
@@ -1,3 +1,4 @@
import { RestInputRequestParserExceptionCode } from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
import { parseStartingAfterRestRequest } from 'src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util';
describe('parseStartingAfterRestRequest', () => {
@@ -12,4 +13,33 @@ describe('parseStartingAfterRestRequest', () => {
expect(parseStartingAfterRestRequest(request)).toEqual('uuid');
});
it('should throw when using wrong pagination parameter name', () => {
const wrongNames = [
'cursor',
'after',
'lastCursor',
'last_cursor',
'startingAfter',
'page_token',
];
for (const wrongName of wrongNames) {
const request: any = { query: { [wrongName]: 'some-cursor' } };
expect(() => parseStartingAfterRestRequest(request)).toThrow(
expect.objectContaining({
code: RestInputRequestParserExceptionCode.INVALID_CURSOR_QUERY_PARAM,
}),
);
}
});
it('should not throw for wrong names when starting_after is also provided', () => {
const request: any = {
query: { starting_after: 'uuid', cursor: 'ignored' },
};
expect(parseStartingAfterRestRequest(request)).toEqual('uuid');
});
});
@@ -1,12 +1,34 @@
import {
RestInputRequestParserException,
RestInputRequestParserExceptionCode,
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
const STARTING_AFTER_WRONG_PARAM_NAMES = [
'cursor',
'after',
'lastCursor',
'last_cursor',
'startingAfter',
'page_token',
];
export const parseStartingAfterRestRequest = (
request: AuthenticatedRequest | RequestContext,
): string | undefined => {
const cursorQuery = request.query?.starting_after;
if (typeof cursorQuery !== 'string') {
for (const wrongName of STARTING_AFTER_WRONG_PARAM_NAMES) {
if (typeof request.query?.[wrongName] === 'string') {
throw new RestInputRequestParserException(
`Unknown pagination parameter '${wrongName}'. Use 'starting_after' instead`,
RestInputRequestParserExceptionCode.INVALID_CURSOR_QUERY_PARAM,
);
}
}
return undefined;
}