Skip to content
This repository was archived by the owner on Oct 29, 2025. It is now read-only.

Commit b3ea8f6

Browse files
authored
Merge pull request #201 from PerimeterX/dev
Dev to master
2 parents 834ae06 + ab84b11 commit b3ea8f6

File tree

12 files changed

+133
-34
lines changed

12 files changed

+133
-34
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## [3.1.1] - 2021-12-29
9+
10+
### Added
11+
12+
- server_info_origin to all Enforcer activities, indicates which CDN POP/Datacenter the request hit
13+
- sensitive route based on GraphQL payload
14+
15+
### Fixed
16+
17+
- Enforced route bugfix
18+
819
## [3.1.0] - 2021-11-28
920

1021
### Changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[PerimeterX](http://www.perimeterx.com) Shared base for NodeJS enforcers
77
=============================================================
88

9-
> Latest stable version: [v3.1.0](https://www.npmjs.com/package/perimeterx-node-core)
9+
> Latest stable version: [v3.1.1](https://www.npmjs.com/package/perimeterx-node-core)
1010
1111
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.
1212

lib/enums/GraphqlOperationType.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const GraphqlOperationType = {
2+
QUERY: 'query',
3+
MUTATION: 'mutation'
4+
};
5+
6+
module.exports = {
7+
GraphqlOperationType
8+
};

lib/models/GraphqlData.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class GraphqlData {
2+
constructor(graphqlOperationType, graphqlOperationName) {
3+
this.operationType = graphqlOperationType;
4+
this.operationName = graphqlOperationName;
5+
}
6+
}
7+
8+
module.exports = {
9+
GraphqlData
10+
};

lib/pxapi.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ function buildRequestData(ctx, config) {
6060
},
6161
};
6262

63+
if (ctx.graphqlData) {
64+
data.additional['graphql_operation_type'] = ctx.graphqlData.operationType;
65+
data.additional['graphql_operation_name'] = ctx.graphqlData.operationName;
66+
}
67+
if (ctx.serverInfoRegion) {
68+
data.additional['server_info_region'] = ctx.serverInfoRegion;
69+
}
70+
6371
if (ctx.s2sCallReason === 'cookie_decryption_failed') {
6472
data.additional.px_orig_cookie = ctx.getCookie(); //No need strigify, already a string
6573
}

lib/pxconfig.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ class PxConfig {
7777
['LOGIN_CREDS_EXTRACTION', 'px_login_credentials_extraction'],
7878
['COMPROMISED_CREDENTIALS_HEADER', 'px_compromised_credentials_header'],
7979
['ENABLE_ADDITIONAL_ACTIVITY_HEADER', 'px_additional_activity_header_enabled'],
80+
['SENSITIVE_GRAPHQL_OPERATION_TYPES', 'px_sensitive_graphql_operation_types'],
81+
['SENSITIVE_GRAPHQL_OPERATION_NAMES', 'px_sensitive_graphql_operation_names'],
8082
];
8183

8284
configKeyMapping.forEach(([targetKey, sourceKey]) => {
@@ -316,6 +318,8 @@ function pxDefaultConfig() {
316318
LOGIN_CREDS_EXTRACTION: [],
317319
COMPROMISED_CREDENTIALS_HEADER: DEFAULT_COMPROMISED_CREDENTIALS_HEADER_NAME,
318320
ENABLE_ADDITIONAL_ACTIVITY_HEADER: false,
321+
SENSITIVE_GRAPHQL_OPERATION_TYPES: [],
322+
SENSITIVE_GRAPHQL_OPERATION_NAMES: []
319323
};
320324
}
321325

@@ -364,6 +368,8 @@ const allowedConfigKeys = [
364368
'px_login_credentials_extraction_enabled',
365369
'px_login_credentials_extraction',
366370
'px_compromised_credentials_header',
371+
'px_sensitive_graphql_operation_types',
372+
'px_sensitive_graphql_operation_names'
367373
];
368374

369375
module.exports = PxConfig;

lib/pxcontext.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ class PxContext {
5252
}
5353
});
5454
}
55+
if (this.uri.includes('graphql')) {
56+
config.logger.debug('Graphql route detected');
57+
this.graphqlData = pxUtil.getGraphqlData(req);
58+
this.sensitiveGraphqlOperation = this.isSensitiveGraphqlOperation(config);
59+
}
60+
if (process.env.AWS_REGION) {
61+
this.serverInfoRegion = process.env.AWS_REGION;
62+
}
5563
}
5664

5765
getCookie() {
@@ -69,6 +77,15 @@ class PxContext {
6977
return Array.isArray(routes) ? routes.some((route) => this.verifyRoute(route, uri)) : false;
7078
}
7179

80+
isSensitiveGraphqlOperation(config) {
81+
if (!this.graphqlData) {
82+
return false;
83+
}
84+
const { operationType, operationName } = this.graphqlData;
85+
return config.SENSITIVE_GRAPHQL_OPERATION_TYPES.includes(operationType)
86+
|| config.SENSITIVE_GRAPHQL_OPERATION_NAMES.includes(operationName);
87+
}
88+
7289
verifyRoute(pattern, uri) {
7390
if (pattern instanceof RegExp && uri.match(pattern)) {
7491
return true;

lib/pxcookie.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ function evalCookie(ctx, config) {
8686
return ScoreEvaluateAction.SENSITIVE_ROUTE;
8787
}
8888

89+
if (ctx.sensitiveGraphqlOperation) {
90+
config.logger.debug(`Sensitive graphql operation, sending Risk API. operation type: ${ctx.graphqlData.operationType}, operation name: ${ctx.graphqlData.operationName}`);
91+
ctx.s2sCallReason = 'sensitive_route';
92+
return ScoreEvaluateAction.SENSITIVE_ROUTE;
93+
}
94+
8995
ctx.passReason = PassReason.COOKIE;
9096
config.logger.debug(`Cookie evaluation ended successfully, risk score: ${cookie.getScore()}`);
9197
return ScoreEvaluateAction.GOOD_SCORE;

lib/pxenforcer.js

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,6 @@ class PxEnforcer {
133133
}
134134

135135
shouldFilterRequest(req) {
136-
let shouldFilterRoute = false;
137-
138136
if (pxUtil.checkForStatic(req, this._config.STATIC_FILES_EXT)) {
139137
this.logger.debug(`Found whitelist ext in path: ${req.originalUrl}`);
140138
return true;
@@ -153,39 +151,12 @@ class PxEnforcer {
153151
}
154152
}
155153
}
156-
157-
if (this._config.ENFORCED_ROUTES && this._config.ENFORCED_ROUTES.length > 0) {
158-
for (const enforceRoute of this._config.ENFORCED_ROUTES) {
159-
if (enforceRoute instanceof RegExp && req.originalUrl.match(enforceRoute)) {
160-
return false;
161-
}
162-
if (typeof enforceRoute === 'string' && req.originalUrl.startsWith(enforceRoute)) {
163-
return false;
164-
}
165-
}
166-
this.logger.debug(`Route ${req.originalUrl} is not listed in specific routes to enforce`);
167-
shouldFilterRoute = true;
168-
}
169-
170-
if (this._config.MONITORED_ROUTES && this._config.MONITORED_ROUTES.length > 0) {
171-
for (const monitorRoute of this._config.MONITORED_ROUTES) {
172-
if (monitorRoute instanceof RegExp && req.originalUrl.match(monitorRoute)) {
173-
return false;
174-
}
175-
if (typeof monitorRoute === 'string' && req.originalUrl.startsWith(monitorRoute)) {
176-
return false;
177-
}
178-
}
179-
this.logger.debug(`Route ${req.path} is not listed in specific routes to monitor`);
180-
}
181-
182-
return shouldFilterRoute;
154+
155+
return false;
183156
}
184157

185158
verifyUserScore(ctx, callback) {
186-
const startRiskRtt = Date.now();
187159
ctx.riskRtt = 0;
188-
189160
try {
190161
if (!ctx.ip || !ctx.uri) {
191162
this.logger.error('perimeterx score evaluation failed. bad parameters.');
@@ -203,6 +174,7 @@ class PxEnforcer {
203174
ctx.blockReason = 'cookie_high_score';
204175
return callback(ScoreEvaluateAction.COOKIE_BLOCK_TRAFFIC);
205176
}
177+
const startRiskRtt = Date.now();
206178

207179
/* when no fallback to s2s call if cookie does not exist or failed on evaluation */
208180
pxApi.evalByServerCall(ctx, this._config, (action) => {
@@ -384,6 +356,9 @@ class PxEnforcer {
384356
pass_reason: ctx.passReason,
385357
...ctx.additionalFields,
386358
};
359+
if (ctx.serverInfoRegion) {
360+
details['server_info_region'] = ctx.serverInfoRegion;
361+
}
387362

388363
if (ctx.passReason === PassReason.S2S_ERROR && ctx.s2sErrorInfo) {
389364
this.setS2SErrorInfo(details, ctx.s2sErrorInfo);
@@ -432,6 +407,9 @@ class PxEnforcer {
432407
simulated_block: pxUtil.isReqInMonitorMode(this._config, ctx),
433408
...ctx.additionalFields,
434409
};
410+
if (ctx.serverInfoRegion) {
411+
details['server_info_region'] = ctx.serverInfoRegion;
412+
}
435413

436414
this.logger.debug(`Sending block activity`);
437415
this.pxClient.sendToPerimeterX(ActivityType.BLOCK, details, ctx, this._config);

lib/pxutil.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const fs = require('fs');
55
const crypto = require('crypto');
66

77
const { ModuleMode } = require('./enums/ModuleMode');
8+
const { GraphqlOperationType } = require('./enums/GraphqlOperationType');
9+
const { GraphqlData } = require('./models/GraphqlData');
810

911
/**
1012
* PerimeterX (http://www.perimeterx.com) NodeJS-Express SDK
@@ -247,7 +249,7 @@ function generateHMAC(cookieSecret, payload) {
247249

248250
function isReqInMonitorMode(pxConfig, pxCtx) {
249251
return (
250-
(pxConfig.MODULE_MODE === ModuleMode.MONITOR && !pxCtx.shouldBypassMonitor) || pxCtx.monitoredRoute
252+
(pxConfig.MODULE_MODE === ModuleMode.MONITOR && !pxCtx.shouldBypassMonitor && !pxCtx.enforcedRoute) || pxCtx.monitoredRoute
251253
);
252254
}
253255

@@ -264,6 +266,45 @@ function getTokenObject(cookie, delimiter = ':') {
264266
return { key: '_px3', value: cookie };
265267
}
266268

269+
function getGraphqlData(req) {
270+
const isGraphqlPath = req.path.includes('graphql') || req.originalUrl.includes('graphql');
271+
if (!isGraphqlPath) {
272+
return null;
273+
}
274+
275+
const query = req.body ? req.body['query'] : null;
276+
if (!query) {
277+
return new GraphqlData(GraphqlOperationType.QUERY);
278+
}
279+
280+
const operationType = extractGraphqlOperationType(query);
281+
const operationName = extractGraphqlOperationName(query, req.body['operationName'])
282+
return new GraphqlData(operationType, operationName);
283+
}
284+
285+
function extractGraphqlOperationType(query) {
286+
const isGraphqlQueryShorthand = query[0] === '{';
287+
if (isGraphqlQueryShorthand) {
288+
return GraphqlOperationType.QUERY;
289+
}
290+
291+
const queryArray = query.split(/[^A-Za-z0-9_]/);
292+
return isValidGraphqlOperationType(queryArray[0]) ? queryArray[0] : GraphqlOperationType.QUERY;
293+
}
294+
295+
function extractGraphqlOperationName(query, operationName) {
296+
if (operationName) {
297+
return operationName;
298+
}
299+
300+
const queryArray = query.split(/[^A-Za-z0-9_]/);
301+
return isValidGraphqlOperationType(queryArray[0]) ? queryArray[1] : queryArray[0];
302+
}
303+
304+
function isValidGraphqlOperationType(operationType) {
305+
return Object.values(GraphqlOperationType).includes(operationType);
306+
}
307+
267308
module.exports = {
268309
formatHeaders,
269310
filterSensitiveHeaders,
@@ -282,4 +323,5 @@ module.exports = {
282323
generateHMAC,
283324
isReqInMonitorMode,
284325
getTokenObject,
326+
getGraphqlData
285327
};

0 commit comments

Comments
 (0)