@@ -5,7 +5,6 @@ const fs = require('fs');
55const crypto = require ( 'crypto' ) ;
66
77const { ModuleMode } = require ( './enums/ModuleMode' ) ;
8- const { GraphqlOperationType } = require ( './enums/GraphqlOperationType' ) ;
98const { GraphqlData } = require ( './models/GraphqlData' ) ;
109const { EMAIL_ADDRESS_REGEX , HASH_ALGORITHM } = require ( './utils/constants' ) ;
1110
@@ -267,64 +266,121 @@ function getTokenObject(cookie, delimiter = ':') {
267266 return { key : '_px3' , value : cookie } ;
268267}
269268
270- function getGraphqlData ( req ) {
271- const isGraphqlPath = req . path . includes ( 'graphql' ) || req . originalUrl . includes ( 'graphql' ) ;
272- if ( ! isGraphqlPath || ! req . body ) {
273- return null ;
269+ function isGraphql ( req , config ) {
270+ if ( req . method . toLowerCase ( ) !== 'post' ) {
271+ return false ;
274272 }
275273
276- const { body, query } = getGraphqlBodyAndQuery ( req ) ;
277- if ( ! body || ! query ) {
278- return null ;
274+ const routes = config [ 'GRAPHQL_ROUTES' ] ;
275+ if ( ! Array . isArray ( routes ) ) {
276+ config . logger . error ( 'Invalid configuration px_graphql_routes' ) ;
277+ return false ;
278+ }
279+ try {
280+ return routes . some ( r => new RegExp ( r ) . test ( req . baseUrl || '' + req . path ) ) ;
281+ } catch ( e ) {
282+ config . logger . error ( `Failed to process graphql routes. exception: ${ e } ` ) ;
283+ return false ;
279284 }
280-
281- const operationType = extractGraphqlOperationType ( query ) ;
282- const operationName = extractGraphqlOperationName ( query , body [ 'operationName' ] ) ;
283- return new GraphqlData ( operationType , operationName ) ;
284285}
285286
286- function getGraphqlBodyAndQuery ( req ) {
287- let body = { } ;
288- let query = '' ;
289-
290- try {
291- body = typeof req . body === 'string' ? JSON . parse ( req . body ) : req . body ;
292- query = body [ 'query' ] ;
293- } catch ( err ) {
294- // json parse error
287+ // query: string (not null)
288+ // output: Record [ OperationName -> OperationType ]
289+ function parseGraphqlBody ( query ) {
290+ const pattern = / \s * ( q u e r y | m u t a t i o n | s u b s c r i p t i o n ) \s + ( \w + ) / gm;
291+ let match ;
292+ const ret = { } ;
293+ while ( ( match = pattern . exec ( query ) ) !== null ) {
294+ const operationName = match [ 2 ] ;
295+ const operationType = match [ 1 ] ;
296+
297+ // if two operations have the same name, the query is illegal.
298+ if ( ret [ operationName ] ) {
299+ return null ;
300+ } else {
301+ ret [ operationName ] = operationType ;
302+ }
295303 }
296304
297- return { body , query } ;
305+ return ret ;
298306}
299307
300- function extractGraphqlOperationType ( query ) {
301- const isGraphqlQueryShorthand = query [ 0 ] === '{' ;
302- if ( isGraphqlQueryShorthand ) {
303- return GraphqlOperationType . QUERY ;
308+ // graphqlData: GraphqlData
309+ // output: boolean
310+ function isSensitiveGraphqlOperation ( graphqlData , config ) {
311+ if ( ! graphqlData ) {
312+ return false ;
313+ } else {
314+ return ( config . SENSITIVE_GRAPHQL_OPERATION_TYPES . includes ( graphqlData . type ) ||
315+ config . SENSITIVE_GRAPHQL_OPERATION_NAMES . includes ( graphqlData . name ) ) ;
304316 }
305-
306- const queryArray = query . split ( / [ ^ A - Z a - z 0 - 9 _ ] / ) ;
307- return isValidGraphqlOperationType ( queryArray [ 0 ] ) ? queryArray [ 0 ] : GraphqlOperationType . QUERY ;
308317}
309318
310- function extractGraphqlOperationName ( query , operationName ) {
311- if ( operationName ) {
312- return operationName ;
319+ // graphqlBodyObject: {query: string?, operationName: string?, variables: any[]?}
320+ // output: GraphqlData?
321+ function getGraphqlData ( graphqlBodyObject ) {
322+ if ( ! graphqlBodyObject || ! graphqlBodyObject . query ) {
323+ return null ;
324+ }
325+
326+ const parsedData = parseGraphqlBody ( graphqlBodyObject . query ) ;
327+ if ( ! parsedData ) {
328+ return null ;
329+ }
330+
331+ const selectedOperationName = graphqlBodyObject [ 'operationName' ] ||
332+ ( Object . keys ( parsedData ) . length === 1 && Object . keys ( parsedData ) [ 0 ] ) ;
333+
334+ if ( ! selectedOperationName || ! parsedData [ selectedOperationName ] ) {
335+ return null ;
313336 }
314337
315- const queryArray = query . split ( / [ ^ A - Z a - z 0 - 9 _ ] / ) ;
316- return isValidGraphqlOperationType ( queryArray [ 0 ] ) ? queryArray [ 1 ] : queryArray [ 0 ] ;
338+ const variables = extractVariables ( graphqlBodyObject . variables ) ;
339+
340+ return new GraphqlData ( parsedData [ selectedOperationName ] ,
341+ selectedOperationName ,
342+ variables ,
343+ ) ;
317344}
318345
319- function isValidGraphqlOperationType ( operationType ) {
320- return Object . values ( GraphqlOperationType ) . includes ( operationType ) ;
346+ // input: object representing variables
347+ // output: list of keys recursively like property file.
348+ function extractVariables ( variables ) {
349+ function go ( variables , prefix ) {
350+ return Object . entries ( variables ) . reduce ( ( total , [ key , value ] ) => {
351+ if ( ! value || typeof value !== 'object' || Object . keys ( value ) . length === 0 ) {
352+ total . push ( prefix + key ) ;
353+ return total ;
354+ } else {
355+ return total . concat ( go ( value , prefix + key + '.' ) ) ;
356+ }
357+ } , [ ] ) ;
358+ }
359+
360+ if ( ! variables || typeof variables !== 'object' ) {
361+ return [ ] ;
362+ } else {
363+ return go ( variables , '' ) ;
364+ }
321365}
322366
323367function isEmailAddress ( str ) {
324368 return EMAIL_ADDRESS_REGEX . test ( str ) ;
325369}
326370
371+ function tryOrNull ( fn , exceptionHandler ) {
372+ try {
373+ return fn ( ) ;
374+ } catch ( e ) {
375+ if ( exceptionHandler ) {
376+ exceptionHandler ( e ) ;
377+ }
378+ return null ;
379+ }
380+ }
381+
327382module . exports = {
383+ isSensitiveGraphqlOperation,
328384 formatHeaders,
329385 filterSensitiveHeaders,
330386 checkForStatic,
@@ -343,5 +399,7 @@ module.exports = {
343399 isReqInMonitorMode,
344400 getTokenObject,
345401 getGraphqlData,
346- isEmailAddress
402+ isEmailAddress,
403+ isGraphql,
404+ tryOrNull,
347405} ;
0 commit comments