Skip to content

Commit 4d3465c

Browse files
authored
test: Rate limit requestMethods is scoped to the configured HTTP methods (#10520)
1 parent 37039b0 commit 4d3465c

4 files changed

Lines changed: 129 additions & 3 deletions

File tree

spec/RateLimit.spec.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,132 @@ describe('rate limit', () => {
12251225
});
12261226
});
12271227

1228+
it('does not apply a requestMethods POST-only limit to direct GET login requests', async () => {
1229+
// `requestMethods` scopes a limit to the listed request methods. `/login` is
1230+
// reachable via both GET and POST, so a POST-only limit intentionally does not
1231+
// apply to GET login requests; operators must list all methods or omit
1232+
// `requestMethods` (default is all methods) to cover the endpoint.
1233+
await reconfigureServer({
1234+
rateLimit: [
1235+
{
1236+
requestPath: '/login',
1237+
requestTimeWindow: 10000,
1238+
requestCount: 1,
1239+
requestMethods: ['POST'],
1240+
errorResponseMessage: 'Too many requests',
1241+
includeInternalRequests: true,
1242+
},
1243+
],
1244+
});
1245+
await Parse.User.signUp('testuser', 'password');
1246+
for (let i = 0; i < 3; i++) {
1247+
const res = await request({
1248+
method: 'GET',
1249+
headers,
1250+
url: 'http://localhost:8378/1/login?username=testuser&password=password',
1251+
});
1252+
expect(res.data.username).toBe('testuser');
1253+
}
1254+
});
1255+
1256+
it('applies the rate limit to direct GET login requests when requestMethods includes GET', async () => {
1257+
await reconfigureServer({
1258+
rateLimit: [
1259+
{
1260+
requestPath: '/login',
1261+
requestTimeWindow: 10000,
1262+
requestCount: 1,
1263+
requestMethods: ['POST', 'GET'],
1264+
errorResponseMessage: 'Too many requests',
1265+
includeInternalRequests: true,
1266+
},
1267+
],
1268+
});
1269+
await Parse.User.signUp('testuser', 'password');
1270+
const res1 = await request({
1271+
method: 'GET',
1272+
headers,
1273+
url: 'http://localhost:8378/1/login?username=testuser&password=password',
1274+
});
1275+
expect(res1.data.username).toBe('testuser');
1276+
const res2 = await request({
1277+
method: 'GET',
1278+
headers,
1279+
url: 'http://localhost:8378/1/login?username=testuser&password=password',
1280+
}).catch(e => e);
1281+
expect(res2.data).toEqual({
1282+
code: Parse.Error.CONNECTION_FAILED,
1283+
error: 'Too many requests',
1284+
});
1285+
});
1286+
1287+
it('applies the rate limit to GET login requests sent via _method override when requestMethods includes GET', async () => {
1288+
await reconfigureServer({
1289+
rateLimit: [
1290+
{
1291+
requestPath: '/login',
1292+
requestTimeWindow: 10000,
1293+
requestCount: 1,
1294+
requestMethods: ['POST', 'GET'],
1295+
errorResponseMessage: 'Too many requests',
1296+
includeInternalRequests: true,
1297+
},
1298+
],
1299+
});
1300+
await Parse.User.signUp('testuser', 'password');
1301+
const res1 = await request({
1302+
method: 'POST',
1303+
headers,
1304+
url: 'http://localhost:8378/1/login',
1305+
body: JSON.stringify({ _method: 'GET', username: 'testuser', password: 'password' }),
1306+
});
1307+
expect(res1.data.username).toBe('testuser');
1308+
const res2 = await request({
1309+
method: 'POST',
1310+
headers,
1311+
url: 'http://localhost:8378/1/login',
1312+
body: JSON.stringify({ _method: 'GET', username: 'testuser', password: 'password' }),
1313+
}).catch(e => e);
1314+
expect(res2.data).toEqual({
1315+
code: Parse.Error.CONNECTION_FAILED,
1316+
error: 'Too many requests',
1317+
});
1318+
});
1319+
1320+
it('applies the rate limit to login requests of any method when requestMethods is omitted', async () => {
1321+
await reconfigureServer({
1322+
rateLimit: [
1323+
{
1324+
requestPath: '/login',
1325+
requestTimeWindow: 10000,
1326+
requestCount: 1,
1327+
errorResponseMessage: 'Too many requests',
1328+
includeInternalRequests: true,
1329+
},
1330+
],
1331+
});
1332+
await Parse.User.signUp('testuser', 'password');
1333+
// First login (POST) consumes the single allowed request across all methods.
1334+
const res1 = await request({
1335+
method: 'POST',
1336+
headers,
1337+
url: 'http://localhost:8378/1/login',
1338+
body: JSON.stringify({ username: 'testuser', password: 'password' }),
1339+
});
1340+
expect(res1.data.username).toBe('testuser');
1341+
// A subsequent GET login (sent via _method override) is still rate limited.
1342+
const res2 = await request({
1343+
method: 'POST',
1344+
headers,
1345+
url: 'http://localhost:8378/1/login',
1346+
body: JSON.stringify({ _method: 'GET', username: 'testuser', password: 'password' }),
1347+
}).catch(e => e);
1348+
expect(res2.data).toEqual({
1349+
code: Parse.Error.CONNECTION_FAILED,
1350+
error: 'Too many requests',
1351+
});
1352+
});
1353+
12281354
it('should allow _method override with PUT', async () => {
12291355
await reconfigureServer({
12301356
rateLimit: [

src/Options/Definitions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,7 @@ module.exports.RateLimitOptions = {
704704
},
705705
requestMethods: {
706706
env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_METHODS',
707-
help: 'Optional, the HTTP request methods to which the rate limit should be applied, default is all methods.',
707+
help: "Optional, the HTTP request methods to which the rate limit should be applied, default is all methods. The method is matched after any `_method` body override has been resolved, i.e. it is the method used to route the request. Note that some endpoints are reachable via more than one HTTP method (for example `/login` and `/verifyPassword` are available via both `GET` and `POST`); to rate limit such an endpoint reliably, include all relevant methods (e.g. `['GET', 'POST']`) or omit this option to apply the limit to all methods.",
708708
action: parsers.arrayParser,
709709
},
710710
requestPath: {

src/Options/docs.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Options/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ export interface RateLimitOptions {
435435
/* The error message that should be returned in the body of the HTTP 429 response when the rate limit is hit. Default is `Too many requests.`.
436436
:DEFAULT: Too many requests. */
437437
errorResponseMessage: ?string;
438-
/* Optional, the HTTP request methods to which the rate limit should be applied, default is all methods. */
438+
/* Optional, the HTTP request methods to which the rate limit should be applied, default is all methods. The method is matched after any `_method` body override has been resolved, i.e. it is the method used to route the request. Note that some endpoints are reachable via more than one HTTP method (for example `/login` and `/verifyPassword` are available via both `GET` and `POST`); to rate limit such an endpoint reliably, include all relevant methods (e.g. `['GET', 'POST']`) or omit this option to apply the limit to all methods. */
439439
requestMethods: ?(string[]);
440440
/* Optional, if `true` the rate limit will also apply to requests using the `masterKey`, default is `false`. Note that a public Cloud Code function that triggers internal requests using the `masterKey` may circumvent rate limiting and be vulnerable to attacks.
441441
:DEFAULT: false */

0 commit comments

Comments
 (0)