Skip to content

Commit c0889c8

Browse files
authored
fix: Master key does not bypass protectedFields on various endpoints (#10412)
1 parent 32680e3 commit c0889c8

File tree

4 files changed

+130
-32
lines changed

4 files changed

+130
-32
lines changed

spec/ProtectedFields.spec.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,6 +2031,62 @@ describe('ProtectedFields', function () {
20312031
expect(response.data.objectId).toBe(user.id);
20322032
});
20332033

2034+
it('/login with master key bypasses protectedFields', async function () {
2035+
await reconfigureServer({
2036+
protectedFields: {
2037+
_User: {
2038+
'*': ['phone'],
2039+
},
2040+
},
2041+
protectedFieldsOwnerExempt: false,
2042+
});
2043+
const user = await Parse.User.signUp('user1', 'password');
2044+
const sessionToken = user.getSessionToken();
2045+
user.set('phone', '555-1234');
2046+
await user.save(null, { sessionToken });
2047+
2048+
const response = await request({
2049+
method: 'POST',
2050+
url: 'http://localhost:8378/1/login',
2051+
headers: {
2052+
'X-Parse-Application-Id': 'test',
2053+
'X-Parse-Master-Key': 'test',
2054+
'Content-Type': 'application/json',
2055+
},
2056+
body: JSON.stringify({ username: 'user1', password: 'password' }),
2057+
});
2058+
expect(response.data.phone).toBe('555-1234');
2059+
expect(response.data.sessionToken).toBeDefined();
2060+
});
2061+
2062+
it('/verifyPassword with master key bypasses protectedFields', async function () {
2063+
await reconfigureServer({
2064+
protectedFields: {
2065+
_User: {
2066+
'*': ['phone'],
2067+
},
2068+
},
2069+
protectedFieldsOwnerExempt: false,
2070+
verifyUserEmails: false,
2071+
});
2072+
const user = await Parse.User.signUp('user1', 'password');
2073+
const sessionToken = user.getSessionToken();
2074+
user.set('phone', '555-1234');
2075+
await user.save(null, { sessionToken });
2076+
2077+
const response = await request({
2078+
method: 'POST',
2079+
url: 'http://localhost:8378/1/verifyPassword',
2080+
headers: {
2081+
'X-Parse-Application-Id': 'test',
2082+
'X-Parse-Master-Key': 'test',
2083+
'Content-Type': 'application/json',
2084+
},
2085+
body: JSON.stringify({ username: 'user1', password: 'password' }),
2086+
});
2087+
expect(response.data.phone).toBe('555-1234');
2088+
});
2089+
20342090
it('owner sees non-protected fields like email when protectedFieldsOwnerExempt is true', async function () {
20352091
await reconfigureServer({
20362092
protectedFields: {

spec/vulnerabilities.spec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5745,5 +5745,31 @@ describe('Vulnerabilities', () => {
57455745
expect(meResponse.data.objectId).toBeDefined();
57465746
expect(meResponse.data.user).toBeDefined();
57475747
});
5748+
5749+
it('should return protected fields on GET /sessions/me with master key', async () => {
5750+
await reconfigureServer({
5751+
protectedFields: {
5752+
_Session: { '*': ['createdWith'] },
5753+
},
5754+
});
5755+
const user = new Parse.User();
5756+
user.setUsername('session-pf-mk');
5757+
user.setPassword('password123');
5758+
user.set('email', 'session-pf-mk@example.com');
5759+
await user.signUp();
5760+
const sessionToken = user.getSessionToken();
5761+
5762+
const meResponse = await request({
5763+
method: 'GET',
5764+
url: 'http://localhost:8378/1/sessions/me',
5765+
headers: {
5766+
...headers,
5767+
'X-Parse-Session-Token': sessionToken,
5768+
'X-Parse-Master-Key': 'test',
5769+
},
5770+
});
5771+
expect(meResponse.data.createdWith).toBeDefined();
5772+
expect(meResponse.data.sessionToken).toBe(sessionToken);
5773+
});
57485774
});
57495775
});

src/Routers/SessionsRouter.js

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,20 @@ export class SessionsRouter extends ClassesRouter {
3434
const sessionObjectId = sessionResponse.results[0].objectId;
3535
const userId = sessionResponse.results[0].user.objectId;
3636
// Re-fetch the session with the caller's auth context so that
37-
// protectedFields and CLP apply correctly
38-
const userAuth = new Auth.Auth({
39-
config: req.config,
40-
isMaster: false,
41-
user: Parse.Object.fromJSON({ className: '_User', objectId: userId }),
42-
installationId: req.info.installationId,
43-
});
37+
// protectedFields and CLP apply correctly; if the caller used master key,
38+
// protectedFields are bypassed, matching the behavior of GET /sessions/:id
39+
const refetchAuth =
40+
req.auth?.isMaster || req.auth?.isMaintenance
41+
? req.auth
42+
: new Auth.Auth({
43+
config: req.config,
44+
isMaster: false,
45+
user: Parse.Object.fromJSON({ className: '_User', objectId: userId }),
46+
installationId: req.info.installationId,
47+
});
4448
const response = await rest.get(
4549
req.config,
46-
userAuth,
50+
refetchAuth,
4751
'_Session',
4852
sessionObjectId,
4953
{},
@@ -82,16 +86,20 @@ export class SessionsRouter extends ClassesRouter {
8286
{ sessionToken: { __op: 'Delete' } }
8387
);
8488
// Re-fetch the session with the caller's auth context so that
85-
// protectedFields filtering applies correctly
86-
const userAuth = new Auth.Auth({
87-
config,
88-
isMaster: false,
89-
user: Parse.Object.fromJSON({ className: '_User', objectId: user.id }),
90-
installationId: req.auth.installationId,
91-
});
89+
// protectedFields filtering applies correctly; if the caller used master key,
90+
// protectedFields are bypassed, matching the behavior of GET /sessions/:id
91+
const refetchAuth =
92+
req.auth.isMaster || req.auth.isMaintenance
93+
? req.auth
94+
: new Auth.Auth({
95+
config,
96+
isMaster: false,
97+
user: Parse.Object.fromJSON({ className: '_User', objectId: user.id }),
98+
installationId: req.auth.installationId,
99+
});
92100
const response = await rest.find(
93101
config,
94-
userAuth,
102+
refetchAuth,
95103
'_Session',
96104
{ sessionToken: sessionData.sessionToken },
97105
{},

src/Routers/UsersRouter.js

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -365,18 +365,22 @@ export class UsersRouter extends ClassesRouter {
365365
);
366366

367367
// Re-fetch the user with the caller's auth context so that
368-
// protectedFields and CLP apply correctly
369-
const userAuth = new Auth.Auth({
370-
config: req.config,
371-
isMaster: false,
372-
user: Parse.Object.fromJSON({ className: '_User', objectId: user.objectId }),
373-
installationId: req.info.installationId,
374-
});
368+
// protectedFields and CLP apply correctly; if the caller used master key,
369+
// protectedFields are bypassed, matching the behavior of GET /users/:id
370+
const refetchAuth =
371+
req.auth.isMaster || req.auth.isMaintenance
372+
? req.auth
373+
: new Auth.Auth({
374+
config: req.config,
375+
isMaster: false,
376+
user: Parse.Object.fromJSON({ className: '_User', objectId: user.objectId }),
377+
installationId: req.info.installationId,
378+
});
375379
let filteredUser;
376380
try {
377381
const filteredUserResponse = await rest.get(
378382
req.config,
379-
userAuth,
383+
refetchAuth,
380384
'_User',
381385
user.objectId,
382386
{},
@@ -464,18 +468,22 @@ export class UsersRouter extends ClassesRouter {
464468
// Remove hidden properties.
465469
UsersRouter.removeHiddenProperties(user);
466470
// Re-fetch the user with the caller's auth context so that
467-
// protectedFields and CLP apply correctly
468-
const userAuth = new Auth.Auth({
469-
config: req.config,
470-
isMaster: false,
471-
user: Parse.Object.fromJSON({ className: '_User', objectId: user.objectId }),
472-
installationId: req.info.installationId,
473-
});
471+
// protectedFields and CLP apply correctly; if the caller used master key,
472+
// protectedFields are bypassed, matching the behavior of GET /users/:id
473+
const refetchAuth =
474+
req.auth.isMaster || req.auth.isMaintenance
475+
? req.auth
476+
: new Auth.Auth({
477+
config: req.config,
478+
isMaster: false,
479+
user: Parse.Object.fromJSON({ className: '_User', objectId: user.objectId }),
480+
installationId: req.info.installationId,
481+
});
474482
let filteredUser;
475483
try {
476484
const filteredUserResponse = await rest.get(
477485
req.config,
478-
userAuth,
486+
refetchAuth,
479487
'_User',
480488
user.objectId,
481489
{},

0 commit comments

Comments
 (0)