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

Commit 834ae06

Browse files
authored
Merge pull request #190 from PerimeterX/dev
Dev
2 parents 33b25a8 + 751b947 commit 834ae06

File tree

8 files changed

+78
-31
lines changed

8 files changed

+78
-31
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ 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.0] - 2021-11-28
9+
10+
### Changed
11+
12+
- Login credentials extraction sends hashed credentials (`creds`) instead of `pass`
13+
- Login credentials extraction normalizes username field to lowercase prior to hashing
14+
- Login credentials extraction fields align with spec (all `snake_case`, not `camelCase`)
15+
- Login credentials extraction handles body encoding based on `Content-Type` request header
16+
17+
### Added
18+
19+
- Login credentials extraction paths are added as sensitive routes automatically
20+
- Added `raw_username` field with default value `null` to `additional_s2s` activity
21+
22+
823
## [3.0.3] - 2021-11-24
924

1025
### 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.0.3](https://www.npmjs.com/package/perimeterx-node-core)
9+
> Latest stable version: [v3.1.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/extract_field/FieldExtractor.js

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
const querystring = require('querystring');
2-
const { sha256, accessNestedObjectValueByStringPath } = require('../pxutil');
2+
const { accessNestedObjectValueByStringPath } = require('../pxutil');
33

4-
const SENT_THROUGH = { BODY: 'body', QUERY_PARAM: 'query-param', HEADER: 'header' };
5-
const ENCODING_TYPE = { URL_ENCODE: 'url-encode', CLEAR_TEXT: 'clear-text', BASE64: 'base64', CUSTOM: 'custom' };
4+
const SentThrough = { BODY: 'body', QUERY_PARAM: 'query-param', HEADER: 'header' };
5+
const ContentType = { JSON: 'application/json', URL_ENCODED: 'application/x-www-form-urlencoded', MULTIPART_FORM: 'multipart/form-data' };
66

77
class FieldExtractor {
8-
constructor(sentThrough, contentType, encoding, fieldsToExtract) {
8+
constructor(sentThrough, fieldsToExtract) {
99
this.containerName = this._getContainerName(sentThrough);
10-
this.contentType = contentType;
11-
this._decode = this._getDecodeFunction(encoding);
1210
this.fieldsToExtract = fieldsToExtract instanceof Array ? fieldsToExtract : [fieldsToExtract];
1311
}
1412

@@ -19,37 +17,32 @@ class FieldExtractor {
1917
}
2018
const fields = {};
2119
this.fieldsToExtract.forEach((desiredField) => {
22-
const value = accessNestedObjectValueByStringPath(desiredField.origRequestFieldName, container);
20+
let value = accessNestedObjectValueByStringPath(desiredField.oldFieldName, container) || container[desiredField.oldFieldName];
2321
if (!value || typeof value !== 'string') {
2422
return;
2523
}
26-
fields[desiredField.resultActivityFieldName] = sha256(value);
24+
value = desiredField.shouldNormalize ? this._normalizeField(value) : value;
25+
fields[desiredField.newFieldName] = value;
2726
});
2827
return fields;
2928
}
3029

30+
_normalizeField(fieldValue) {
31+
return fieldValue.toLowerCase();
32+
}
33+
3134
_getContainerName(sentThrough) {
3235
switch (sentThrough) {
33-
case SENT_THROUGH.QUERY_PARAM:
36+
case SentThrough.QUERY_PARAM:
3437
return 'query';
35-
case SENT_THROUGH.HEADER:
38+
case SentThrough.HEADER:
3639
return 'headers';
37-
case SENT_THROUGH.BODY:
40+
case SentThrough.BODY:
3841
default:
3942
return 'body';
4043
}
4144
}
4245

43-
_getDecodeFunction(encoding) {
44-
switch (encoding) {
45-
case ENCODING_TYPE.URL_ENCODE:
46-
return (string) => querystring.parse(string);
47-
case ENCODING_TYPE.CLEAR_TEXT:
48-
default:
49-
return (string) => JSON.parse(string);
50-
}
51-
}
52-
5346
_getContainer(request) {
5447
const container = request[this.containerName];
5548
if (!container) {
@@ -59,7 +52,14 @@ class FieldExtractor {
5952
if (typeof container === 'object') {
6053
return container;
6154
}
62-
return this._decode(container);
55+
56+
const contentType = request.headers['content-type'];
57+
if (contentType.includes(ContentType.JSON)) {
58+
return JSON.parse(container);
59+
} else if (contentType.includes(ContentType.URL_ENCODED)) {
60+
return querystring.parse(container);
61+
}
62+
return null;
6363
}
6464
}
6565

lib/extract_field/FieldExtractorManager.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
const FieldExtractor = require('./FieldExtractor');
22
const ExtractionField = require('../models/ExtractionField');
3+
const { sha256 } = require('../pxutil');
34

45
const USERNAME_FIELD = 'user';
56
const PASSWORD_FIELD = 'pass';
7+
const CREDENTIALS_FIELD = 'creds';
68

79
class FieldExtractorManager {
810
constructor(logger, extractObjects) {
@@ -13,8 +15,13 @@ class FieldExtractorManager {
1315
ExtractFields(request) {
1416
const key = this._generateMapKey(request.path, request.method);
1517
const extractor = this.extractorMap[key];
18+
if (!extractor) {
19+
return {};
20+
}
21+
this.logger.debug(`Attempting to extract credentials for ${request.method} ${request.path} request`);
1622
try {
17-
return extractor ? extractor.ExtractFields(request) : {};
23+
const fields = extractor.ExtractFields(request);
24+
return this._processFields(fields);
1825
} catch (e) {
1926
this.logger.error(e);
2027
return {};
@@ -29,13 +36,25 @@ class FieldExtractorManager {
2936
const map = {};
3037
for (const extractFields of extractObjects) {
3138
const key = this._generateMapKey(extractFields.path, extractFields.method.toUpperCase());
32-
map[key] = new FieldExtractor(extractFields.sentThrough, extractFields.contentType, extractFields.encoding, [
33-
new ExtractionField(extractFields.userField, USERNAME_FIELD),
34-
new ExtractionField(extractFields.passField, PASSWORD_FIELD)
39+
map[key] = new FieldExtractor(extractFields.sent_through, [
40+
new ExtractionField(extractFields.user_field, USERNAME_FIELD, true),
41+
new ExtractionField(extractFields.pass_field, PASSWORD_FIELD, false)
3542
]);
3643
}
3744
return map;
3845
}
46+
47+
_processFields(fields) {
48+
if (!fields || !fields[USERNAME_FIELD] || !fields[PASSWORD_FIELD]) {
49+
this.logger.debug('Failed extracting credentials');
50+
return {};
51+
}
52+
this.logger.debug('Successfully extracted credentials')
53+
return {
54+
[USERNAME_FIELD]: sha256(fields[USERNAME_FIELD]),
55+
[CREDENTIALS_FIELD]: sha256(fields[USERNAME_FIELD] + fields[PASSWORD_FIELD])
56+
};
57+
}
3958
}
4059

4160
module.exports = FieldExtractorManager;

lib/models/ExtractionField.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
class ExtractionField {
2-
constructor(origRequestFieldName, resultActivityFieldName) {
3-
this.origRequestFieldName = origRequestFieldName;
4-
this.resultActivityFieldName = resultActivityFieldName;
2+
constructor(oldFieldName, newFieldName, shouldNormalize) {
3+
this.oldFieldName = oldFieldName;
4+
this.newFieldName = newFieldName;
5+
this.shouldNormalize = shouldNormalize;
56
}
67
}
78

lib/pxclient.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class PxClient {
3434
if (activityType === ActivityType.ADDITIONAL_S2S) {
3535
details['http_status_code'] = null;
3636
details['login_successful'] = null;
37+
details['raw_username'] = null;
3738
} else {
3839
activity.headers = ctx.headers;
3940
activity.pxhd = ctx.pxhdClient ? ctx.pxhdClient : undefined;

lib/pxconfig.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ class PxConfig {
8888
);
8989
});
9090

91+
this.addLoginCredentialsExtractionPathsToSensitiveRoutes();
92+
9193
// validate that app_id is configured
9294
if (this.PX_DEFAULT.PX_APP_ID !== 'PX_APP_ID') {
9395
// set backend url
@@ -113,6 +115,15 @@ class PxConfig {
113115
return Object.assign(this.PX_DEFAULT, this.PX_INTERNAL);
114116
}
115117

118+
addLoginCredentialsExtractionPathsToSensitiveRoutes() {
119+
if (this.PX_DEFAULT.ENABLE_LOGIN_CREDS_EXTRACTION) {
120+
this.PX_DEFAULT.SENSITIVE_ROUTES = [
121+
...this.PX_DEFAULT.SENSITIVE_ROUTES,
122+
...this.PX_DEFAULT.LOGIN_CREDS_EXTRACTION.map((extractionObject) => extractionObject.path)
123+
];
124+
}
125+
}
126+
116127
mergeConfigFileParams(configParams) {
117128
let mergedParams = configParams;
118129
try {

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.0.3",
3+
"version": "3.1.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)