From 96c6670d5b54861d13295fd876eec788b2f1cd59 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:34:59 +0000 Subject: [PATCH 1/3] fix: GHSA-wjqw-r9x4-j59v v9 --- spec/vulnerabilities.spec.js | 79 ++++++++++++++++++++++++++++++++++++ src/RestWrite.js | 10 ++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index f645b95628..3481ee5330 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -2934,6 +2934,85 @@ describe('(GHSA-fjxm-vhvc-gcmj) LiveQuery Operator Type Confusion', () => { }); }); + describe('(GHSA-wjqw-r9x4-j59v) Empty authData session issuance bypass', () => { + it('rejects signup with empty authData and no credentials', async () => { + await reconfigureServer({ enableAnonymousUsers: false }); + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/users', + headers: { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body: JSON.stringify({ authData: {} }), + }) + ).toBeRejected(); + }); + + it('rejects signup with empty authData and no credentials when anonymous users enabled', async () => { + await reconfigureServer({ enableAnonymousUsers: true }); + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/users', + headers: { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body: JSON.stringify({ authData: {} }), + }) + ).toBeRejected(); + }); + + it('rejects signup with authData containing only empty provider data and no credentials', async () => { + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/users', + headers: { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body: JSON.stringify({ authData: { bogus: {} } }), + }) + ).toBeRejected(); + }); + + it('rejects signup with authData containing null provider data and no credentials', async () => { + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/users', + headers: { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body: JSON.stringify({ authData: { bogus: null } }), + }) + ).toBeRejected(); + }); + + it('allows signup with empty authData when username and password are provided', async () => { + const res = await request({ + method: 'POST', + url: 'http://localhost:8378/1/users', + headers: { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body: JSON.stringify({ username: 'emptyauth', password: 'pass1234', authData: {} }), + }); + expect(res.data.objectId).toBeDefined(); + expect(res.data.sessionToken).toBeDefined(); + }); + }); + describe('(GHSA-r3xq-68wh-gwvh) Password reset single-use token bypass via concurrent requests', () => { let sendPasswordResetEmail; diff --git a/src/RestWrite.js b/src/RestWrite.js index 937f8644e9..af2a04f52c 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -452,8 +452,14 @@ RestWrite.prototype.validateAuthData = function () { const authData = this.data.authData; const hasUsernameAndPassword = typeof this.data.username === 'string' && typeof this.data.password === 'string'; + const hasAuthData = + authData && + Object.keys(authData).some(provider => { + const providerData = authData[provider]; + return providerData && typeof providerData === 'object' && Object.keys(providerData).length; + }); - if (!this.query && !authData) { + if (!this.query && !hasAuthData) { if (typeof this.data.username !== 'string' || _.isEmpty(this.data.username)) { throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'bad or missing username'); } @@ -463,7 +469,7 @@ RestWrite.prototype.validateAuthData = function () { } if ( - (authData && !Object.keys(authData).length) || + !hasAuthData || !Object.prototype.hasOwnProperty.call(this.data, 'authData') ) { // Nothing to validate here From 41d6355bfb3525d6765cbb54b2b8b8446f64a497 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:15:54 +0000 Subject: [PATCH 2/3] fix: address review feedback - Separate credential gating from provider validation skip - Non-object authData values now still go through provider validation - Strengthen test assertions with specific status codes and error codes --- spec/vulnerabilities.spec.js | 86 ++++++++++++++++-------------------- src/RestWrite.js | 25 +++++------ 2 files changed, 50 insertions(+), 61 deletions(-) diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index 3481ee5330..d7c1dded6b 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -2935,66 +2935,56 @@ describe('(GHSA-fjxm-vhvc-gcmj) LiveQuery Operator Type Confusion', () => { }); describe('(GHSA-wjqw-r9x4-j59v) Empty authData session issuance bypass', () => { + const signupHeaders = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + it('rejects signup with empty authData and no credentials', async () => { await reconfigureServer({ enableAnonymousUsers: false }); - await expectAsync( - request({ - method: 'POST', - url: 'http://localhost:8378/1/users', - headers: { - 'Content-Type': 'application/json', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - }, - body: JSON.stringify({ authData: {} }), - }) - ).toBeRejected(); + const res = await request({ + method: 'POST', + url: 'http://localhost:8378/1/users', + headers: signupHeaders, + body: JSON.stringify({ authData: {} }), + }).catch(e => e); + expect(res.status).toBe(400); + expect(res.data.code).toBe(Parse.Error.USERNAME_MISSING); }); it('rejects signup with empty authData and no credentials when anonymous users enabled', async () => { await reconfigureServer({ enableAnonymousUsers: true }); - await expectAsync( - request({ - method: 'POST', - url: 'http://localhost:8378/1/users', - headers: { - 'Content-Type': 'application/json', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - }, - body: JSON.stringify({ authData: {} }), - }) - ).toBeRejected(); + const res = await request({ + method: 'POST', + url: 'http://localhost:8378/1/users', + headers: signupHeaders, + body: JSON.stringify({ authData: {} }), + }).catch(e => e); + expect(res.status).toBe(400); + expect(res.data.code).toBe(Parse.Error.USERNAME_MISSING); }); it('rejects signup with authData containing only empty provider data and no credentials', async () => { - await expectAsync( - request({ - method: 'POST', - url: 'http://localhost:8378/1/users', - headers: { - 'Content-Type': 'application/json', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - }, - body: JSON.stringify({ authData: { bogus: {} } }), - }) - ).toBeRejected(); + const res = await request({ + method: 'POST', + url: 'http://localhost:8378/1/users', + headers: signupHeaders, + body: JSON.stringify({ authData: { bogus: {} } }), + }).catch(e => e); + expect(res.status).toBe(400); + expect(res.data.code).toBe(Parse.Error.USERNAME_MISSING); }); it('rejects signup with authData containing null provider data and no credentials', async () => { - await expectAsync( - request({ - method: 'POST', - url: 'http://localhost:8378/1/users', - headers: { - 'Content-Type': 'application/json', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - }, - body: JSON.stringify({ authData: { bogus: null } }), - }) - ).toBeRejected(); + const res = await request({ + method: 'POST', + url: 'http://localhost:8378/1/users', + headers: signupHeaders, + body: JSON.stringify({ authData: { bogus: null } }), + }).catch(e => e); + expect(res.status).toBe(400); + expect(res.data.code).toBe(Parse.Error.USERNAME_MISSING); }); it('allows signup with empty authData when username and password are provided', async () => { diff --git a/src/RestWrite.js b/src/RestWrite.js index af2a04f52c..e986a35c67 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -468,13 +468,10 @@ RestWrite.prototype.validateAuthData = function () { } } - if ( - !hasAuthData || - !Object.prototype.hasOwnProperty.call(this.data, 'authData') - ) { + if (!Object.prototype.hasOwnProperty.call(this.data, 'authData')) { // Nothing to validate here return; - } else if (Object.prototype.hasOwnProperty.call(this.data, 'authData') && !this.data.authData) { + } else if (!this.data.authData) { // Handle saving authData to null throw new Parse.Error( Parse.Error.UNSUPPORTED_SERVICE, @@ -483,14 +480,16 @@ RestWrite.prototype.validateAuthData = function () { } var providers = Object.keys(authData); - if (providers.length > 0) { - const canHandleAuthData = providers.some(provider => { - const providerAuthData = authData[provider] || {}; - return !!Object.keys(providerAuthData).length; - }); - if (canHandleAuthData || hasUsernameAndPassword || this.auth.isMaster || this.getUserId()) { - return this.handleAuthData(authData); - } + if (!providers.length) { + // Empty authData object, nothing to validate + return; + } + const canHandleAuthData = providers.some(provider => { + const providerAuthData = authData[provider] || {}; + return !!Object.keys(providerAuthData).length; + }); + if (canHandleAuthData || hasUsernameAndPassword || this.auth.isMaster || this.getUserId()) { + return this.handleAuthData(authData); } throw new Parse.Error( Parse.Error.UNSUPPORTED_SERVICE, From 3074910e5fc8878503e7cb595afbbf1d332c4955 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:16:59 +0000 Subject: [PATCH 3/3] test: add regression test for non-object authData provider validation --- spec/vulnerabilities.spec.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index d7c1dded6b..a8567a3718 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -2987,15 +2987,22 @@ describe('(GHSA-fjxm-vhvc-gcmj) LiveQuery Operator Type Confusion', () => { expect(res.data.code).toBe(Parse.Error.USERNAME_MISSING); }); + it('rejects signup with non-object authData provider value even when credentials are provided', async () => { + const res = await request({ + method: 'POST', + url: 'http://localhost:8378/1/users', + headers: signupHeaders, + body: JSON.stringify({ username: 'bogusauth', password: 'pass1234', authData: { bogus: 'x' } }), + }).catch(e => e); + expect(res.status).toBe(400); + expect(res.data.code).toBe(Parse.Error.UNSUPPORTED_SERVICE); + }); + it('allows signup with empty authData when username and password are provided', async () => { const res = await request({ method: 'POST', url: 'http://localhost:8378/1/users', - headers: { - 'Content-Type': 'application/json', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - }, + headers: signupHeaders, body: JSON.stringify({ username: 'emptyauth', password: 'pass1234', authData: {} }), }); expect(res.data.objectId).toBeDefined();