diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md
index de1b09b5b8..be75c111b8 100644
--- a/changelogs/CHANGELOG_alpha.md
+++ b/changelogs/CHANGELOG_alpha.md
@@ -1,3 +1,10 @@
+## [9.6.1-alpha.1](https://github.com/parse-community/parse-server/compare/9.6.0...9.6.1-alpha.1) (2026-03-22)
+
+
+### Bug Fixes
+
+* User cannot retrieve own email with `protectedFieldsOwnerExempt: false` despite `email` not in `protectedFields` ([#10284](https://github.com/parse-community/parse-server/issues/10284)) ([4a65d77](https://github.com/parse-community/parse-server/commit/4a65d77ea3fd2ccb121d4bd28e92435295203bf7))
+
# [9.6.0-alpha.56](https://github.com/parse-community/parse-server/compare/9.6.0-alpha.55...9.6.0-alpha.56) (2026-03-22)
diff --git a/package-lock.json b/package-lock.json
index 900d57538e..941493522e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "parse-server",
- "version": "9.6.0",
+ "version": "9.6.1-alpha.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "parse-server",
- "version": "9.6.0",
+ "version": "9.6.1-alpha.1",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
diff --git a/package.json b/package.json
index 20564cf248..aed8111d6c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "parse-server",
- "version": "9.6.0",
+ "version": "9.6.1-alpha.1",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
diff --git a/spec/ProtectedFields.spec.js b/spec/ProtectedFields.spec.js
index 24101b21a0..1771cbf8a8 100644
--- a/spec/ProtectedFields.spec.js
+++ b/spec/ProtectedFields.spec.js
@@ -1972,5 +1972,61 @@ describe('ProtectedFields', function () {
expect(response.data.phone).toBeUndefined();
expect(response.data.objectId).toBe(user.id);
});
+
+ it('owner sees non-protected fields like email when protectedFieldsOwnerExempt is true', async function () {
+ await reconfigureServer({
+ protectedFields: {
+ _User: {
+ '*': ['phone'],
+ },
+ },
+ protectedFieldsOwnerExempt: true,
+ });
+ const user = await Parse.User.signUp('user1', 'password');
+ const sessionToken = user.getSessionToken();
+ user.set('phone', '555-1234');
+ user.set('email', 'user1@example.com');
+ await user.save(null, { sessionToken });
+
+ // Owner fetches own object â phone and email should be visible (owner exempt)
+ const response = await request({
+ url: `http://localhost:8378/1/users/${user.id}`,
+ headers: {
+ 'X-Parse-Application-Id': 'test',
+ 'X-Parse-REST-API-Key': 'rest',
+ 'X-Parse-Session-Token': sessionToken,
+ },
+ });
+ expect(response.data.phone).toBe('555-1234');
+ expect(response.data.email).toBe('user1@example.com');
+ });
+
+ it('owner sees non-protected fields like email when protectedFieldsOwnerExempt is false', async function () {
+ await reconfigureServer({
+ protectedFields: {
+ _User: {
+ '*': ['phone'],
+ },
+ },
+ protectedFieldsOwnerExempt: false,
+ });
+ const user = await Parse.User.signUp('user1', 'password');
+ const sessionToken = user.getSessionToken();
+ user.set('phone', '555-1234');
+ user.set('email', 'user1@example.com');
+ await user.save(null, { sessionToken });
+
+ // Owner fetches own object â phone should be hidden, email should be visible
+ const response = await request({
+ url: `http://localhost:8378/1/users/${user.id}`,
+ headers: {
+ 'X-Parse-Application-Id': 'test',
+ 'X-Parse-REST-API-Key': 'rest',
+ 'X-Parse-Session-Token': sessionToken,
+ },
+ });
+ expect(response.data.phone).toBeUndefined();
+ expect(response.data.email).toBe('user1@example.com');
+ });
});
});
diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js
index f5da09c352..6ebff326fe 100644
--- a/src/Options/Definitions.js
+++ b/src/Options/Definitions.js
@@ -480,7 +480,7 @@ module.exports.ParseServerOptions = {
},
protectedFieldsOwnerExempt: {
env: 'PARSE_SERVER_PROTECTED_FIELDS_OWNER_EXEMPT',
- help: "Whether the `_User` class is exempt from `protectedFields` when the logged-in user queries their own user object. If `true` (default), a user can see all their own fields regardless of `protectedFields` configuration. If `false`, `protectedFields` applies equally to the user's own object, consistent with all other classes. Defaults to `true`.",
+ help: "Whether the `_User` class is exempt from `protectedFields` when the logged-in user queries their own user object. If `true` (default), a user can see all their own fields regardless of `protectedFields` configuration; default protected fields (e.g. `email`) are merged into any custom `protectedFields` configuration. If `false`, `protectedFields` applies equally to the user's own object, consistent with all other classes; only explicitly configured protected fields apply, defaults are not merged. Defaults to `true`.",
action: parsers.booleanParser,
default: true,
},
diff --git a/src/Options/docs.js b/src/Options/docs.js
index 8b0d520eee..31f245f2c0 100644
--- a/src/Options/docs.js
+++ b/src/Options/docs.js
@@ -89,7 +89,7 @@
* @property {Boolean} preventLoginWithUnverifiedEmail Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Supports a function with a return value of `true` or `false` for conditional prevention. The function receives a request object that includes `createdWith` to indicate whether the invocation is for `signup` or `login` and the used auth provider.
The `createdWith` values per scenario:
- Password signup: `{ action: 'signup', authProvider: 'password' }`
- Auth provider signup: `{ action: 'signup', authProvider: '' }`
- Password login: `{ action: 'login', authProvider: 'password' }`
- Auth provider login: function not invoked; auth provider login bypasses email verification
Default is `false`.
Requires option `verifyUserEmails: true`.
* @property {Boolean} preventSignupWithUnverifiedEmail If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.
Default is `false`.
Requires option `verifyUserEmails: true`.
* @property {ProtectedFields} protectedFields Fields per class that are hidden from query results for specific user groups. Protected fields are stripped from the server response, but can still be used internally (e.g. in Cloud Code triggers). Configure as `{ 'ClassName': { 'UserGroup': ['field1', 'field2'] } }` where `UserGroup` is one of: `'*'` (all users), `'authenticated'` (authenticated users), `'role:RoleName'` (users with a specific role), `'userField:FieldName'` (users referenced by a pointer field), or a user `objectId` to target a specific user. When multiple groups apply, the intersection of their protected fields is used. By default, `email` is protected on the `_User` class for all users. On the `_User` class, the object owner is exempt from protected fields by default; see `protectedFieldsOwnerExempt` to change this.
- * @property {Boolean} protectedFieldsOwnerExempt Whether the `_User` class is exempt from `protectedFields` when the logged-in user queries their own user object. If `true` (default), a user can see all their own fields regardless of `protectedFields` configuration. If `false`, `protectedFields` applies equally to the user's own object, consistent with all other classes. Defaults to `true`.
+ * @property {Boolean} protectedFieldsOwnerExempt Whether the `_User` class is exempt from `protectedFields` when the logged-in user queries their own user object. If `true` (default), a user can see all their own fields regardless of `protectedFields` configuration; default protected fields (e.g. `email`) are merged into any custom `protectedFields` configuration. If `false`, `protectedFields` applies equally to the user's own object, consistent with all other classes; only explicitly configured protected fields apply, defaults are not merged. Defaults to `true`.
* @property {Union} publicServerURL Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`.
* @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications
* @property {RateLimitOptions[]} rateLimit Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.
âšī¸ Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and use case.
diff --git a/src/Options/index.js b/src/Options/index.js
index 32878d23b3..9699015bc7 100644
--- a/src/Options/index.js
+++ b/src/Options/index.js
@@ -171,7 +171,7 @@ export interface ParseServerOptions {
/* Fields per class that are hidden from query results for specific user groups. Protected fields are stripped from the server response, but can still be used internally (e.g. in Cloud Code triggers). Configure as `{ 'ClassName': { 'UserGroup': ['field1', 'field2'] } }` where `UserGroup` is one of: `'*'` (all users), `'authenticated'` (authenticated users), `'role:RoleName'` (users with a specific role), `'userField:FieldName'` (users referenced by a pointer field), or a user `objectId` to target a specific user. When multiple groups apply, the intersection of their protected fields is used. By default, `email` is protected on the `_User` class for all users. On the `_User` class, the object owner is exempt from protected fields by default; see `protectedFieldsOwnerExempt` to change this.
:DEFAULT: {"_User": {"*": ["email"]}} */
protectedFields: ?ProtectedFields;
- /* Whether the `_User` class is exempt from `protectedFields` when the logged-in user queries their own user object. If `true` (default), a user can see all their own fields regardless of `protectedFields` configuration. If `false`, `protectedFields` applies equally to the user's own object, consistent with all other classes. Defaults to `true`.
+ /* Whether the `_User` class is exempt from `protectedFields` when the logged-in user queries their own user object. If `true` (default), a user can see all their own fields regardless of `protectedFields` configuration; default protected fields (e.g. `email`) are merged into any custom `protectedFields` configuration. If `false`, `protectedFields` applies equally to the user's own object, consistent with all other classes; only explicitly configured protected fields apply, defaults are not merged. Defaults to `true`.
:ENV: PARSE_SERVER_PROTECTED_FIELDS_OWNER_EXEMPT
:DEFAULT: true */
protectedFieldsOwnerExempt: ?boolean;
diff --git a/src/ParseServer.ts b/src/ParseServer.ts
index b1f03c5863..edc085cd74 100644
--- a/src/ParseServer.ts
+++ b/src/ParseServer.ts
@@ -666,6 +666,9 @@ function injectDefaults(options: ParseServerOptions) {
options.protectedFields[c] = defaults.protectedFields[c];
} else {
Object.keys(defaults.protectedFields[c]).forEach(r => {
+ if (options.protectedFields[c][r] && options.protectedFieldsOwnerExempt === false) {
+ return;
+ }
const unq = new Set([
...(options.protectedFields[c][r] || []),
...defaults.protectedFields[c][r],