@@ -1706,6 +1706,213 @@ describe('(GHSA-r2m8-pxm9-9c4g) Protected fields WHERE clause bypass via dot-not
17061706 } ) ;
17071707} ) ;
17081708
1709+ describe ( '(GHSA-j7mm-f4rv-6q6q) Protected fields bypass via LiveQuery dot-notation WHERE' , ( ) => {
1710+ let obj ;
1711+
1712+ beforeEach ( async ( ) => {
1713+ Parse . CoreManager . getLiveQueryController ( ) . setDefaultLiveQueryClient ( null ) ;
1714+ await reconfigureServer ( {
1715+ liveQuery : { classNames : [ 'SecretClass' ] } ,
1716+ startLiveQueryServer : true ,
1717+ verbose : false ,
1718+ silent : true ,
1719+ } ) ;
1720+ const config = Config . get ( Parse . applicationId ) ;
1721+ const schemaController = await config . database . loadSchema ( ) ;
1722+ await schemaController . addClassIfNotExists (
1723+ 'SecretClass' ,
1724+ { secretObj : { type : 'Object' } , publicField : { type : 'String' } } ,
1725+ ) ;
1726+ await schemaController . updateClass (
1727+ 'SecretClass' ,
1728+ { } ,
1729+ {
1730+ find : { '*' : true } ,
1731+ get : { '*' : true } ,
1732+ create : { '*' : true } ,
1733+ update : { '*' : true } ,
1734+ delete : { '*' : true } ,
1735+ addField : { } ,
1736+ protectedFields : { '*' : [ 'secretObj' ] } ,
1737+ }
1738+ ) ;
1739+
1740+ obj = new Parse . Object ( 'SecretClass' ) ;
1741+ obj . set ( 'secretObj' , { apiKey : 'SENSITIVE_KEY_123' , score : 42 } ) ;
1742+ obj . set ( 'publicField' , 'visible' ) ;
1743+ await obj . save ( null , { useMasterKey : true } ) ;
1744+ } ) ;
1745+
1746+ afterEach ( async ( ) => {
1747+ const client = await Parse . CoreManager . getLiveQueryController ( ) . getDefaultLiveQueryClient ( ) ;
1748+ if ( client ) {
1749+ await client . close ( ) ;
1750+ }
1751+ } ) ;
1752+
1753+ it ( 'should reject LiveQuery subscription with dot-notation on protected field in where clause' , async ( ) => {
1754+ const query = new Parse . Query ( 'SecretClass' ) ;
1755+ query . _addCondition ( 'secretObj.apiKey' , '$eq' , 'SENSITIVE_KEY_123' ) ;
1756+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
1757+ new Parse . Error ( Parse . Error . OPERATION_FORBIDDEN , 'Permission denied' )
1758+ ) ;
1759+ } ) ;
1760+
1761+ it ( 'should reject LiveQuery subscription with protected field directly in where clause' , async ( ) => {
1762+ const query = new Parse . Query ( 'SecretClass' ) ;
1763+ query . exists ( 'secretObj' ) ;
1764+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
1765+ new Parse . Error ( Parse . Error . OPERATION_FORBIDDEN , 'Permission denied' )
1766+ ) ;
1767+ } ) ;
1768+
1769+ it ( 'should reject LiveQuery subscription with protected field in $or' , async ( ) => {
1770+ const q1 = new Parse . Query ( 'SecretClass' ) ;
1771+ q1 . _addCondition ( 'secretObj.apiKey' , '$eq' , 'SENSITIVE_KEY_123' ) ;
1772+ const q2 = new Parse . Query ( 'SecretClass' ) ;
1773+ q2 . _addCondition ( 'secretObj.apiKey' , '$eq' , 'other' ) ;
1774+ const query = Parse . Query . or ( q1 , q2 ) ;
1775+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
1776+ new Parse . Error ( Parse . Error . OPERATION_FORBIDDEN , 'Permission denied' )
1777+ ) ;
1778+ } ) ;
1779+
1780+ it ( 'should reject LiveQuery subscription with protected field in $and' , async ( ) => {
1781+ // Build $and manually since Parse SDK doesn't expose it directly
1782+ const query = new Parse . Query ( 'SecretClass' ) ;
1783+ query . _where = { $and : [ { 'secretObj.apiKey' : 'SENSITIVE_KEY_123' } , { publicField : 'visible' } ] } ;
1784+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
1785+ new Parse . Error ( Parse . Error . OPERATION_FORBIDDEN , 'Permission denied' )
1786+ ) ;
1787+ } ) ;
1788+
1789+ it ( 'should reject LiveQuery subscription with protected field in $nor' , async ( ) => {
1790+ // Build $nor manually since Parse SDK doesn't expose it directly
1791+ const query = new Parse . Query ( 'SecretClass' ) ;
1792+ query . _where = { $nor : [ { 'secretObj.apiKey' : 'SENSITIVE_KEY_123' } ] } ;
1793+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
1794+ new Parse . Error ( Parse . Error . OPERATION_FORBIDDEN , 'Permission denied' )
1795+ ) ;
1796+ } ) ;
1797+
1798+ it ( 'should reject LiveQuery subscription with $regex on protected field (boolean oracle)' , async ( ) => {
1799+ const query = new Parse . Query ( 'SecretClass' ) ;
1800+ query . _addCondition ( 'secretObj.apiKey' , '$regex' , '^S' ) ;
1801+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
1802+ new Parse . Error ( Parse . Error . OPERATION_FORBIDDEN , 'Permission denied' )
1803+ ) ;
1804+ } ) ;
1805+
1806+ it ( 'should reject LiveQuery subscription with deeply nested dot-notation on protected field' , async ( ) => {
1807+ const query = new Parse . Query ( 'SecretClass' ) ;
1808+ query . _addCondition ( 'secretObj.nested.deep.key' , '$eq' , 'value' ) ;
1809+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
1810+ new Parse . Error ( Parse . Error . OPERATION_FORBIDDEN , 'Permission denied' )
1811+ ) ;
1812+ } ) ;
1813+
1814+ it ( 'should allow LiveQuery subscription on non-protected fields and strip protected fields from response' , async ( ) => {
1815+ const query = new Parse . Query ( 'SecretClass' ) ;
1816+ query . exists ( 'publicField' ) ;
1817+ const subscription = await query . subscribe ( ) ;
1818+ await Promise . all ( [
1819+ new Promise ( resolve => {
1820+ subscription . on ( 'update' , object => {
1821+ expect ( object . get ( 'secretObj' ) ) . toBeUndefined ( ) ;
1822+ expect ( object . get ( 'publicField' ) ) . toBe ( 'updated' ) ;
1823+ resolve ( ) ;
1824+ } ) ;
1825+ } ) ,
1826+ obj . save ( { publicField : 'updated' } , { useMasterKey : true } ) ,
1827+ ] ) ;
1828+ } ) ;
1829+
1830+ it ( 'should reject admin user querying protected field when both * and role protect it' , async ( ) => {
1831+ // Common case: protectedFields has both '*' and 'role:admin' entries.
1832+ // Even without resolving user roles, the '*' protection applies and blocks the query.
1833+ // This validates that role-based exemptions are irrelevant when '*' covers the field.
1834+ const config = Config . get ( Parse . applicationId ) ;
1835+ const schemaController = await config . database . loadSchema ( ) ;
1836+ await schemaController . updateClass (
1837+ 'SecretClass' ,
1838+ { } ,
1839+ {
1840+ find : { '*' : true } ,
1841+ get : { '*' : true } ,
1842+ create : { '*' : true } ,
1843+ update : { '*' : true } ,
1844+ delete : { '*' : true } ,
1845+ addField : { } ,
1846+ protectedFields : { '*' : [ 'secretObj' ] , 'role:admin' : [ 'secretObj' ] } ,
1847+ }
1848+ ) ;
1849+
1850+ const user = new Parse . User ( ) ;
1851+ user . setUsername ( 'adminuser' ) ;
1852+ user . setPassword ( 'password' ) ;
1853+ await user . signUp ( ) ;
1854+
1855+ const roleACL = new Parse . ACL ( ) ;
1856+ roleACL . setPublicReadAccess ( true ) ;
1857+ const role = new Parse . Role ( 'admin' , roleACL ) ;
1858+ role . getUsers ( ) . add ( user ) ;
1859+ await role . save ( null , { useMasterKey : true } ) ;
1860+
1861+ const query = new Parse . Query ( 'SecretClass' ) ;
1862+ query . _addCondition ( 'secretObj.apiKey' , '$eq' , 'SENSITIVE_KEY_123' ) ;
1863+ await expectAsync ( query . subscribe ( user . getSessionToken ( ) ) ) . toBeRejectedWith (
1864+ new Parse . Error ( Parse . Error . OPERATION_FORBIDDEN , 'Permission denied' )
1865+ ) ;
1866+ } ) ;
1867+
1868+ it ( 'should not reject when role-only protection exists without * entry' , async ( ) => {
1869+ // Edge case: protectedFields only has a role entry, no '*'.
1870+ // Without resolving roles, the protection set is empty, so the subscription is allowed.
1871+ // This is a correctness gap, not a security issue: the role entry means "protect this
1872+ // field FROM role members" (i.e. admins should not see it). Not resolving roles means
1873+ // the admin loses their own restriction — they see data meant to be hidden from them.
1874+ // This does not allow unprivileged users to access protected data.
1875+ const config = Config . get ( Parse . applicationId ) ;
1876+ const schemaController = await config . database . loadSchema ( ) ;
1877+ await schemaController . updateClass (
1878+ 'SecretClass' ,
1879+ { } ,
1880+ {
1881+ find : { '*' : true } ,
1882+ get : { '*' : true } ,
1883+ create : { '*' : true } ,
1884+ update : { '*' : true } ,
1885+ delete : { '*' : true } ,
1886+ addField : { } ,
1887+ protectedFields : { 'role:admin' : [ 'secretObj' ] } ,
1888+ }
1889+ ) ;
1890+
1891+ const user = new Parse . User ( ) ;
1892+ user . setUsername ( 'adminuser2' ) ;
1893+ user . setPassword ( 'password' ) ;
1894+ await user . signUp ( ) ;
1895+
1896+ const roleACL = new Parse . ACL ( ) ;
1897+ roleACL . setPublicReadAccess ( true ) ;
1898+ const role = new Parse . Role ( 'admin' , roleACL ) ;
1899+ role . getUsers ( ) . add ( user ) ;
1900+ await role . save ( null , { useMasterKey : true } ) ;
1901+
1902+ // This subscribes successfully because without '*' entry, no fields are protected
1903+ // for purposes of WHERE clause validation. The role-only config means "hide secretObj
1904+ // from admins" — a restriction ON the privileged user, not a security boundary.
1905+ const query = new Parse . Query ( 'SecretClass' ) ;
1906+ query . _addCondition ( 'secretObj.apiKey' , '$eq' , 'SENSITIVE_KEY_123' ) ;
1907+ const subscription = await query . subscribe ( user . getSessionToken ( ) ) ;
1908+ expect ( subscription ) . toBeDefined ( ) ;
1909+ } ) ;
1910+
1911+ // Note: master key bypass is inherently tested by the `!client.hasMasterKey` guard
1912+ // in the implementation. Testing master key LiveQuery requires configuring keyPairs
1913+ // in the LiveQuery server config, which is not part of the default test setup.
1914+ } ) ;
1915+
17091916describe ( '(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint' , ( ) => {
17101917 let sendVerificationEmail ;
17111918
0 commit comments