Skip to content

Commit 875cf10

Browse files
authored
fix: Auth data exposed via /users/me endpoint ([GHSA-37mj-c2wf-cx96](GHSA-37mj-c2wf-cx96)) (#10278)
1 parent e61af4f commit 875cf10

File tree

2 files changed

+143
-24
lines changed

2 files changed

+143
-24
lines changed

spec/vulnerabilities.spec.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4497,4 +4497,107 @@ describe('(GHSA-p2w6-rmh7-w8q3) SQL Injection via aggregate and distinct field n
44974497
expect(response.data?.results).toEqual(['Alice']);
44984498
});
44994499
});
4500+
4501+
describe('(GHSA-37mj-c2wf-cx96) /users/me leaks raw authData via master context', () => {
4502+
const headers = {
4503+
'X-Parse-Application-Id': 'test',
4504+
'X-Parse-REST-API-Key': 'rest',
4505+
'Content-Type': 'application/json',
4506+
};
4507+
4508+
it('does not leak raw MFA authData via /users/me', async () => {
4509+
await reconfigureServer({
4510+
auth: {
4511+
mfa: {
4512+
enabled: true,
4513+
options: ['TOTP'],
4514+
algorithm: 'SHA1',
4515+
digits: 6,
4516+
period: 30,
4517+
},
4518+
},
4519+
});
4520+
const user = await Parse.User.signUp('username', 'password');
4521+
const sessionToken = user.getSessionToken();
4522+
const OTPAuth = require('otpauth');
4523+
const secret = new OTPAuth.Secret();
4524+
const totp = new OTPAuth.TOTP({
4525+
algorithm: 'SHA1',
4526+
digits: 6,
4527+
period: 30,
4528+
secret,
4529+
});
4530+
const token = totp.generate();
4531+
// Enable MFA
4532+
await user.save(
4533+
{ authData: { mfa: { secret: secret.base32, token } } },
4534+
{ sessionToken }
4535+
);
4536+
// Verify MFA data is stored (master key)
4537+
await user.fetch({ useMasterKey: true });
4538+
expect(user.get('authData').mfa.secret).toBe(secret.base32);
4539+
expect(user.get('authData').mfa.recovery).toBeDefined();
4540+
// GET /users/me should NOT include raw MFA data
4541+
const response = await request({
4542+
headers: {
4543+
...headers,
4544+
'X-Parse-Session-Token': sessionToken,
4545+
},
4546+
method: 'GET',
4547+
url: 'http://localhost:8378/1/users/me',
4548+
});
4549+
expect(response.data.authData?.mfa?.secret).toBeUndefined();
4550+
expect(response.data.authData?.mfa?.recovery).toBeUndefined();
4551+
expect(response.data.authData?.mfa).toEqual({ status: 'enabled' });
4552+
});
4553+
4554+
it('returns same authData from /users/me and /users/:id', async () => {
4555+
await reconfigureServer({
4556+
auth: {
4557+
mfa: {
4558+
enabled: true,
4559+
options: ['TOTP'],
4560+
algorithm: 'SHA1',
4561+
digits: 6,
4562+
period: 30,
4563+
},
4564+
},
4565+
});
4566+
const user = await Parse.User.signUp('username', 'password');
4567+
const sessionToken = user.getSessionToken();
4568+
const OTPAuth = require('otpauth');
4569+
const secret = new OTPAuth.Secret();
4570+
const totp = new OTPAuth.TOTP({
4571+
algorithm: 'SHA1',
4572+
digits: 6,
4573+
period: 30,
4574+
secret,
4575+
});
4576+
await user.save(
4577+
{ authData: { mfa: { secret: secret.base32, token: totp.generate() } } },
4578+
{ sessionToken }
4579+
);
4580+
// Fetch via /users/me
4581+
const meResponse = await request({
4582+
headers: {
4583+
...headers,
4584+
'X-Parse-Session-Token': sessionToken,
4585+
},
4586+
method: 'GET',
4587+
url: 'http://localhost:8378/1/users/me',
4588+
});
4589+
// Fetch via /users/:id
4590+
const idResponse = await request({
4591+
headers: {
4592+
...headers,
4593+
'X-Parse-Session-Token': sessionToken,
4594+
},
4595+
method: 'GET',
4596+
url: `http://localhost:8378/1/users/${user.id}`,
4597+
});
4598+
// Both should return the same sanitized authData
4599+
expect(meResponse.data.authData).toEqual(idResponse.data.authData);
4600+
expect(meResponse.data.authData?.mfa).toEqual({ status: 'enabled' });
4601+
});
4602+
});
45004603
});

src/Routers/UsersRouter.js

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -176,34 +176,50 @@ export class UsersRouter extends ClassesRouter {
176176
});
177177
}
178178

179-
handleMe(req) {
179+
async handleMe(req) {
180180
if (!req.info || !req.info.sessionToken) {
181181
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
182182
}
183183
const sessionToken = req.info.sessionToken;
184-
return rest
185-
.find(
186-
req.config,
187-
Auth.master(req.config),
188-
'_Session',
189-
{ sessionToken },
190-
{ include: 'user' },
191-
req.info.clientSDK,
192-
req.info.context
193-
)
194-
.then(response => {
195-
if (!response.results || response.results.length == 0 || !response.results[0].user) {
196-
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
197-
} else {
198-
const user = response.results[0].user;
199-
// Send token back on the login, because SDKs expect that.
200-
user.sessionToken = sessionToken;
201-
202-
// Remove hidden properties.
203-
UsersRouter.removeHiddenProperties(user);
204-
return { response: user };
205-
}
206-
});
184+
// Query the session with master key to validate the session token,
185+
// but do NOT include 'user' to avoid leaking user data via master context
186+
const sessionResponse = await rest.find(
187+
req.config,
188+
Auth.master(req.config),
189+
'_Session',
190+
{ sessionToken },
191+
{},
192+
req.info.clientSDK,
193+
req.info.context
194+
);
195+
if (
196+
!sessionResponse.results ||
197+
sessionResponse.results.length == 0 ||
198+
!sessionResponse.results[0].user
199+
) {
200+
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
201+
}
202+
const userId = sessionResponse.results[0].user.objectId;
203+
// Re-fetch the user with the caller's auth context so that
204+
// protectedFields, CLP, and auth adapter afterFind apply correctly
205+
const userResponse = await rest.get(
206+
req.config,
207+
req.auth,
208+
'_User',
209+
userId,
210+
{},
211+
req.info.clientSDK,
212+
req.info.context
213+
);
214+
if (!userResponse.results || userResponse.results.length == 0) {
215+
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
216+
}
217+
const user = userResponse.results[0];
218+
// Send token back on the login, because SDKs expect that.
219+
user.sessionToken = sessionToken;
220+
// Remove hidden properties.
221+
UsersRouter.removeHiddenProperties(user);
222+
return { response: user };
207223
}
208224

209225
async handleLogIn(req) {

0 commit comments

Comments
 (0)