Skip to content

Commit 0d0a554

Browse files
authored
fix: Account takeover via operator injection in authentication data identifier ([GHSA-5fw2-8jcv-xh87](GHSA-5fw2-8jcv-xh87)) (#10185)
1 parent ad826e1 commit 0d0a554

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed

spec/RegexVulnerabilities.spec.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,129 @@ describe('Regex Vulnerabilities', () => {
220220
});
221221
});
222222

223+
describe('on authData id operator injection', () => {
224+
it('should reject $regex operator in anonymous authData id on login', async () => {
225+
// Create a victim anonymous user with a known ID prefix
226+
const victimId = 'victim_' + Date.now();
227+
const signupRes = await request({
228+
url: `${serverURL}/users`,
229+
method: 'POST',
230+
headers,
231+
body: JSON.stringify({
232+
...keys,
233+
_method: 'POST',
234+
authData: { anonymous: { id: victimId } },
235+
}),
236+
});
237+
expect(signupRes.data.objectId).toBeDefined();
238+
239+
// Attacker tries to login with $regex to match the victim
240+
try {
241+
await request({
242+
url: `${serverURL}/users`,
243+
method: 'POST',
244+
headers,
245+
body: JSON.stringify({
246+
...keys,
247+
_method: 'POST',
248+
authData: { anonymous: { id: { $regex: '^victim_' } } },
249+
}),
250+
});
251+
fail('should not allow $regex in authData id');
252+
} catch (e) {
253+
expect(e.data.code).toEqual(Parse.Error.INVALID_VALUE);
254+
}
255+
});
256+
257+
it('should reject $ne operator in anonymous authData id on login', async () => {
258+
const victimId = 'victim_ne_' + Date.now();
259+
await request({
260+
url: `${serverURL}/users`,
261+
method: 'POST',
262+
headers,
263+
body: JSON.stringify({
264+
...keys,
265+
_method: 'POST',
266+
authData: { anonymous: { id: victimId } },
267+
}),
268+
});
269+
270+
try {
271+
await request({
272+
url: `${serverURL}/users`,
273+
method: 'POST',
274+
headers,
275+
body: JSON.stringify({
276+
...keys,
277+
_method: 'POST',
278+
authData: { anonymous: { id: { $ne: 'nonexistent' } } },
279+
}),
280+
});
281+
fail('should not allow $ne in authData id');
282+
} catch (e) {
283+
expect(e.data.code).toEqual(Parse.Error.INVALID_VALUE);
284+
}
285+
});
286+
287+
it('should reject $exists operator in anonymous authData id on login', async () => {
288+
const victimId = 'victim_exists_' + Date.now();
289+
await request({
290+
url: `${serverURL}/users`,
291+
method: 'POST',
292+
headers,
293+
body: JSON.stringify({
294+
...keys,
295+
_method: 'POST',
296+
authData: { anonymous: { id: victimId } },
297+
}),
298+
});
299+
300+
try {
301+
await request({
302+
url: `${serverURL}/users`,
303+
method: 'POST',
304+
headers,
305+
body: JSON.stringify({
306+
...keys,
307+
_method: 'POST',
308+
authData: { anonymous: { id: { $exists: true } } },
309+
}),
310+
});
311+
fail('should not allow $exists in authData id');
312+
} catch (e) {
313+
expect(e.data.code).toEqual(Parse.Error.INVALID_VALUE);
314+
}
315+
});
316+
317+
it('should allow valid string authData id for anonymous login', async () => {
318+
const userId = 'valid_anon_' + Date.now();
319+
const signupRes = await request({
320+
url: `${serverURL}/users`,
321+
method: 'POST',
322+
headers,
323+
body: JSON.stringify({
324+
...keys,
325+
_method: 'POST',
326+
authData: { anonymous: { id: userId } },
327+
}),
328+
});
329+
expect(signupRes.data.objectId).toBeDefined();
330+
331+
// Same ID should successfully log in
332+
const loginRes = await request({
333+
url: `${serverURL}/users`,
334+
method: 'POST',
335+
headers,
336+
body: JSON.stringify({
337+
...keys,
338+
_method: 'POST',
339+
authData: { anonymous: { id: userId } },
340+
}),
341+
});
342+
expect(loginRes.data.objectId).toEqual(signupRes.data.objectId);
343+
});
344+
});
345+
223346
describe('on resend verification email', () => {
224347
// The PagesRouter uses express.urlencoded({ extended: false }) which does not parse
225348
// nested objects (e.g. token[$regex]=^.), so the HTTP layer already blocks object injection.
@@ -354,3 +477,44 @@ describe('Regex Vulnerabilities', () => {
354477
});
355478
});
356479
});
480+
481+
describe('Regex Vulnerabilities - authData operator injection with custom adapter', () => {
482+
it('should reject non-string authData id for custom auth adapter on login', async () => {
483+
await reconfigureServer({
484+
auth: {
485+
myAdapter: {
486+
validateAuthData: () => Promise.resolve(),
487+
validateAppId: () => Promise.resolve(),
488+
},
489+
},
490+
});
491+
492+
const victimId = 'adapter_victim_' + Date.now();
493+
await request({
494+
url: `${serverURL}/users`,
495+
method: 'POST',
496+
headers,
497+
body: JSON.stringify({
498+
...keys,
499+
_method: 'POST',
500+
authData: { myAdapter: { id: victimId, token: 'valid' } },
501+
}),
502+
});
503+
504+
try {
505+
await request({
506+
url: `${serverURL}/users`,
507+
method: 'POST',
508+
headers,
509+
body: JSON.stringify({
510+
...keys,
511+
_method: 'POST',
512+
authData: { myAdapter: { id: { $regex: '^adapter_victim_' }, token: 'valid' } },
513+
}),
514+
});
515+
fail('should not allow $regex in custom adapter authData id');
516+
} catch (e) {
517+
expect(e.data.code).toEqual(Parse.Error.INVALID_VALUE);
518+
}
519+
});
520+
});

src/Auth.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,10 @@ const findUsersWithAuthData = async (config, authData, beforeFind, currentUserAu
452452
return null;
453453
}
454454

455+
if (typeof providerAuthData.id !== 'string') {
456+
throw new Parse.Error(Parse.Error.INVALID_VALUE, `Invalid authData id for provider '${provider}'.`);
457+
}
458+
455459
return { [`authData.${provider}.id`]: providerAuthData.id };
456460
})
457461
);

0 commit comments

Comments
 (0)