Skip to content

Commit 14d06b0

Browse files
committed
fix
1 parent e236ddf commit 14d06b0

File tree

6 files changed

+152
-3
lines changed

6 files changed

+152
-3
lines changed

spec/RateLimit.spec.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default;
2+
const request = require('../lib/request');
3+
4+
const headers = {
5+
'Content-Type': 'application/json',
6+
'X-Parse-Application-Id': 'test',
7+
'X-Parse-REST-API-Key': 'rest',
8+
};
9+
210
describe('rate limit', () => {
311
it('can limit cloud functions', async () => {
412
Parse.Cloud.define('test', () => 'Abc');
@@ -487,6 +495,125 @@ describe('rate limit', () => {
487495
})
488496
).toBeRejectedWith(`Invalid rate limit option "path"`);
489497
});
498+
describe('batch', () => {
499+
it('should reject batch request when sub-requests exceed rate limit for a path', async () => {
500+
await reconfigureServer({
501+
rateLimit: [
502+
{
503+
requestPath: '/classes/*path',
504+
requestTimeWindow: 10000,
505+
requestCount: 2,
506+
errorResponseMessage: 'Too many requests',
507+
includeInternalRequests: true,
508+
},
509+
],
510+
});
511+
const response = await request({
512+
method: 'POST',
513+
headers: headers,
514+
url: 'http://localhost:8378/1/batch',
515+
body: JSON.stringify({
516+
requests: [
517+
{ method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' } },
518+
{ method: 'POST', path: '/1/classes/MyObject', body: { key: 'value2' } },
519+
{ method: 'POST', path: '/1/classes/MyObject', body: { key: 'value3' } },
520+
],
521+
}),
522+
}).catch(e => e);
523+
expect(response.data).toEqual({
524+
code: Parse.Error.CONNECTION_FAILED,
525+
error: 'Batch request exceeds rate limit for endpoint',
526+
});
527+
});
528+
529+
it('should allow batch request when sub-requests are within rate limit', async () => {
530+
await reconfigureServer({
531+
rateLimit: [
532+
{
533+
requestPath: '/classes/*path',
534+
requestTimeWindow: 10000,
535+
requestCount: 5,
536+
errorResponseMessage: 'Too many requests',
537+
includeInternalRequests: true,
538+
},
539+
],
540+
});
541+
const response = await request({
542+
method: 'POST',
543+
headers: headers,
544+
url: 'http://localhost:8378/1/batch',
545+
body: JSON.stringify({
546+
requests: [
547+
{ method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' } },
548+
{ method: 'POST', path: '/1/classes/MyObject', body: { key: 'value2' } },
549+
{ method: 'POST', path: '/1/classes/MyObject', body: { key: 'value3' } },
550+
],
551+
}),
552+
});
553+
expect(response.data.length).toBe(3);
554+
expect(response.data[0].success).toBeDefined();
555+
});
556+
557+
it('should reject batch when sub-requests for one rate-limited path exceed limit among mixed paths', async () => {
558+
await reconfigureServer({
559+
rateLimit: [
560+
{
561+
requestPath: '/login',
562+
requestTimeWindow: 10000,
563+
requestCount: 1,
564+
errorResponseMessage: 'Too many login requests',
565+
includeInternalRequests: true,
566+
},
567+
],
568+
});
569+
await Parse.User.signUp('testuser', 'password');
570+
const response = await request({
571+
method: 'POST',
572+
headers: headers,
573+
url: 'http://localhost:8378/1/batch',
574+
body: JSON.stringify({
575+
requests: [
576+
{ method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' } },
577+
{ method: 'POST', path: '/1/login', body: { username: 'testuser', password: 'password' } },
578+
{ method: 'POST', path: '/1/login', body: { username: 'testuser', password: 'wrong' } },
579+
],
580+
}),
581+
}).catch(e => e);
582+
expect(response.data).toEqual({
583+
code: Parse.Error.CONNECTION_FAILED,
584+
error: 'Batch request exceeds rate limit for endpoint',
585+
});
586+
});
587+
588+
it('should not reject batch when sub-requests target non-rate-limited paths', async () => {
589+
await reconfigureServer({
590+
rateLimit: [
591+
{
592+
requestPath: '/login',
593+
requestTimeWindow: 10000,
594+
requestCount: 1,
595+
errorResponseMessage: 'Too many login requests',
596+
includeInternalRequests: true,
597+
},
598+
],
599+
});
600+
const response = await request({
601+
method: 'POST',
602+
headers: headers,
603+
url: 'http://localhost:8378/1/batch',
604+
body: JSON.stringify({
605+
requests: [
606+
{ method: 'POST', path: '/1/classes/MyObject', body: { key: 'value1' } },
607+
{ method: 'POST', path: '/1/classes/MyObject', body: { key: 'value2' } },
608+
{ method: 'POST', path: '/1/classes/MyObject', body: { key: 'value3' } },
609+
],
610+
}),
611+
});
612+
expect(response.data.length).toBe(3);
613+
expect(response.data[0].success).toBeDefined();
614+
});
615+
});
616+
490617
describe_only(() => {
491618
return process.env.PARSE_SERVER_TEST_CACHE === 'redis';
492619
})('with RedisCache', function () {

src/Options/Definitions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ module.exports.RateLimitOptions = {
637637
},
638638
requestCount: {
639639
env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT',
640-
help: 'The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied.',
640+
help: 'The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied. For batch requests, this also limits the number of sub-requests in a single batch that target this path; however, requests already consumed in the current time window are not counted against the batch, so the effective limit may be higher when combining individual and batch requests. Note that this is a basic server-level rate limit; for comprehensive protection, use a reverse proxy or WAF for rate limiting.',
641641
action: parsers.numberParser('requestCount'),
642642
},
643643
requestMethods: {

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
@@ -387,7 +387,7 @@ export interface RateLimitOptions {
387387
requestPath: string;
388388
/* The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied. */
389389
requestTimeWindow: ?number;
390-
/* The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied. */
390+
/* The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied. For batch requests, this also limits the number of sub-requests in a single batch that target this path; however, requests already consumed in the current time window are not counted against the batch, so the effective limit may be higher when combining individual and batch requests. Note that this is a basic server-level rate limit; for comprehensive protection, use a reverse proxy or WAF for rate limiting. */
391391
requestCount: ?number;
392392
/* 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.`.
393393
:DEFAULT: Too many requests. */

src/batch.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,27 @@ function handleBatch(router, req) {
8383
req.config.publicServerURL
8484
);
8585

86+
// Check if batch sub-requests would exceed any configured rate limits.
87+
// Count how many sub-requests target each rate-limited path and reject
88+
// the entire batch if any path's count exceeds its requestCount.
89+
const rateLimits = req.config.rateLimits || [];
90+
for (const limit of rateLimits) {
91+
const pathExp = limit.path.regexp || limit.path;
92+
let matchCount = 0;
93+
for (const restRequest of req.body.requests) {
94+
const routablePath = makeRoutablePath(restRequest.path);
95+
if (pathExp.test(routablePath)) {
96+
matchCount++;
97+
}
98+
}
99+
if (matchCount > limit.requestCount) {
100+
throw new Parse.Error(
101+
Parse.Error.CONNECTION_FAILED,
102+
'Batch request exceeds rate limit for endpoint'
103+
);
104+
}
105+
}
106+
86107
const batch = transactionRetries => {
87108
let initialPromise = Promise.resolve();
88109
if (req.body?.transaction === true) {

src/middlewares.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,7 @@ export const addRateLimit = (route, config, cloud) => {
584584
}
585585
config.rateLimits.push({
586586
path: pathToRegexp(route.requestPath),
587+
requestCount: route.requestCount,
587588
handler: rateLimit({
588589
windowMs: route.requestTimeWindow,
589590
max: route.requestCount,

0 commit comments

Comments
 (0)