Skip to content
This repository was archived by the owner on Oct 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 5 additions & 1 deletion lib/pxconfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]) => {
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -359,6 +361,7 @@ function pxDefaultConfig() {
JWT_HEADER_NAME: '',
JWT_HEADER_USER_ID_FIELD_NAME: '',
JWT_HEADER_ADDITIONAL_FIELD_NAMES: [],
CUSTOM_IS_SENSITIVE_REQUEST: ''
};
}

Expand Down Expand Up @@ -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;
21 changes: 20 additions & 1 deletion lib/pxcontext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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') {
Expand Down
2 changes: 1 addition & 1 deletion lib/pxcookie.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 7 additions & 6 deletions lib/pxutil.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
9 changes: 9 additions & 0 deletions test/graphql.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }',
Expand Down
4 changes: 2 additions & 2 deletions test/pxenforcer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
});
});
Expand Down