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:
Manuel
2026-03-22 03:57:50 +00:00
committed by GitHub
parent e61af4fdfe
commit 875cf10ac9
2 changed files with 143 additions and 24 deletions
+103
View File
@@ -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
View File
@@ -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) {