Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions spec/RateLimit.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
2 changes: 1 addition & 1 deletion src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Loading