mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: Auth data exposed via /users/me endpoint ([GHSA-37mj-c2wf-cx96](https://github.com/parse-community/parse-server/security/advisories/GHSA-37mj-c2wf-cx96)) (#10278)
This commit is contained in:
@@ -4497,4 +4497,107 @@ describe('(GHSA-p2w6-rmh7-w8q3) SQL Injection via aggregate and distinct field n
|
||||
expect(response.data?.results).toEqual(['Alice']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('(GHSA-37mj-c2wf-cx96) /users/me leaks raw authData via master context', () => {
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
it('does not leak raw MFA authData via /users/me', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mfa: {
|
||||
enabled: true,
|
||||
options: ['TOTP'],
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const sessionToken = user.getSessionToken();
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
// Enable MFA
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken }
|
||||
);
|
||||
// Verify MFA data is stored (master key)
|
||||
await user.fetch({ useMasterKey: true });
|
||||
expect(user.get('authData').mfa.secret).toBe(secret.base32);
|
||||
expect(user.get('authData').mfa.recovery).toBeDefined();
|
||||
// GET /users/me should NOT include raw MFA data
|
||||
const response = await request({
|
||||
headers: {
|
||||
...headers,
|
||||
'X-Parse-Session-Token': sessionToken,
|
||||
},
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8378/1/users/me',
|
||||
});
|
||||
expect(response.data.authData?.mfa?.secret).toBeUndefined();
|
||||
expect(response.data.authData?.mfa?.recovery).toBeUndefined();
|
||||
expect(response.data.authData?.mfa).toEqual({ status: 'enabled' });
|
||||
});
|
||||
|
||||
it('returns same authData from /users/me and /users/:id', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mfa: {
|
||||
enabled: true,
|
||||
options: ['TOTP'],
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const sessionToken = user.getSessionToken();
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token: totp.generate() } } },
|
||||
{ sessionToken }
|
||||
);
|
||||
// Fetch via /users/me
|
||||
const meResponse = await request({
|
||||
headers: {
|
||||
...headers,
|
||||
'X-Parse-Session-Token': sessionToken,
|
||||
},
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8378/1/users/me',
|
||||
});
|
||||
// Fetch via /users/:id
|
||||
const idResponse = await request({
|
||||
headers: {
|
||||
...headers,
|
||||
'X-Parse-Session-Token': sessionToken,
|
||||
},
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/users/${user.id}`,
|
||||
});
|
||||
// Both should return the same sanitized authData
|
||||
expect(meResponse.data.authData).toEqual(idResponse.data.authData);
|
||||
expect(meResponse.data.authData?.mfa).toEqual({ status: 'enabled' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+40
-24
@@ -176,34 +176,50 @@ export class UsersRouter extends ClassesRouter {
|
||||
});
|
||||
}
|
||||
|
||||
handleMe(req) {
|
||||
async handleMe(req) {
|
||||
if (!req.info || !req.info.sessionToken) {
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
|
||||
}
|
||||
const sessionToken = req.info.sessionToken;
|
||||
return rest
|
||||
.find(
|
||||
req.config,
|
||||
Auth.master(req.config),
|
||||
'_Session',
|
||||
{ sessionToken },
|
||||
{ include: 'user' },
|
||||
req.info.clientSDK,
|
||||
req.info.context
|
||||
)
|
||||
.then(response => {
|
||||
if (!response.results || response.results.length == 0 || !response.results[0].user) {
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
|
||||
} else {
|
||||
const user = response.results[0].user;
|
||||
// Send token back on the login, because SDKs expect that.
|
||||
user.sessionToken = sessionToken;
|
||||
|
||||
// Remove hidden properties.
|
||||
UsersRouter.removeHiddenProperties(user);
|
||||
return { response: user };
|
||||
}
|
||||
});
|
||||
// Query the session with master key to validate the session token,
|
||||
// but do NOT include 'user' to avoid leaking user data via master context
|
||||
const sessionResponse = await rest.find(
|
||||
req.config,
|
||||
Auth.master(req.config),
|
||||
'_Session',
|
||||
{ sessionToken },
|
||||
{},
|
||||
req.info.clientSDK,
|
||||
req.info.context
|
||||
);
|
||||
if (
|
||||
!sessionResponse.results ||
|
||||
sessionResponse.results.length == 0 ||
|
||||
!sessionResponse.results[0].user
|
||||
) {
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
|
||||
}
|
||||
const userId = sessionResponse.results[0].user.objectId;
|
||||
// Re-fetch the user with the caller's auth context so that
|
||||
// protectedFields, CLP, and auth adapter afterFind apply correctly
|
||||
const userResponse = await rest.get(
|
||||
req.config,
|
||||
req.auth,
|
||||
'_User',
|
||||
userId,
|
||||
{},
|
||||
req.info.clientSDK,
|
||||
req.info.context
|
||||
);
|
||||
if (!userResponse.results || userResponse.results.length == 0) {
|
||||
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
|
||||
}
|
||||
const user = userResponse.results[0];
|
||||
// Send token back on the login, because SDKs expect that.
|
||||
user.sessionToken = sessionToken;
|
||||
// Remove hidden properties.
|
||||
UsersRouter.removeHiddenProperties(user);
|
||||
return { response: user };
|
||||
}
|
||||
|
||||
async handleLogIn(req) {
|
||||
|
||||
Reference in New Issue
Block a user