Skip to content

Commit 4f7cb53

Browse files
authored
feat: Add protectedFieldsSaveResponseExempt option to strip protected fields from save responses (#10289)
1 parent d131dc1 commit 4f7cb53

7 files changed

Lines changed: 200 additions & 0 deletions

File tree

DEPRECATIONS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
2626
| DEPPS20 | Remove config option `allowExpiredAuthDataToken` | | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
2727
| DEPPS21 | Config option `protectedFieldsOwnerExempt` defaults to `false` | | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
2828
| DEPPS22 | Config option `protectedFieldsTriggerExempt` defaults to `true` | | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
29+
| DEPPS23 | Config option `protectedFieldsSaveResponseExempt` defaults to `false` | | 9.7.0 (2026) | 10.0.0 (2027) | deprecated | - |
2930

3031
[i_deprecation]: ## "The version and date of the deprecation."
3132
[i_change]: ## "The version and date of the planned change."

spec/ProtectedFields.spec.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2216,4 +2216,156 @@ describe('ProtectedFields', function () {
22162216
expect(triggerOriginal.hasSecret).toBe(false);
22172217
});
22182218
});
2219+
2220+
describe('protectedFieldsSaveResponseExempt', function () {
2221+
it('should strip protected fields from update response when protectedFieldsSaveResponseExempt is false', async function () {
2222+
await reconfigureServer({
2223+
protectedFields: { MyClass: { '*': ['secretField'] } },
2224+
protectedFieldsTriggerExempt: true,
2225+
protectedFieldsSaveResponseExempt: false,
2226+
});
2227+
2228+
// Create object with master key
2229+
const obj = new Parse.Object('MyClass');
2230+
obj.set('secretField', 'hidden-value');
2231+
obj.set('publicField', 'visible-value');
2232+
const acl = new Parse.ACL();
2233+
acl.setPublicReadAccess(true);
2234+
acl.setPublicWriteAccess(true);
2235+
obj.setACL(acl);
2236+
await obj.save(null, { useMasterKey: true });
2237+
2238+
// beforeSave trigger modifies the protected field
2239+
Parse.Cloud.beforeSave('MyClass', req => {
2240+
req.object.set('secretField', 'trigger-modified-value');
2241+
});
2242+
2243+
// Update via raw HTTP to inspect the actual server response
2244+
const user = await Parse.User.signUp('testuser', 'password');
2245+
const response = await request({
2246+
method: 'PUT',
2247+
url: `http://localhost:8378/1/classes/MyClass/${obj.id}`,
2248+
headers: {
2249+
'X-Parse-Application-Id': 'test',
2250+
'X-Parse-REST-API-Key': 'rest',
2251+
'X-Parse-Session-Token': user.getSessionToken(),
2252+
'Content-Type': 'application/json',
2253+
},
2254+
body: JSON.stringify({ publicField: 'updated-value' }),
2255+
});
2256+
2257+
// The server response should NOT contain the protected field
2258+
expect(response.data.updatedAt).toBeDefined();
2259+
expect(response.data.secretField).toBeUndefined();
2260+
});
2261+
2262+
it('should strip protected fields from update response for _User class when protectedFieldsSaveResponseExempt is false', async function () {
2263+
await reconfigureServer({
2264+
protectedFields: { _User: { '*': ['email'] } },
2265+
protectedFieldsOwnerExempt: false,
2266+
protectedFieldsTriggerExempt: true,
2267+
protectedFieldsSaveResponseExempt: false,
2268+
});
2269+
2270+
// Create user
2271+
const user = new Parse.User();
2272+
user.setUsername('testuser');
2273+
user.setPassword('password');
2274+
user.setEmail('test@example.com');
2275+
user.set('publicField', 'visible-value');
2276+
await user.signUp();
2277+
2278+
// beforeSave trigger modifies the protected field
2279+
Parse.Cloud.beforeSave(Parse.User, req => {
2280+
req.object.set('email', 'trigger-modified@example.com');
2281+
});
2282+
2283+
// Update via raw HTTP
2284+
const response = await request({
2285+
method: 'PUT',
2286+
url: `http://localhost:8378/1/users/${user.id}`,
2287+
headers: {
2288+
'X-Parse-Application-Id': 'test',
2289+
'X-Parse-REST-API-Key': 'rest',
2290+
'X-Parse-Session-Token': user.getSessionToken(),
2291+
'Content-Type': 'application/json',
2292+
},
2293+
body: JSON.stringify({ publicField: 'updated-value' }),
2294+
});
2295+
2296+
// The server response should NOT contain the protected field
2297+
expect(response.data.updatedAt).toBeDefined();
2298+
expect(response.data.email).toBeUndefined();
2299+
});
2300+
2301+
it('should include protected fields in update response when protectedFieldsSaveResponseExempt is true', async function () {
2302+
await reconfigureServer({
2303+
protectedFields: { MyClass: { '*': ['secretField'] } },
2304+
protectedFieldsTriggerExempt: true,
2305+
protectedFieldsSaveResponseExempt: true,
2306+
});
2307+
2308+
// Create object with master key
2309+
const obj = new Parse.Object('MyClass');
2310+
obj.set('secretField', 'hidden-value');
2311+
obj.set('publicField', 'visible-value');
2312+
const acl = new Parse.ACL();
2313+
acl.setPublicReadAccess(true);
2314+
acl.setPublicWriteAccess(true);
2315+
obj.setACL(acl);
2316+
await obj.save(null, { useMasterKey: true });
2317+
2318+
// beforeSave trigger modifies the protected field
2319+
Parse.Cloud.beforeSave('MyClass', req => {
2320+
req.object.set('secretField', 'trigger-modified-value');
2321+
});
2322+
2323+
// Update via raw HTTP
2324+
const user = await Parse.User.signUp('testuser', 'password');
2325+
const response = await request({
2326+
method: 'PUT',
2327+
url: `http://localhost:8378/1/classes/MyClass/${obj.id}`,
2328+
headers: {
2329+
'X-Parse-Application-Id': 'test',
2330+
'X-Parse-REST-API-Key': 'rest',
2331+
'X-Parse-Session-Token': user.getSessionToken(),
2332+
'Content-Type': 'application/json',
2333+
},
2334+
body: JSON.stringify({ publicField: 'updated-value' }),
2335+
});
2336+
2337+
// The server response SHOULD contain the protected field (current behavior preserved)
2338+
expect(response.data.secretField).toBe('trigger-modified-value');
2339+
});
2340+
2341+
it('should strip protected fields from create response when protectedFieldsSaveResponseExempt is false', async function () {
2342+
await reconfigureServer({
2343+
protectedFields: { MyClass: { '*': ['secretField'] } },
2344+
protectedFieldsSaveResponseExempt: false,
2345+
});
2346+
2347+
// Create via raw HTTP as a regular user
2348+
const user = await Parse.User.signUp('testuser', 'password');
2349+
const response = await request({
2350+
method: 'POST',
2351+
url: 'http://localhost:8378/1/classes/MyClass',
2352+
headers: {
2353+
'X-Parse-Application-Id': 'test',
2354+
'X-Parse-REST-API-Key': 'rest',
2355+
'X-Parse-Session-Token': user.getSessionToken(),
2356+
'Content-Type': 'application/json',
2357+
},
2358+
body: JSON.stringify({
2359+
secretField: 'hidden-value',
2360+
publicField: 'visible-value',
2361+
ACL: { '*': { read: true, write: true } },
2362+
}),
2363+
});
2364+
2365+
// The server response should NOT contain the protected field
2366+
expect(response.data.objectId).toBeDefined();
2367+
expect(response.data.createdAt).toBeDefined();
2368+
expect(response.data.secretField).toBeUndefined();
2369+
});
2370+
});
22192371
});

src/Deprecator/Deprecations.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,9 @@ module.exports = [
9696
changeNewDefault: 'true',
9797
solution: "Set 'protectedFieldsTriggerExempt' to 'true' to make Cloud Code triggers (e.g. beforeSave, afterSave) receive the full object including protected fields, or to 'false' to keep the current behavior where protected fields are stripped from trigger objects.",
9898
},
99+
{
100+
optionKey: 'protectedFieldsSaveResponseExempt',
101+
changeNewDefault: 'false',
102+
solution: "Set 'protectedFieldsSaveResponseExempt' to 'false' to strip protected fields from write operation responses (create, update), consistent with how they are stripped from query results. Set to 'true' to keep the current behavior where protected fields are included in write responses.",
103+
},
99104
];

src/Options/Definitions.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,12 @@ module.exports.ParseServerOptions = {
484484
action: parsers.booleanParser,
485485
default: true,
486486
},
487+
protectedFieldsSaveResponseExempt: {
488+
env: 'PARSE_SERVER_PROTECTED_FIELDS_SAVE_RESPONSE_EXEMPT',
489+
help: 'Whether save operation responses (create, update) are exempt from `protectedFields`. If `true` (default), protected fields modified during a save are included in the response to the client. If `false`, protected fields are stripped from save responses, consistent with how they are stripped from query results. Defaults to `true`.',
490+
action: parsers.booleanParser,
491+
default: true,
492+
},
487493
protectedFieldsTriggerExempt: {
488494
env: 'PARSE_SERVER_PROTECTED_FIELDS_TRIGGER_EXEMPT',
489495
help: "Whether Cloud Code triggers (e.g. `beforeSave`, `afterSave`) are exempt from `protectedFields`. If `true`, triggers receive the full object including protected fields in `request.object` and `request.original`, regardless of the caller's auth context. If `false`, protected fields are stripped from the original object fetch used to build trigger objects. Defaults to `false`.",

src/Options/docs.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Options/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@ export interface ParseServerOptions {
179179
:ENV: PARSE_SERVER_PROTECTED_FIELDS_TRIGGER_EXEMPT
180180
:DEFAULT: false */
181181
protectedFieldsTriggerExempt: ?boolean;
182+
/* Whether save operation responses (create, update) are exempt from `protectedFields`. If `true` (default), protected fields modified during a save are included in the response to the client. If `false`, protected fields are stripped from save responses, consistent with how they are stripped from query results. Defaults to `true`.
183+
:ENV: PARSE_SERVER_PROTECTED_FIELDS_SAVE_RESPONSE_EXEMPT
184+
:DEFAULT: true */
185+
protectedFieldsSaveResponseExempt: ?boolean;
182186
/* Enable (or disable) anonymous users, defaults to true
183187
:ENV: PARSE_SERVER_ENABLE_ANON_USERS
184188
:DEFAULT: true */

src/RestWrite.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ RestWrite.prototype.execute = function () {
158158
.then(() => {
159159
return this.cleanUserAuthData();
160160
})
161+
.then(() => {
162+
return this.filterProtectedFieldsInResponse();
163+
})
161164
.then(() => {
162165
// Append the authDataResponse if exists
163166
if (this.authDataResponse) {
@@ -1894,6 +1897,34 @@ RestWrite.prototype.cleanUserAuthData = function () {
18941897
}
18951898
};
18961899

1900+
// Strips protected fields from the write response when protectedFieldsSaveResponseExempt is false.
1901+
RestWrite.prototype.filterProtectedFieldsInResponse = async function () {
1902+
if (this.config.protectedFieldsSaveResponseExempt !== false) {
1903+
return;
1904+
}
1905+
if (this.auth.isMaster || this.auth.isMaintenance) {
1906+
return;
1907+
}
1908+
if (!this.response || !this.response.response) {
1909+
return;
1910+
}
1911+
const schemaController = await this.config.database.loadSchema();
1912+
const protectedFields = this.config.database.addProtectedFields(
1913+
schemaController,
1914+
this.className,
1915+
this.query ? { objectId: this.query.objectId } : {},
1916+
this.auth.user ? [this.auth.user.id].concat(this.auth.userRoles || []) : [],
1917+
this.auth,
1918+
{}
1919+
);
1920+
if (!protectedFields) {
1921+
return;
1922+
}
1923+
for (const field of protectedFields) {
1924+
delete this.response.response[field];
1925+
}
1926+
};
1927+
18971928
RestWrite.prototype._updateResponseWithData = function (response, data) {
18981929
const stateController = Parse.CoreManager.getObjectStateController();
18991930
const [pending] = stateController.getPendingOps(this.pendingOps.identifier);

0 commit comments

Comments
 (0)