diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bfa920..e7b8453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [3.10.0] - 2023-03-28 + +### Added +- Support for handling graphQL requests with empty query field +- Support custom is sensitive request via function + ## [3.9.0] - 2023-01-29 ### Added diff --git a/README.md b/README.md index 0c2d0ea..ab602ed 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [PerimeterX](http://www.perimeterx.com) Shared base for NodeJS enforcers ============================================================= -> Latest stable version: [v3.9.0](https://www.npmjs.com/package/perimeterx-node-core) +> Latest stable version: [v3.10.0](https://www.npmjs.com/package/perimeterx-node-core) This is a shared base implementation for PerimeterX Express enforcer and future NodeJS enforcers. For a fully functioning implementation example, see the [Node-Express enforcer](https://github.com/PerimeterX/perimeterx-node-express/) implementation. diff --git a/lib/pxconfig.js b/lib/pxconfig.js index feab4d3..27807ce 100644 --- a/lib/pxconfig.js +++ b/lib/pxconfig.js @@ -102,6 +102,7 @@ class PxConfig { ['JWT_HEADER_NAME', 'px_jwt_header_name'], ['JWT_HEADER_USER_ID_FIELD_NAME', 'px_jwt_header_user_id_field_name'], ['JWT_HEADER_ADDITIONAL_FIELD_NAMES', 'px_jwt_header_additional_field_names'], + ['CUSTOM_IS_SENSITIVE_REQUEST', 'px_custom_is_sensitive_request'] ]; configKeyMapping.forEach(([targetKey, sourceKey]) => { @@ -176,7 +177,8 @@ class PxConfig { userInput === 'px_login_successful_custom_callback' || userInput === 'px_modify_context' || userInput === 'px_cors_create_custom_block_response_headers' || - userInput === 'px_cors_custom_preflight_handler' + userInput === 'px_cors_custom_preflight_handler' || + userInput === 'px_custom_is_sensitive_request' ) { if (typeof params[userInput] === 'function') { return params[userInput]; @@ -359,6 +361,7 @@ function pxDefaultConfig() { JWT_HEADER_NAME: '', JWT_HEADER_USER_ID_FIELD_NAME: '', JWT_HEADER_ADDITIONAL_FIELD_NAMES: [], + CUSTOM_IS_SENSITIVE_REQUEST: '' }; } @@ -431,6 +434,7 @@ const allowedConfigKeys = [ 'px_jwt_header_name', 'px_jwt_header_user_id_field_name', 'px_jwt_header_additional_field_names', + 'px_custom_is_sensitive_request' ]; module.exports = PxConfig; diff --git a/lib/pxcontext.js b/lib/pxcontext.js index 672e866..9f349f7 100644 --- a/lib/pxcontext.js +++ b/lib/pxcontext.js @@ -23,7 +23,7 @@ class PxContext { this.originalRequest = req.originalRequest || req; this.httpVersion = req.httpVersion || ''; this.httpMethod = req.method || ''; - this.sensitiveRoute = this.isSpecialRoute(config.SENSITIVE_ROUTES, this.uri); + this.sensitiveRequest = () => this.isSensitiveRequest(req, config); this.enforcedRoute = this.isSpecialRoute(config.ENFORCED_ROUTES, this.uri); this.whitelistRoute = this.isSpecialRoute(config.WHITELIST_ROUTES, this.uri); this.monitoredRoute = !this.enforcedRoute && this.isSpecialRoute(config.MONITORED_ROUTES, this.uri); @@ -85,6 +85,25 @@ class PxContext { } } + isSensitiveRequest(request, config) { + return this.isSpecialRoute(config.SENSITIVE_ROUTES, this.uri) || + this.isCustomSensitiveRequest(request, config); + } + + isCustomSensitiveRequest(request, config) { + const customIsSensitiveRequest = config.CUSTOM_IS_SENSITIVE_REQUEST; + try { + if (customIsSensitiveRequest && customIsSensitiveRequest(request)) { + config.logger.debug('Custom sensitive request matched'); + return true; + } + } catch (err) { + config.logger.debug(`Caught exception on custom sensitive request function: ${err}`); + } + + return false; + } + getGraphqlDataFromBody(body) { let jsonBody = null; if (typeof body === 'string') { diff --git a/lib/pxcookie.js b/lib/pxcookie.js index 8fc9cdc..fa69cde 100644 --- a/lib/pxcookie.js +++ b/lib/pxcookie.js @@ -81,7 +81,7 @@ function evalCookie(ctx, config) { return ScoreEvaluateAction.COOKIE_INVALID; } - if (ctx.sensitiveRoute) { + if (ctx.sensitiveRoute()) { config.logger.debug(`Sensitive route match, sending Risk API. path: ${ctx.uri}`); ctx.s2sCallReason = 'sensitive_route'; return ScoreEvaluateAction.SENSITIVE_ROUTE; diff --git a/lib/pxutil.js b/lib/pxutil.js index 204010a..300e71f 100644 --- a/lib/pxutil.js +++ b/lib/pxutil.js @@ -288,6 +288,10 @@ function isGraphql(req, config) { // query: string (not null) // output: Record [ OperationName -> OperationType ] function parseGraphqlBody(query) { + if (!query) { + return null; + } + const pattern = /\s*(query|mutation|subscription)\s+(\w+)/gm; let match; const ret = {}; @@ -322,25 +326,22 @@ function isSensitiveGraphqlOperation(graphqlData, config) { // graphqlBodyObject: {query: string?, operationName: string?, variables: any[]?} // output: GraphqlData? function getGraphqlData(graphqlBodyObject) { - if (!graphqlBodyObject || !graphqlBodyObject.query) { + if (!graphqlBodyObject) { return null; } const parsedData = parseGraphqlBody(graphqlBodyObject.query); - if (!parsedData) { - return null; - } const selectedOperationName = graphqlBodyObject['operationName'] || (Object.keys(parsedData).length === 1 && Object.keys(parsedData)[0]); - if (!selectedOperationName || !parsedData[selectedOperationName]) { + if (!selectedOperationName || (parsedData && !parsedData[selectedOperationName])) { return null; } const variables = extractVariables(graphqlBodyObject.variables); - return new GraphqlData(parsedData[selectedOperationName], selectedOperationName, variables); + return new GraphqlData(parsedData && parsedData[selectedOperationName], selectedOperationName, variables); } // input: object representing variables diff --git a/package-lock.json b/package-lock.json index b300bf2..7945277 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "perimeterx-node-core", - "version": "3.9.0", + "version": "3.10.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "perimeterx-node-core", - "version": "3.9.0", + "version": "3.10.0", "license": "ISC", "dependencies": { "agent-phin": "^1.0.4", diff --git a/package.json b/package.json index 20f15fa..1cf40f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "perimeterx-node-core", - "version": "3.9.0", + "version": "3.10.0", "description": "PerimeterX NodeJS shared core for various applications to monitor and block traffic according to PerimeterX risk score", "main": "index.js", "scripts": { diff --git a/test/graphql.test.js b/test/graphql.test.js index 167c7d3..be14bd0 100644 --- a/test/graphql.test.js +++ b/test/graphql.test.js @@ -30,6 +30,15 @@ describe('Graphql Testing', () => { graphqlData.type.should.be.exactly('query'); }); + it('should extract operation name if !query', () => { + const gqlObj = { + operationName: 'q1', + }; + + const graphqlData = pxutil.getGraphqlData(gqlObj); + graphqlData.name.should.be.exactly('q1'); + }); + it('extract with many queries', () => { const gqlObj = { query: 'query q1 { \n abc \n }\nmutation q2 {\n def\n }', diff --git a/test/pxenforcer.test.js b/test/pxenforcer.test.js index ee308cf..10cea39 100644 --- a/test/pxenforcer.test.js +++ b/test/pxenforcer.test.js @@ -847,7 +847,7 @@ describe('PX Enforcer - pxenforcer.js', () => { return callback ? callback(null, data) : ''; }); - const modifyCtx = sinon.stub().callsFake((ctx) => ctx.sensitiveRoute = true); + const modifyCtx = sinon.stub().callsFake((ctx) => ctx.sensitiveRequest = true); const curParams = { ...params, px_modify_context: modifyCtx, @@ -857,7 +857,7 @@ describe('PX Enforcer - pxenforcer.js', () => { enforcer = new pxenforcer(curParams, pxClient); enforcer.enforce(req, null, () => { (modifyCtx.calledOnce).should.equal(true); - (req.locals.pxCtx.sensitiveRoute).should.equal(true); + (req.locals.pxCtx.sensitiveRequest).should.equal(true); done(); }); });