test: Rate limit requestMethods is scoped to the configured HTTP methods (#10520)

This commit is contained in:
Manuel
2026-06-20 03:42:22 +02:00
committed by GitHub
parent 37039b09e7
commit 4d3465c1b9
4 changed files with 129 additions and 3 deletions
+126
View File
@@ -1225,6 +1225,132 @@ describe('rate limit', () => {
});
});
it('does not apply a requestMethods POST-only limit to direct GET login requests', async () => {
// `requestMethods` scopes a limit to the listed request methods. `/login` is
// reachable via both GET and POST, so a POST-only limit intentionally does not
// apply to GET login requests; operators must list all methods or omit
// `requestMethods` (default is all methods) to cover the endpoint.
await reconfigureServer({
rateLimit: [
{
requestPath: '/login',
requestTimeWindow: 10000,
requestCount: 1,
requestMethods: ['POST'],
errorResponseMessage: 'Too many requests',
includeInternalRequests: true,
},
],
});
await Parse.User.signUp('testuser', 'password');
for (let i = 0; i < 3; i++) {
const res = await request({
method: 'GET',
headers,
url: 'http://localhost:8378/1/login?username=testuser&password=password',
});
expect(res.data.username).toBe('testuser');
}
});
it('applies the rate limit to direct GET login requests when requestMethods includes GET', async () => {
await reconfigureServer({
rateLimit: [
{
requestPath: '/login',
requestTimeWindow: 10000,
requestCount: 1,
requestMethods: ['POST', 'GET'],
errorResponseMessage: 'Too many requests',
includeInternalRequests: true,
},
],
});
await Parse.User.signUp('testuser', 'password');
const res1 = await request({
method: 'GET',
headers,
url: 'http://localhost:8378/1/login?username=testuser&password=password',
});
expect(res1.data.username).toBe('testuser');
const res2 = await request({
method: 'GET',
headers,
url: 'http://localhost:8378/1/login?username=testuser&password=password',
}).catch(e => e);
expect(res2.data).toEqual({
code: Parse.Error.CONNECTION_FAILED,
error: 'Too many requests',
});
});
it('applies the rate limit to GET login requests sent via _method override when requestMethods includes GET', async () => {
await reconfigureServer({
rateLimit: [
{
requestPath: '/login',
requestTimeWindow: 10000,
requestCount: 1,
requestMethods: ['POST', 'GET'],
errorResponseMessage: 'Too many requests',
includeInternalRequests: true,
},
],
});
await Parse.User.signUp('testuser', 'password');
const res1 = await request({
method: 'POST',
headers,
url: 'http://localhost:8378/1/login',
body: JSON.stringify({ _method: 'GET', username: 'testuser', password: 'password' }),
});
expect(res1.data.username).toBe('testuser');
const res2 = await request({
method: 'POST',
headers,
url: 'http://localhost:8378/1/login',
body: JSON.stringify({ _method: 'GET', username: 'testuser', password: 'password' }),
}).catch(e => e);
expect(res2.data).toEqual({
code: Parse.Error.CONNECTION_FAILED,
error: 'Too many requests',
});
});
it('applies the rate limit to login requests of any method when requestMethods is omitted', async () => {
await reconfigureServer({
rateLimit: [
{
requestPath: '/login',
requestTimeWindow: 10000,
requestCount: 1,
errorResponseMessage: 'Too many requests',
includeInternalRequests: true,
},
],
});
await Parse.User.signUp('testuser', 'password');
// First login (POST) consumes the single allowed request across all methods.
const res1 = await request({
method: 'POST',
headers,
url: 'http://localhost:8378/1/login',
body: JSON.stringify({ username: 'testuser', password: 'password' }),
});
expect(res1.data.username).toBe('testuser');
// A subsequent GET login (sent via _method override) is still rate limited.
const res2 = await request({
method: 'POST',
headers,
url: 'http://localhost:8378/1/login',
body: JSON.stringify({ _method: 'GET', username: 'testuser', password: 'password' }),
}).catch(e => e);
expect(res2.data).toEqual({
code: Parse.Error.CONNECTION_FAILED,
error: 'Too many requests',
});
});
it('should allow _method override with PUT', async () => {
await reconfigureServer({
rateLimit: [
+1 -1
View File
@@ -704,7 +704,7 @@ module.exports.RateLimitOptions = {
},
requestMethods: {
env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_METHODS',
help: 'Optional, the HTTP request methods to which the rate limit should be applied, default is all methods.',
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.",
action: parsers.arrayParser,
},
requestPath: {
+1 -1
View File
@@ -131,7 +131,7 @@
* @property {Boolean} includeMasterKey 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.
* @property {String} redisUrl Optional, the URL of the Redis server to store rate limit data. This allows to rate limit requests for multiple servers by calculating the sum of all requests across all servers. This is useful if multiple servers are processing requests behind a load balancer. For example, the limit of 10 requests is reached if each of 2 servers processed 5 requests.
* @property {Number} requestCount 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.
* @property {String[]} requestMethods Optional, the HTTP request methods to which the rate limit should be applied, default is all methods.
* @property {String[]} requestMethods 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.
* @property {String} requestPath The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings or string patterns following <a href="https://github.com/pillarjs/path-to-regexp">path-to-regexp v8</a> syntax.
* @property {Number} requestTimeWindow The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied.
* @property {String} zone The type of rate limit to apply. The following types are supported:<ul><li>`global`: rate limit based on the number of requests made by all users</li><li>`ip`: rate limit based on the IP address of the request</li><li>`user`: rate limit based on the user ID of the request</li><li>`session`: rate limit based on the session token of the request</li></ul>Default is `ip`.
+1 -1
View File
@@ -435,7 +435,7 @@ export interface RateLimitOptions {
/* 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.`.
:DEFAULT: Too many requests. */
errorResponseMessage: ?string;
/* Optional, the HTTP request methods to which the rate limit should be applied, default is all methods. */
/* 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. */
requestMethods: ?(string[]);
/* 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.
:DEFAULT: false */