diff --git a/spec/RateLimit.spec.js b/spec/RateLimit.spec.js index 92b0bf3ae9..7ce39a16df 100644 --- a/spec/RateLimit.spec.js +++ b/spec/RateLimit.spec.js @@ -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: [ diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 5bd564088e..5b2e6e497a 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -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: { diff --git a/src/Options/docs.js b/src/Options/docs.js index fcc2b9500b..91d58cbe9b 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -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 path-to-regexp v8 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:Default is `ip`. diff --git a/src/Options/index.js b/src/Options/index.js index baa198e843..d8f56defca 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -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 */