Skip to content

Commit 1f84b95

Browse files
Merge pull request #272 from PerimeterX/release/v3.9.0
Release/v3.9.0 to master
2 parents 798bd37 + 1f04629 commit 1f84b95

File tree

12 files changed

+386
-72
lines changed

12 files changed

+386
-72
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ 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.9.0] - 2023-01-29
9+
10+
### Added
11+
- Support for CORS preflight requests and CORS headers in block responses
12+
813
## [3.8.0] - 2023-01-25
914

1015
### Added

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.8.0](https://www.npmjs.com/package/perimeterx-node-core)
9+
> Latest stable version: [v3.9.0](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/pxconfig.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ class PxConfig {
9292
['LOGIN_SUCCESSFUL_BODY_REGEX', 'px_login_successful_body_regex'],
9393
['LOGIN_SUCCESSFUL_CUSTOM_CALLBACK', 'px_login_successful_custom_callback'],
9494
['MODIFY_CONTEXT', 'px_modify_context'],
95+
['CORS_SUPPORT_ENABLED', 'px_cors_support_enabled'],
96+
['CORS_CREATE_CUSTOM_BLOCK_RESPONSE_HEADERS', 'px_cors_create_custom_block_response_headers'],
97+
['CORS_CUSTOM_PREFLIGHT_HANDLER', 'px_cors_custom_preflight_handler'],
98+
['CORS_PREFLIGHT_REQUEST_FILTER_ENABLED', 'px_cors_preflight_request_filter_enabled'],
9599
['JWT_COOKIE_NAME', 'px_jwt_cookie_name'],
96100
['JWT_COOKIE_USER_ID_FIELD_NAME', 'px_jwt_cookie_user_id_field_name'],
97101
['JWT_COOKIE_ADDITIONAL_FIELD_NAMES', 'px_jwt_cookie_additional_field_names'],
@@ -170,7 +174,9 @@ class PxConfig {
170174
userInput === 'px_custom_request_handler' ||
171175
userInput === 'px_enrich_custom_parameters' ||
172176
userInput === 'px_login_successful_custom_callback' ||
173-
userInput === 'px_modify_context'
177+
userInput === 'px_modify_context' ||
178+
userInput === 'px_cors_create_custom_block_response_headers' ||
179+
userInput === 'px_cors_custom_preflight_handler'
174180
) {
175181
if (typeof params[userInput] === 'function') {
176182
return params[userInput];
@@ -343,6 +349,10 @@ function pxDefaultConfig() {
343349
LOGIN_SUCCESSFUL_CUSTOM_CALLBACK: null,
344350
MODIFY_CONTEXT: null,
345351
GRAPHQL_ROUTES: ['^/graphql$'],
352+
CORS_CUSTOM_PREFLIGHT_HANDLER: null,
353+
CORS_CREATE_CUSTOM_BLOCK_RESPONSE_HEADERS: null,
354+
CORS_PREFLIGHT_REQUEST_FILTER_ENABLED: false,
355+
CORS_SUPPORT_ENABLED: false,
346356
JWT_COOKIE_NAME: '',
347357
JWT_COOKIE_USER_ID_FIELD_NAME: '',
348358
JWT_COOKIE_ADDITIONAL_FIELD_NAMES: [],
@@ -411,6 +421,10 @@ const allowedConfigKeys = [
411421
'px_login_successful_custom_callback',
412422
'px_modify_context',
413423
'px_graphql_routes',
424+
'px_cors_support_enabled',
425+
'px_cors_preflight_request_filter_enabled',
426+
'px_cors_create_custom_block_response_headers',
427+
'px_cors_custom_preflight_handler',
414428
'px_jwt_cookie_name',
415429
'px_jwt_cookie_user_id_field_name',
416430
'px_jwt_cookie_additional_field_names',

lib/pxcors.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const { ORIGIN_HEADER, ACCESS_CONTROL_REQUEST_METHOD_HEADER } = require('./utils/constants');
2+
3+
function isPreflightRequest(request) {
4+
return request.method.toUpperCase() === 'OPTIONS' && request.get(ORIGIN_HEADER) && request.get(ACCESS_CONTROL_REQUEST_METHOD_HEADER);
5+
}
6+
7+
function runPreflightCustomHandler(pxConfig, request) {
8+
const corsCustomPreflightFunction = pxConfig.CORS_CUSTOM_PREFLIGHT_HANDLER;
9+
10+
if (corsCustomPreflightFunction) {
11+
try {
12+
return corsCustomPreflightFunction(request);
13+
} catch (e) {
14+
pxConfig.logger.debug(`Error while executing custom preflight handler: ${e}`);
15+
}
16+
}
17+
18+
return null;
19+
}
20+
21+
function isCorsRequest(request) {
22+
return request.get(ORIGIN_HEADER);
23+
}
24+
25+
function getCorsBlockHeaders(request, pxConfig, pxCtx) {
26+
let corsHeaders = getDefaultCorsHeaders(request);
27+
const createCustomCorsHeaders = pxConfig.CORS_CREATE_CUSTOM_BLOCK_RESPONSE_HEADERS;
28+
29+
if (createCustomCorsHeaders) {
30+
try {
31+
corsHeaders = createCustomCorsHeaders(pxCtx, request);
32+
} catch (e) {
33+
pxConfig.logger.debug(`Caught error in px_cors_create_custom_block_response_headers custom function: ${e}`);
34+
}
35+
}
36+
37+
return corsHeaders;
38+
}
39+
40+
function getDefaultCorsHeaders(request) {
41+
const originHeader = request.get(ORIGIN_HEADER);
42+
43+
if (!originHeader) {
44+
return {};
45+
}
46+
47+
return { 'Access-Control-Allow-Origin': originHeader, 'Access-Control-Allow-Credentials': 'true' };
48+
}
49+
50+
module.exports = {
51+
isPreflightRequest,
52+
runPreflightCustomHandler,
53+
isCorsRequest,
54+
getCorsBlockHeaders,
55+
};

lib/pxenforcer.js

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ const PxDataEnrichment = require('./pxdataenrichment');
2323
const telemetryHandler = require('./telemetry_handler.js');
2424
const LoginCredentialsExtractor = require('./extract_field/LoginCredentialsExtractor');
2525
const { LoginSuccessfulParserFactory } = require('./extract_field/login_successful/LoginSuccessfulParserFactory');
26-
const { CI_RAW_USERNAME_FIELD, CI_VERSION_FIELD, CI_SSO_STEP_FIELD, CI_CREDENTIALS_COMPROMISED_FIELD } = require('./utils/constants');
26+
const {
27+
CI_RAW_USERNAME_FIELD,
28+
CI_VERSION_FIELD,
29+
CI_SSO_STEP_FIELD,
30+
CI_CREDENTIALS_COMPROMISED_FIELD,
31+
} = require('./utils/constants');
32+
const pxCors = require('./pxcors');
2733

2834
class PxEnforcer {
2935
constructor(params, client) {
@@ -82,6 +88,18 @@ class PxEnforcer {
8288
return cb();
8389
}
8490

91+
if (this._config.CORS_SUPPORT_ENABLED && pxCors.isPreflightRequest(req)) {
92+
const response = pxCors.runPreflightCustomHandler(this._config, req);
93+
if (response) {
94+
return cb(null, response);
95+
}
96+
97+
if (this._config.CORS_PREFLIGHT_REQUEST_FILTER_ENABLED) {
98+
this.logger.debug('Skipping verification due to preflight request');
99+
return cb();
100+
}
101+
}
102+
85103
if (userAgent && this._config.FILTER_BY_USERAGENT && this._config.FILTER_BY_USERAGENT.length > 0) {
86104
for (const ua of this._config.FILTER_BY_USERAGENT) {
87105
if (pxUtil.isStringMatchWith(userAgent, ua)) {
@@ -327,19 +345,25 @@ class PxEnforcer {
327345
}`,
328346
);
329347
const config = this._config;
330-
this.generateResponse(ctx, isJsonResponse, function (responseObject) {
348+
349+
this.generateResponse(ctx, isJsonResponse, function(responseObject) {
331350
const response = {
332351
status: '403',
333352
statusDescription: 'Forbidden',
334353
};
335354

355+
if (pxCors.isCorsRequest(req) && config.CORS_SUPPORT_ENABLED) {
356+
response.headers = pxCors.getCorsBlockHeaders(req, config, ctx);
357+
}
358+
336359
if (ctx.blockAction === 'r') {
337360
response.status = '429';
338361
response.statusDescription = 'Too Many Requests';
339362
}
340363

341364
if (isJsonResponse) {
342-
response.header = { key: 'Content-Type', value: 'application/json' };
365+
pxUtil.appendContentType(response, 'application/json');
366+
343367
response.body = {
344368
appId: responseObject.appId,
345369
jsClientSrc: responseObject.jsClientSrc,
@@ -353,12 +377,13 @@ class PxEnforcer {
353377
};
354378
return cb(null, response);
355379
}
380+
381+
pxUtil.appendContentType(response, 'text/html');
356382

357-
response.header = { key: 'Content-Type', value: 'text/html' };
358383
response.body = responseObject;
359384

360385
if (ctx.cookieOrigin === CookieOrigin.HEADER) {
361-
response.header = { key: 'Content-Type', value: 'application/json' };
386+
pxUtil.appendContentType(response, 'application/json');
362387
response.body = {
363388
action: pxUtil.parseAction(ctx.blockAction),
364389
uuid: ctx.uuid,

lib/pxutil.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,10 @@ function tryOrNull(fn, exceptionHandler) {
379379
}
380380
}
381381

382+
function appendContentType(response, contentTypeValue) {
383+
response.headers = Object.assign(response.headers || {}, { 'Content-Type': contentTypeValue });
384+
}
385+
382386
module.exports = {
383387
isSensitiveGraphqlOperation,
384388
formatHeaders,
@@ -402,4 +406,5 @@ module.exports = {
402406
isEmailAddress,
403407
isGraphql,
404408
tryOrNull,
409+
appendContentType
405410
};

lib/utils/constants.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ const EMAIL_ADDRESS_REGEX =
2828
/^([a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)$/;
2929
const HASH_ALGORITHM = { SHA256: 'sha256' };
3030

31+
const ORIGIN_HEADER = 'origin';
32+
const ACCESS_CONTROL_REQUEST_METHOD_HEADER = 'access-control-request-method';
3133
const TOKEN_SEPARATOR = '.';
3234
const APP_USER_ID_FIELD_NAME = 'app_user_id';
3335
const JWT_ADDITIONAL_FIELDS_FIELD_NAME = 'jwt_additional_fields';
@@ -56,6 +58,8 @@ module.exports = {
5658
GQL_OPERATIONS_FIELD,
5759
EMAIL_ADDRESS_REGEX,
5860
HASH_ALGORITHM,
61+
ORIGIN_HEADER,
62+
ACCESS_CONTROL_REQUEST_METHOD_HEADER,
5963
TOKEN_SEPARATOR,
6064
APP_USER_ID_FIELD_NAME,
6165
JWT_ADDITIONAL_FIELDS_FIELD_NAME,

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "perimeterx-node-core",
3-
"version": "3.8.0",
3+
"version": "3.9.0",
44
"description": "PerimeterX NodeJS shared core for various applications to monitor and block traffic according to PerimeterX risk score",
55
"main": "index.js",
66
"scripts": {

0 commit comments

Comments
 (0)