Skip to content

Commit

Permalink
merge changes of restrict_iast_update
Browse files Browse the repository at this point in the history
  • Loading branch information
sumitsuthar committed Oct 9, 2024
2 parents ac77285 + 15cfbac commit b7c50f2
Show file tree
Hide file tree
Showing 22 changed files with 594 additions and 356 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:

strategy:
matrix:
node-version: [16.x, 18.x, 20.x, 22.x]
node-version: [18.x, 20.x, 22.x]

steps:
- uses: actions/checkout@v3
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
### v2.0.0 (2024-09-20)
### ⚠ BREAKING CHANGES
* Dropped support for Node.js v16
* Dropped functionality to generate snapshot file
#### Features
* Support to honour proxy settings via config
* Support for secure cookie security event generation
* Report error to Error Inbox upon connection failure to Security Engine
* Support to detect application and server path
* Functionality to truncate Incoming HTTP request upto default limit
* Dropped support for Node.js v16
* Dropped functionality to generate snapshot file
#### Bug fixes
* Handling for empty data in IAST fuzzing header
* Added identifiers in events
* Fix for file integrity security event generation
* Fix for missing identifiers in iast-data-request JSON

### v1.5.0 (2024-08-14)
#### Features
* Support for Node.js v22.x
Expand Down
19 changes: 18 additions & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ code, the source code can be found at [https://github.com/newrelic/csec-node-age
* [find-package-json](#find-package-json)
* [hash.js](#hashjs)
* [html-entities](#html-entities)
* [https-proxy-agent](#https-proxy-agent)
* [is-invalid-path](#is-invalid-path)
* [js-yaml](#js-yaml)
* [jsonschema](#jsonschema)
Expand Down Expand Up @@ -239,6 +240,22 @@ THE SOFTWARE.
```

### https-proxy-agent

This product includes source derived from [https-proxy-agent](https://github.com/TooTallNate/node-https-proxy-agent) ([v5.0.1](https://github.com/TooTallNate/node-https-proxy-agent/tree/v5.0.1)), distributed under the [MIT License](https://github.com/TooTallNate/node-https-proxy-agent/blob/v5.0.1/README.md):

```
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

### is-invalid-path

This product includes source derived from [is-invalid-path](https://github.com/jonschlinkert/is-invalid-path) ([v1.0.2](https://github.com/jonschlinkert/is-invalid-path/tree/v1.0.2)), distributed under the [MIT License](https://github.com/jonschlinkert/is-invalid-path/blob/v1.0.2/LICENSE):
Expand Down Expand Up @@ -588,7 +605,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI

### ws

This product includes source derived from [ws](https://github.com/websockets/ws) ([v8.17.1](https://github.com/websockets/ws/tree/v8.17.1)), distributed under the [MIT License](https://github.com/websockets/ws/blob/v8.17.1/LICENSE):
This product includes source derived from [ws](https://github.com/websockets/ws) ([v8.18.0](https://github.com/websockets/ws/tree/v8.18.0)), distributed under the [MIT License](https://github.com/websockets/ws/blob/v8.18.0/LICENSE):

```
Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
Expand Down
34 changes: 29 additions & 5 deletions lib/instrumentation-security/core/request-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
const requestMap = new Map();
const API = require("../../nr-security-api");
const NRAgent = API.getNRAgent();
const logger = API.getLogger();
const regexPatterns = NRAgent && NRAgent.config.security.exclude_from_iast_scan.api

/**
Expand All @@ -23,12 +24,33 @@ function getRequestFromId(id) {
* @param {*} requestData
*/
function setRequest(id, requestData) {

requestMap.set(id, requestData);
const stringToMatch = requestData.uri ? requestData.uri : requestData.url;
const filteredString = stringToMatch.split('?')[0];
if (regexPatterns.some(regex => filteredString.match(regex))) {
requestMap.delete(id);
try {
const stringToMatch = requestData.url;
// const filteredString = stringToMatch.split('?')[0];
const filteredString = stringToMatch;
let isRegexMatchestoURL = false;
for (let index = 0; index < regexPatterns.length; index++) {
const regex = new RegExp(regexPatterns[index], 'gm');
const matchResult = filteredString.match(regex);
if (matchResult && matchResult.length > 0) {
const data = matchResult[0];
if (filteredString === data) {
isRegexMatchestoURL = true;
}
}
}
if (isRegexMatchestoURL) {
requestMap.delete(id);
if (API.getSecAgent().status.getStatus() !== 'disabled') {
logger.debug("Excluding URL %s from IAST processing due to ignore API setting", filteredString);
}
}
} catch (error) {
logger.debug("Error while processing API regex for restriction", error);
}

}

/**
Expand Down Expand Up @@ -56,13 +78,14 @@ function getRequest(shim) {
* @param {*} shim
* @param {*} data
*/
function updateRequestBody(shim, data) {
function updateRequestBody(shim, data, isTruncated) {
const segment = shim.getActiveSegment();
if (segment) {
const transactionId = segment.transaction.id;
const requestData = getRequestFromId(transactionId);
if (requestData) {
requestData.body = data;
requestData.dataTruncated = isTruncated;
setRequest(transactionId, requestData);
}
}
Expand All @@ -73,6 +96,7 @@ function updateRequestBody(shim, data) {
const requestData = getRequestFromId(traceId);
if (requestData) {
requestData.body = data;
requestData.dataTruncated = isTruncated;
setRequest(traceId, requestData)
}
}
Expand Down
1 change: 1 addition & 0 deletions lib/instrumentation-security/core/sec-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ function addRequestData(shim, request) {
});
}
data.clientIP = requestIp.getClientIp(request);
data.dataTruncated = false;
const transactionId = segment.transaction.id;
const storedRequest = requestManager.getRequestFromId(transactionId);
if (storedRequest && storedRequest.uri) {
Expand Down
33 changes: 7 additions & 26 deletions lib/instrumentation-security/hooks/fs/nr-fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const RENAME = 'rename';
const FSReqCallback = 'FSReqCallback';
const FSReqWrap = 'FSReqWrap';
const OBJECT = 'object';
const semver = require('semver');

const functionsProbableToFA = [
"fstat",
Expand All @@ -37,7 +38,7 @@ const functionsProbableToFA = [
"realpath",
];

const functionProbableToFI = [
let functionProbableToFI = [
"rename",
"ftruncate",
"rmdir",
Expand Down Expand Up @@ -143,9 +144,8 @@ function openHook(shim, mod, moduleName) {
parameters[0] = API.getSecAgent().applicationInfo.serverInfo.deployedApplications[0].deployedPath + SLASH + parameters[0];
} else if (parameters[0].startsWith(SLASHDOTDOT)) {
parameters[0] = API.getSecAgent().applicationInfo.serverInfo.deployedApplications[0].deployedPath + parameters[0];
} else {
parameters[0] = path.resolve(parameters[0]);
}
}

} catch (error) {

}
Expand Down Expand Up @@ -189,11 +189,6 @@ function probableToFAHooks(shim, mod, moduleName) {
const request = requestManager.getRequest(shim);
if (request && typeof arguments[0] === STRING && !lodash.isEmpty(arguments[0])) {
const traceObject = secUtils.getTraceObject(shim);
try {
parameters[0] = path.resolve(parameters[0]);
} catch (error) {

}
let absoluteParameters = [parameters[0]];
const secMetadata = securityMetaData.getSecurityMetaData(request, absoluteParameters, traceObject, secUtils.getExecutionId(), EVENT_TYPE.FILE_OPERATION, EVENT_CATEGORY.FILE)
this.secEvent = API.generateSecEvent(secMetadata);
Expand All @@ -216,6 +211,9 @@ function probableToFAHooks(shim, mod, moduleName) {
* @param {*} moduleName
*/
function probableToFIHooks(shim, mod, moduleName) {
if (semver.satisfies(process.version, '>19.0.0')) {
functionProbableToFI.push('writeFileSync');
}
functionProbableToFI.forEach(function (fun) {
shim.wrap(mod, fun, function makeFIWrapper(shim, fn) {
logger.debug(`Instrumenting ${moduleName}.${fun}`);
Expand All @@ -226,12 +224,6 @@ function probableToFIHooks(shim, mod, moduleName) {
const request = requestManager.getRequest(shim);
if (request && typeof arguments[0] === STRING && !lodash.isEmpty(arguments[0])) {
const traceObject = secUtils.getTraceObject(shim);
try {
parameters[0] = path.resolve(parameters[0]);
parameters[1] = path.resolve(parameters[1]);
} catch (error) {

}
let absoluteParameters = [parameters[0]];
if (fun === COPY_FILE || fun === RENAME) {
const secMetadata = securityMetaData.getSecurityMetaData(request, parameters[1], traceObject, secUtils.getExecutionId(), getCase(arguments[1]), EVENT_CATEGORY.FILE)
Expand Down Expand Up @@ -269,11 +261,6 @@ function probablePromiseToFAHooks(shim, mod, moduleName) {
const request = requestManager.getRequest(shim);
if (request && typeof arguments[0] === STRING && !lodash.isEmpty(arguments[0])) {
const traceObject = secUtils.getTraceObject(shim);
try {
parameters[0] = path.resolve(parameters[0]);
} catch (error) {

}
let absoluteParameters = [parameters[0]];
const secMetadata = securityMetaData.getSecurityMetaData(request, absoluteParameters, traceObject, secUtils.getExecutionId(), EVENT_TYPE.FILE_OPERATION, EVENT_CATEGORY.FILE)
this.secEvent = API.generateSecEvent(secMetadata);
Expand Down Expand Up @@ -302,12 +289,6 @@ function probablePromiseToFIHooks(shim, mod, moduleName) {
const request = requestManager.getRequest(shim);
if (request && typeof arguments[0] === STRING && !lodash.isEmpty(arguments[0])) {
const traceObject = secUtils.getTraceObject(shim);
try {
parameters[0] = path.resolve(parameters[0]);
parameters[1] = path.resolve(parameters[1]);
} catch (error) {

}
let absoluteParameters = [parameters[0]];
if (fun === COPY_FILE || fun === RENAME) {
const secMetadata = securityMetaData.getSecurityMetaData(request, parameters[1], traceObject, secUtils.getExecutionId(), getCase(arguments[1]), EVENT_CATEGORY.FILE)
Expand Down
82 changes: 74 additions & 8 deletions lib/instrumentation-security/hooks/http/nr-http.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ const semver = require('semver');
const ExceptionReporting = require('../../../nr-security-agent/lib/core/ExceptionReporting');

const CSEC_SEP = ':IAST:';
const find = '{{NR_CSEC_VALIDATOR_HOME_TMP}}';
const sep = require('path').sep;
const find = `${sep}{{NR_CSEC_VALIDATOR_HOME_TMP}}`;
const CSEC_HOME_TMP_CONST = new RegExp(find, 'g');
let CSEC_HOME = NRAgent && NRAgent.config.newrelic_home ? NRAgent.config.newrelic_home : NRAgent && NRAgent.config.logging.filepath ? path.dirname(NRAgent.config.logging.filepath) : require('path').join(process.cwd());
const CSEC_HOME_TMP = `${CSEC_HOME}/nr-security-home/tmp/language-agent/${process.env.applicationUUID}`
let lastTransactionId = EMPTY_STRING;
let uncaughtExceptionReportFlag = false;
const requestBodyLimit = 500;


/**
Expand Down Expand Up @@ -178,6 +180,7 @@ function parseFuzzheaders(requestData, transactionId) {
if (additionalData.length >= 8) {
let encryptedData = additionalData[6].trim();
let hashVerifier = additionalData[7].trim();

if (lodash.isEmpty(encryptedData) || lodash.isEmpty(hashVerifier)) {
return;
}
Expand All @@ -191,6 +194,7 @@ function parseFuzzheaders(requestData, transactionId) {
logger.debug("fliesTocreate:", filesToCreate);
for (let i = 0; i < filesToCreate.length; i++) {
let file = filesToCreate[i].trim();
file = decodeURIComponent(file);
file = file.replace(CSEC_HOME_TMP_CONST, CSEC_HOME_TMP);
file = path.resolve(file);
const parentDir = path.dirname(file);
Expand Down Expand Up @@ -251,6 +255,7 @@ function addRequestData(shim, request) {
});
}
data.clientIP = requestIp.getClientIp(request);
data.dataTruncated = false;
const transactionId = segment.transaction.id;
const storedRequest = requestManager.getRequestFromId(transactionId);
lastTransactionId = transactionId;
Expand Down Expand Up @@ -291,7 +296,7 @@ function emitHook(shim, mod, moduleName) {
const resp = arguments[2];

if (arguments[0] == 'request') {
if (NRAgent && NRAgent.config.security.detection.rxss.enabled && !NRAgent.config.security.exclude_from_iast_scan.iast_detection_category.rxss) {
if (NRAgent && !NRAgent.config.security.exclude_from_iast_scan.iast_detection_category.rxss && NRAgent.config.security.detection.rxss.enabled) {
responseHook(resp, req, shim);
}

Expand Down Expand Up @@ -370,7 +375,21 @@ function onDataHook(shim, mod) {
const requestData = requestManager.getRequestFromId(transactionId);
if (requestData) {
data = requestData.body ? requestData.body.concat(chunk.toString()) : chunk.toString();
requestManager.updateRequestBody(shim, data);
requestManager.updateRequestBody(shim, data, false);
try {
const contentLength = Buffer.byteLength(data, 'utf8');
let bodyLimit = requestBodyLimit;
if (!isNaN(bodyLimit)) {
bodyLimit = bodyLimit * 1000;
if (contentLength && contentLength > bodyLimit) {
data = truncateStringToBytes(data, bodyLimit);
requestManager.updateRequestBody(shim, data, true);
}
}

} catch (error) {
logger.error("Error while truncating request body", error);
}
}

}
Expand Down Expand Up @@ -425,7 +444,12 @@ function responseHook(resp, req, shim) {
} catch (error) {
logger.debug("Error while reporting error", error);
}
secureCookieCheck(response, shim);
try {
secureCookieCheck(response, shim);
} catch (error) {
logger.debug("Error while generating secure cookie event");
}

responseBodyCompute(response, arguments);

const construct = API.checkForReflectedXSS(request, response.res.body, response.getHeaders());
Expand Down Expand Up @@ -613,20 +637,48 @@ process.on('uncaughtException', (err, origin) => {
uncaughtExceptionReportFlag = true;
});

/**
* utility to parse response headers for cookie check
* @param {*} headersString
* @returns
*/
function responseRawHeaderParsing(headersString) {
let setCookieValue;
try {
const headersArray = headersString.split('\r\n');
for (const header of headersArray) {
if (header.toLowerCase().startsWith('set-cookie:')) {
setCookieValue = header.trim().replace('set-cookie:', '');
break;
}
}
} catch (error) {

}
return setCookieValue;
}

/**
* Utility to check insecure cookie settings
* @param {*} response
* @param {*} shim
*/
function secureCookieCheck(response, shim) {
const cookieHeader = response.getHeader('set-cookie');
let csec_request = requestManager.getRequest(shim);
let cookieHeader = response.getHeader('set-cookie') ? response.getHeader('set-cookie') : responseRawHeaderParsing(response._header);
let cookieHeaderList = [];
if (!cookieHeader) {
return;
}
if (typeof cookieHeader === 'string') {
cookieHeaderList.push(cookieHeader)
}
else {
cookieHeaderList = cookieHeaderList.concat(cookieHeader);
}
let csec_request = requestManager.getRequest(shim);
try {
let parameters = [];
for (const cookieString of cookieHeader) {
for (const cookieString of cookieHeaderList) {
if (cookieString) {
const keyValuePairs = cookieString.split(SEMI_COLON);
let params = {
Expand Down Expand Up @@ -663,4 +715,18 @@ function secureCookieCheck(response, shim) {
logger.debug("Error while generating secure cookie event:", error);
}

}
}

function truncateStringToBytes(inputString, maxBytes) {
if (!inputString || maxBytes <= 0) {
return EMPTY_STRING; // Return an empty string if input is invalid
}
const buffer = Buffer.from(inputString, UTF8);
if (buffer.length <= maxBytes) {
return inputString;
}
const truncatedBuffer = buffer.slice(0, maxBytes);
const truncatedString = truncatedBuffer.toString(UTF8);
return truncatedString;
}

Loading

0 comments on commit b7c50f2

Please sign in to comment.