Skip to content

Commit

Permalink
Merge pull request #272 from PerimeterX/release/v3.9.0
Browse files Browse the repository at this point in the history
Release/v3.9.0 to master
  • Loading branch information
chen-zimmer-px authored Jan 29, 2023
2 parents 798bd37 + 1f04629 commit 1f84b95
Show file tree
Hide file tree
Showing 12 changed files with 386 additions and 72 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ 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.9.0] - 2023-01-29

### Added
- Support for CORS preflight requests and CORS headers in block responses

## [3.8.0] - 2023-01-25

### 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.8.0](https://www.npmjs.com/package/perimeterx-node-core)
> Latest stable version: [v3.9.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
16 changes: 15 additions & 1 deletion lib/pxconfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ class PxConfig {
['LOGIN_SUCCESSFUL_BODY_REGEX', 'px_login_successful_body_regex'],
['LOGIN_SUCCESSFUL_CUSTOM_CALLBACK', 'px_login_successful_custom_callback'],
['MODIFY_CONTEXT', 'px_modify_context'],
['CORS_SUPPORT_ENABLED', 'px_cors_support_enabled'],
['CORS_CREATE_CUSTOM_BLOCK_RESPONSE_HEADERS', 'px_cors_create_custom_block_response_headers'],
['CORS_CUSTOM_PREFLIGHT_HANDLER', 'px_cors_custom_preflight_handler'],
['CORS_PREFLIGHT_REQUEST_FILTER_ENABLED', 'px_cors_preflight_request_filter_enabled'],
['JWT_COOKIE_NAME', 'px_jwt_cookie_name'],
['JWT_COOKIE_USER_ID_FIELD_NAME', 'px_jwt_cookie_user_id_field_name'],
['JWT_COOKIE_ADDITIONAL_FIELD_NAMES', 'px_jwt_cookie_additional_field_names'],
Expand Down Expand Up @@ -170,7 +174,9 @@ class PxConfig {
userInput === 'px_custom_request_handler' ||
userInput === 'px_enrich_custom_parameters' ||
userInput === 'px_login_successful_custom_callback' ||
userInput === 'px_modify_context'
userInput === 'px_modify_context' ||
userInput === 'px_cors_create_custom_block_response_headers' ||
userInput === 'px_cors_custom_preflight_handler'
) {
if (typeof params[userInput] === 'function') {
return params[userInput];
Expand Down Expand Up @@ -343,6 +349,10 @@ function pxDefaultConfig() {
LOGIN_SUCCESSFUL_CUSTOM_CALLBACK: null,
MODIFY_CONTEXT: null,
GRAPHQL_ROUTES: ['^/graphql$'],
CORS_CUSTOM_PREFLIGHT_HANDLER: null,
CORS_CREATE_CUSTOM_BLOCK_RESPONSE_HEADERS: null,
CORS_PREFLIGHT_REQUEST_FILTER_ENABLED: false,
CORS_SUPPORT_ENABLED: false,
JWT_COOKIE_NAME: '',
JWT_COOKIE_USER_ID_FIELD_NAME: '',
JWT_COOKIE_ADDITIONAL_FIELD_NAMES: [],
Expand Down Expand Up @@ -411,6 +421,10 @@ const allowedConfigKeys = [
'px_login_successful_custom_callback',
'px_modify_context',
'px_graphql_routes',
'px_cors_support_enabled',
'px_cors_preflight_request_filter_enabled',
'px_cors_create_custom_block_response_headers',
'px_cors_custom_preflight_handler',
'px_jwt_cookie_name',
'px_jwt_cookie_user_id_field_name',
'px_jwt_cookie_additional_field_names',
Expand Down
55 changes: 55 additions & 0 deletions lib/pxcors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const { ORIGIN_HEADER, ACCESS_CONTROL_REQUEST_METHOD_HEADER } = require('./utils/constants');

function isPreflightRequest(request) {
return request.method.toUpperCase() === 'OPTIONS' && request.get(ORIGIN_HEADER) && request.get(ACCESS_CONTROL_REQUEST_METHOD_HEADER);
}

function runPreflightCustomHandler(pxConfig, request) {
const corsCustomPreflightFunction = pxConfig.CORS_CUSTOM_PREFLIGHT_HANDLER;

if (corsCustomPreflightFunction) {
try {
return corsCustomPreflightFunction(request);
} catch (e) {
pxConfig.logger.debug(`Error while executing custom preflight handler: ${e}`);
}
}

return null;
}

function isCorsRequest(request) {
return request.get(ORIGIN_HEADER);
}

function getCorsBlockHeaders(request, pxConfig, pxCtx) {
let corsHeaders = getDefaultCorsHeaders(request);
const createCustomCorsHeaders = pxConfig.CORS_CREATE_CUSTOM_BLOCK_RESPONSE_HEADERS;

if (createCustomCorsHeaders) {
try {
corsHeaders = createCustomCorsHeaders(pxCtx, request);
} catch (e) {
pxConfig.logger.debug(`Caught error in px_cors_create_custom_block_response_headers custom function: ${e}`);
}
}

return corsHeaders;
}

function getDefaultCorsHeaders(request) {
const originHeader = request.get(ORIGIN_HEADER);

if (!originHeader) {
return {};
}

return { 'Access-Control-Allow-Origin': originHeader, 'Access-Control-Allow-Credentials': 'true' };
}

module.exports = {
isPreflightRequest,
runPreflightCustomHandler,
isCorsRequest,
getCorsBlockHeaders,
};
35 changes: 30 additions & 5 deletions lib/pxenforcer.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ const PxDataEnrichment = require('./pxdataenrichment');
const telemetryHandler = require('./telemetry_handler.js');
const LoginCredentialsExtractor = require('./extract_field/LoginCredentialsExtractor');
const { LoginSuccessfulParserFactory } = require('./extract_field/login_successful/LoginSuccessfulParserFactory');
const { CI_RAW_USERNAME_FIELD, CI_VERSION_FIELD, CI_SSO_STEP_FIELD, CI_CREDENTIALS_COMPROMISED_FIELD } = require('./utils/constants');
const {
CI_RAW_USERNAME_FIELD,
CI_VERSION_FIELD,
CI_SSO_STEP_FIELD,
CI_CREDENTIALS_COMPROMISED_FIELD,
} = require('./utils/constants');
const pxCors = require('./pxcors');

class PxEnforcer {
constructor(params, client) {
Expand Down Expand Up @@ -82,6 +88,18 @@ class PxEnforcer {
return cb();
}

if (this._config.CORS_SUPPORT_ENABLED && pxCors.isPreflightRequest(req)) {
const response = pxCors.runPreflightCustomHandler(this._config, req);
if (response) {
return cb(null, response);
}

if (this._config.CORS_PREFLIGHT_REQUEST_FILTER_ENABLED) {
this.logger.debug('Skipping verification due to preflight request');
return cb();
}
}

if (userAgent && this._config.FILTER_BY_USERAGENT && this._config.FILTER_BY_USERAGENT.length > 0) {
for (const ua of this._config.FILTER_BY_USERAGENT) {
if (pxUtil.isStringMatchWith(userAgent, ua)) {
Expand Down Expand Up @@ -327,19 +345,25 @@ class PxEnforcer {
}`,
);
const config = this._config;
this.generateResponse(ctx, isJsonResponse, function (responseObject) {

this.generateResponse(ctx, isJsonResponse, function(responseObject) {
const response = {
status: '403',
statusDescription: 'Forbidden',
};

if (pxCors.isCorsRequest(req) && config.CORS_SUPPORT_ENABLED) {
response.headers = pxCors.getCorsBlockHeaders(req, config, ctx);
}

if (ctx.blockAction === 'r') {
response.status = '429';
response.statusDescription = 'Too Many Requests';
}

if (isJsonResponse) {
response.header = { key: 'Content-Type', value: 'application/json' };
pxUtil.appendContentType(response, 'application/json');

response.body = {
appId: responseObject.appId,
jsClientSrc: responseObject.jsClientSrc,
Expand All @@ -353,12 +377,13 @@ class PxEnforcer {
};
return cb(null, response);
}

pxUtil.appendContentType(response, 'text/html');

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

if (ctx.cookieOrigin === CookieOrigin.HEADER) {
response.header = { key: 'Content-Type', value: 'application/json' };
pxUtil.appendContentType(response, 'application/json');
response.body = {
action: pxUtil.parseAction(ctx.blockAction),
uuid: ctx.uuid,
Expand Down
5 changes: 5 additions & 0 deletions lib/pxutil.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,10 @@ function tryOrNull(fn, exceptionHandler) {
}
}

function appendContentType(response, contentTypeValue) {
response.headers = Object.assign(response.headers || {}, { 'Content-Type': contentTypeValue });
}

module.exports = {
isSensitiveGraphqlOperation,
formatHeaders,
Expand All @@ -402,4 +406,5 @@ module.exports = {
isEmailAddress,
isGraphql,
tryOrNull,
appendContentType
};
4 changes: 4 additions & 0 deletions lib/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const EMAIL_ADDRESS_REGEX =
/^([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])?)$/;
const HASH_ALGORITHM = { SHA256: 'sha256' };

const ORIGIN_HEADER = 'origin';
const ACCESS_CONTROL_REQUEST_METHOD_HEADER = 'access-control-request-method';
const TOKEN_SEPARATOR = '.';
const APP_USER_ID_FIELD_NAME = 'app_user_id';
const JWT_ADDITIONAL_FIELDS_FIELD_NAME = 'jwt_additional_fields';
Expand Down Expand Up @@ -56,6 +58,8 @@ module.exports = {
GQL_OPERATIONS_FIELD,
EMAIL_ADDRESS_REGEX,
HASH_ALGORITHM,
ORIGIN_HEADER,
ACCESS_CONTROL_REQUEST_METHOD_HEADER,
TOKEN_SEPARATOR,
APP_USER_ID_FIELD_NAME,
JWT_ADDITIONAL_FIELDS_FIELD_NAME,
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.8.0",
"version": "3.9.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
Loading

0 comments on commit 1f84b95

Please sign in to comment.