@@ -5885,4 +5885,102 @@ describe('Vulnerabilities', () => {
58855885 expect ( meResponse . data . sessionToken ) . toBe ( sessionToken ) ;
58865886 } ) ;
58875887 } ) ;
5888+
5889+ describe ( '(GHSA-38m6-82c8-4xfm) Pre-auth polynomial ReDoS via client version parsing' , ( ) => {
5890+ const middlewares = require ( '../lib/middlewares' ) ;
5891+ const AppCache = require ( '../lib/cache' ) . AppCache ;
5892+
5893+ const AppCachePut = ( appId , config ) =>
5894+ AppCache . put ( appId , {
5895+ ...config ,
5896+ maintenanceKeyIpsStore : new Map ( ) ,
5897+ masterKeyIpsStore : new Map ( ) ,
5898+ readOnlyMasterKeyIpsStore : new Map ( ) ,
5899+ } ) ;
5900+
5901+ const buildFakeReq = ( { headers = { } , body = { } } = { } ) => {
5902+ const req = {
5903+ ip : '127.0.0.1' ,
5904+ originalUrl : 'http://example.com/parse/' ,
5905+ url : 'http://example.com/' ,
5906+ body : { _ApplicationId : 'FakeAppId' , ...body } ,
5907+ headers,
5908+ get : key => req . headers [ key . toLowerCase ( ) ] ,
5909+ } ;
5910+ return req ;
5911+ } ;
5912+
5913+ beforeEach ( ( ) => {
5914+ AppCachePut ( 'FakeAppId' , {
5915+ masterKeyIps : [ '0.0.0.0/0' ] ,
5916+ } ) ;
5917+ } ) ;
5918+
5919+ afterEach ( ( ) => {
5920+ AppCache . del ( 'FakeAppId' ) ;
5921+ } ) ;
5922+
5923+ it ( 'does not capture client version from X-Parse-Client-Version header into req.info' , async ( ) => {
5924+ const req = buildFakeReq ( { headers : { 'x-parse-client-version' : 'js5.0.0' } } ) ;
5925+ const res = jasmine . createSpyObj ( 'res' , [ 'end' , 'status' ] ) ;
5926+ let nextCalled = false ;
5927+ await middlewares . handleParseHeaders ( req , res , ( ) => {
5928+ nextCalled = true ;
5929+ } ) ;
5930+ expect ( nextCalled ) . toBe ( true ) ;
5931+ expect ( res . status ) . not . toHaveBeenCalled ( ) ;
5932+ expect ( req . info . clientVersion ) . toBeUndefined ( ) ;
5933+ expect ( req . info . clientSDK ) . toBeUndefined ( ) ;
5934+ } ) ;
5935+
5936+ it ( 'does not capture client version from _ClientVersion body field into req.info' , async ( ) => {
5937+ const req = buildFakeReq ( { body : { _ClientVersion : 'js5.0.0' } } ) ;
5938+ const res = jasmine . createSpyObj ( 'res' , [ 'end' , 'status' ] ) ;
5939+ let nextCalled = false ;
5940+ await middlewares . handleParseHeaders ( req , res , ( ) => {
5941+ nextCalled = true ;
5942+ } ) ;
5943+ expect ( nextCalled ) . toBe ( true ) ;
5944+ expect ( res . status ) . not . toHaveBeenCalled ( ) ;
5945+ expect ( req . info . clientVersion ) . toBeUndefined ( ) ;
5946+ expect ( req . info . clientSDK ) . toBeUndefined ( ) ;
5947+ expect ( req . body . _ClientVersion ) . toBeUndefined ( ) ;
5948+ } ) ;
5949+
5950+ it ( 'does not invoke any regex on adversarial X-Parse-Client-Version header (16 KB of dashes)' , async ( ) => {
5951+ const adversarial = '-' . repeat ( 16000 ) ;
5952+ const req = buildFakeReq ( { headers : { 'x-parse-client-version' : adversarial } } ) ;
5953+ const res = jasmine . createSpyObj ( 'res' , [ 'end' , 'status' ] ) ;
5954+ await middlewares . handleParseHeaders ( req , res , ( ) => { } ) ;
5955+ expect ( req . info . clientVersion ) . toBeUndefined ( ) ;
5956+ expect ( req . info . clientSDK ) . toBeUndefined ( ) ;
5957+ } ) ;
5958+
5959+ it ( 'does not invoke any regex on adversarial _ClientVersion body field (200 KB of dashes)' , async ( ) => {
5960+ const adversarial = '-' . repeat ( 200000 ) ;
5961+ const req = buildFakeReq ( { body : { _ClientVersion : adversarial } } ) ;
5962+ const res = jasmine . createSpyObj ( 'res' , [ 'end' , 'status' ] ) ;
5963+ const t0 = process . hrtime . bigint ( ) ;
5964+ await middlewares . handleParseHeaders ( req , res , ( ) => { } ) ;
5965+ const elapsedMs = Number ( process . hrtime . bigint ( ) - t0 ) / 1e6 ;
5966+ expect ( elapsedMs ) . toBeLessThan ( 3000 ) ;
5967+ expect ( req . info . clientVersion ) . toBeUndefined ( ) ;
5968+ expect ( req . info . clientSDK ) . toBeUndefined ( ) ;
5969+ expect ( req . body . _ClientVersion ) . toBeUndefined ( ) ;
5970+ } ) ;
5971+
5972+ it ( 'strips _ClientVersion from req.body even when value is non-string (no rejection, no capture)' , async ( ) => {
5973+ const req = buildFakeReq ( { body : { _ClientVersion : { toLowerCase : 'evil' } } } ) ;
5974+ const res = jasmine . createSpyObj ( 'res' , [ 'end' , 'status' ] ) ;
5975+ let nextCalled = false ;
5976+ await middlewares . handleParseHeaders ( req , res , ( ) => {
5977+ nextCalled = true ;
5978+ } ) ;
5979+ expect ( nextCalled ) . toBe ( true ) ;
5980+ expect ( res . status ) . not . toHaveBeenCalled ( ) ;
5981+ expect ( req . body . _ClientVersion ) . toBeUndefined ( ) ;
5982+ expect ( req . info . clientVersion ) . toBeUndefined ( ) ;
5983+ expect ( req . info . clientSDK ) . toBeUndefined ( ) ;
5984+ } ) ;
5985+ } ) ;
58885986} ) ;
0 commit comments