Skip to content

Commit b532170

Browse files
committed
fix: batch login sub-request rate limit uses IP-based keying
1 parent 12d6fae commit b532170

File tree

2 files changed

+121
-1
lines changed

2 files changed

+121
-1
lines changed

spec/RateLimit.spec.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,122 @@ describe('rate limit', () => {
10271027
});
10281028

10291029
describe('batch method bypass', () => {
1030+
it('should use IP-based keying for batch login sub-requests with session zone', async () => {
1031+
await reconfigureServer({
1032+
rateLimit: [
1033+
{
1034+
requestPath: '/login',
1035+
requestTimeWindow: 10000,
1036+
requestCount: 1,
1037+
errorResponseMessage: 'Too many requests',
1038+
includeInternalRequests: true,
1039+
zone: Parse.Server.RateLimitZone.session,
1040+
},
1041+
],
1042+
});
1043+
// Create two users and get their session tokens
1044+
const res1 = await request({
1045+
method: 'POST',
1046+
headers,
1047+
url: 'http://localhost:8378/1/users',
1048+
body: JSON.stringify({ username: 'user1', password: 'password1' }),
1049+
});
1050+
const sessionToken1 = res1.data.sessionToken;
1051+
const res2 = await request({
1052+
method: 'POST',
1053+
headers,
1054+
url: 'http://localhost:8378/1/users',
1055+
body: JSON.stringify({ username: 'user2', password: 'password2' }),
1056+
});
1057+
const sessionToken2 = res2.data.sessionToken;
1058+
// First batch login with TOKEN1 — should succeed
1059+
const batch1 = await request({
1060+
method: 'POST',
1061+
headers: { ...headers, 'X-Parse-Session-Token': sessionToken1 },
1062+
url: 'http://localhost:8378/1/batch',
1063+
body: JSON.stringify({
1064+
requests: [
1065+
{ method: 'POST', path: '/1/login', body: { username: 'user1', password: 'password1' } },
1066+
],
1067+
}),
1068+
});
1069+
expect(batch1.status).toBe(200);
1070+
// Second batch login with TOKEN2 — should be rate limited because
1071+
// login rate limit must use IP-based keying, not session-token keying;
1072+
// rotating session tokens must not create independent rate limit counters
1073+
const batch2 = await request({
1074+
method: 'POST',
1075+
headers: { ...headers, 'X-Parse-Session-Token': sessionToken2 },
1076+
url: 'http://localhost:8378/1/batch',
1077+
body: JSON.stringify({
1078+
requests: [
1079+
{ method: 'POST', path: '/1/login', body: { username: 'user1', password: 'password1' } },
1080+
],
1081+
}),
1082+
}).catch(e => e);
1083+
expect(batch2.data).toEqual({
1084+
code: Parse.Error.CONNECTION_FAILED,
1085+
error: 'Too many requests',
1086+
});
1087+
});
1088+
1089+
it('should use IP-based keying for batch login sub-requests with user zone', async () => {
1090+
await reconfigureServer({
1091+
rateLimit: [
1092+
{
1093+
requestPath: '/login',
1094+
requestTimeWindow: 10000,
1095+
requestCount: 1,
1096+
errorResponseMessage: 'Too many requests',
1097+
includeInternalRequests: true,
1098+
zone: Parse.Server.RateLimitZone.user,
1099+
},
1100+
],
1101+
});
1102+
// Create two users and get their session tokens
1103+
const res1 = await request({
1104+
method: 'POST',
1105+
headers,
1106+
url: 'http://localhost:8378/1/users',
1107+
body: JSON.stringify({ username: 'user1', password: 'password1' }),
1108+
});
1109+
const sessionToken1 = res1.data.sessionToken;
1110+
const res2 = await request({
1111+
method: 'POST',
1112+
headers,
1113+
url: 'http://localhost:8378/1/users',
1114+
body: JSON.stringify({ username: 'user2', password: 'password2' }),
1115+
});
1116+
const sessionToken2 = res2.data.sessionToken;
1117+
// First batch login with TOKEN1 — should succeed
1118+
const batch1 = await request({
1119+
method: 'POST',
1120+
headers: { ...headers, 'X-Parse-Session-Token': sessionToken1 },
1121+
url: 'http://localhost:8378/1/batch',
1122+
body: JSON.stringify({
1123+
requests: [
1124+
{ method: 'POST', path: '/1/login', body: { username: 'user1', password: 'password1' } },
1125+
],
1126+
}),
1127+
});
1128+
expect(batch1.status).toBe(200);
1129+
// Second batch login with TOKEN2 — should be rate limited
1130+
const batch2 = await request({
1131+
method: 'POST',
1132+
headers: { ...headers, 'X-Parse-Session-Token': sessionToken2 },
1133+
url: 'http://localhost:8378/1/batch',
1134+
body: JSON.stringify({
1135+
requests: [
1136+
{ method: 'POST', path: '/1/login', body: { username: 'user1', password: 'password1' } },
1137+
],
1138+
}),
1139+
}).catch(e => e);
1140+
expect(batch2.data).toEqual({
1141+
code: Parse.Error.CONNECTION_FAILED,
1142+
error: 'Too many requests',
1143+
});
1144+
});
1145+
10301146
it('should enforce POST rate limit on batch sub-requests using GET method for login', async () => {
10311147
Parse.Cloud.beforeLogin(() => {}, {
10321148
rateLimit: {

src/batch.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,17 @@ async function handleBatch(router, req) {
106106
if (!pathExp.test(routablePath)) {
107107
continue;
108108
}
109+
const info = { ...req.info };
110+
if (routablePath === '/login') {
111+
delete info.sessionToken;
112+
}
109113
const fakeReq = {
110114
ip: req.ip || req.config?.ip || '127.0.0.1',
111115
method: (restRequest.method || 'GET').toUpperCase(),
112116
_batchOriginalMethod: 'POST',
113117
config: req.config,
114118
auth: req.auth,
115-
info: req.info,
119+
info,
116120
};
117121
const fakeRes = { setHeader() {} };
118122
try {

0 commit comments

Comments
 (0)