Socket
Socket
Sign inDemoInstall

@contrast/protect

Package Overview
Dependencies
Maintainers
17
Versions
73
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@contrast/protect - npm Package Compare versions

Comparing version 1.7.0 to 1.8.0

235

lib/input-analysis/handlers.js

@@ -18,3 +18,9 @@ /*

const { BLOCKING_MODES, simpleTraverse, Rule, isString } = require('@contrast/common');
const {
BLOCKING_MODES,
simpleTraverse,
Rule,
isString,
ProtectRuleMode: { OFF },
} = require('@contrast/common');
const address = require('ipaddr.js');

@@ -61,2 +67,10 @@

const jsonInputTypes = {
keyType: agentLib.InputType.JsonKey, inputType: agentLib.InputType.JsonValue
};
const parameterInputTypes = {
keyType: agentLib.InputType.ParameterKey, inputType: agentLib.InputType.ParameterValue
};
// all handlers will be invoked with two arguments:

@@ -117,4 +131,2 @@ // 1) sourceContext object containing:

inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
if (!sourceContext || sourceContext.allowed) return;
const { policy: { rulesMask } } = sourceContext;

@@ -128,2 +140,3 @@

const findings = agentLib.scoreRequestConnect(rulesMask, connectInputs, preferWW);
block = mergeFindings(sourceContext, findings);

@@ -136,74 +149,2 @@ }

/**
* handleRequestEnd()
*
* Invoked when the request is complete.
*
* @param {Object} sourceContext
*/
inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) {
if (!config.protect.probe_analysis.enable) return;
const { resultsMap } = sourceContext.findings;
const probesRules = [Rule.CMD_INJECTION, Rule.PATH_TRAVERSAL, Rule.SQL_INJECTION, Rule.XXE];
const props = {};
// Detecting probes
Object.values(resultsMap).forEach(resultsByRuleId => {
resultsByRuleId.forEach((resultByRuleId) => {
const {
ruleId,
blocked,
details,
value,
inputType
} = resultByRuleId;
if (blocked || !blocked && details.length > 0 || !probesRules.some(rule => rule === ruleId)) return;
const { policy: { rulesMask } } = sourceContext;
const results = (agentLib.scoreAtom(
rulesMask,
value,
agentLib.InputType[inputType],
{
preferWorthWatching: false
}
) || []).filter(({ score }) => score >= 90);
if (!results.length) return;
results.forEach(result => {
const isAlreadyBlocked = (resultsMap[result.ruleId] || []).some(element =>
element.blocked && element.inputType === inputType && element.value === value
);
if (isAlreadyBlocked) return;
const probe = Object.assign({}, resultByRuleId, result, {
mappedId: result.ruleId
});
const key = [probe.ruleId, probe.inputType, ...probe.path, probe.value].join('|');
props[key] = probe;
});
});
});
Object.values(props).forEach(prop => {
if (!resultsMap[prop.ruleId]) {
resultsMap[prop.ruleId] = [];
}
resultsMap[prop.ruleId].push(prop);
});
};
const jsonInputTypes = {
keyType: agentLib.InputType.JsonKey, inputType: agentLib.InputType.JsonValue
};
const parameterInputTypes = {
keyType: agentLib.InputType.ParameterKey, inputType: agentLib.InputType.ParameterValue
};
/**
* handleQueryParams()

@@ -233,3 +174,2 @@ *

inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: queryParams });

@@ -273,6 +213,9 @@

}
const items = agentLib.scoreAtom(rulesMask, value, UrlParameter, preferWW);
if (!items) {
return;
}
for (const item of items) {

@@ -322,2 +265,6 @@ resultsList.push({

inputAnalysis.handleVirtualPatches(sourceContext, { HEADERS: cookies });
const { policy: { rulesMask } } = sourceContext;
const cookiesArr = Object.entries(cookies).reduce((acc, [key, value]) => {

@@ -328,5 +275,2 @@ acc.push(key, value);

inputAnalysis.handleVirtualPatches(sourceContext, { HEADERS: cookies });
const { policy: { rulesMask } } = sourceContext;
const cookieFindings = agentLib.scoreRequestConnect(rulesMask, { cookies: cookiesArr }, preferWW);

@@ -352,2 +296,3 @@

if (sourceContext.analyzedBody) return;
sourceContext.analyzedBody = true;

@@ -371,2 +316,3 @@

}
const block = commonObjectAnalyzer(sourceContext, parsedBody, inputTypes);

@@ -493,2 +439,66 @@

/**
* handleRequestEnd()
*
* Invoked when the request is complete.
*
* @param {Object} sourceContext
*/
inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) {
if (!config.protect.probe_analysis.enable || sourceContext.allowed) return;
const { resultsMap } = sourceContext.findings;
const probesRules = [Rule.CMD_INJECTION, Rule.PATH_TRAVERSAL, Rule.SQL_INJECTION, Rule.XXE];
const props = {};
// Detecting probes
Object.values(resultsMap).forEach(resultsByRuleId => {
resultsByRuleId.forEach((resultByRuleId) => {
const {
ruleId,
blocked,
details,
value,
inputType
} = resultByRuleId;
if (blocked || !blocked && details.length > 0 || !probesRules.some(rule => rule === ruleId)) return;
const { policy: { rulesMask } } = sourceContext;
const results = (agentLib.scoreAtom(
rulesMask,
value,
agentLib.InputType[inputType],
{
preferWorthWatching: false
}
) || []).filter(({ score }) => score >= 90);
if (!results.length) return;
results.forEach(result => {
const isAlreadyBlocked = (resultsMap[result.ruleId] || []).some(element =>
element.blocked && element.inputType === inputType && element.value === value
);
if (isAlreadyBlocked) return;
const probe = Object.assign({}, resultByRuleId, result, {
mappedId: result.ruleId
});
const key = [probe.ruleId, probe.inputType, ...probe.path, probe.value].join('|');
props[key] = probe;
});
});
});
Object.values(props).forEach(prop => {
if (!resultsMap[prop.ruleId]) {
resultsMap[prop.ruleId] = [];
}
resultsMap[prop.ruleId].push(prop);
});
};
/**
* commonObjectAnalyzer() walks an object supplied by the end-user and checks

@@ -547,3 +557,5 @@ * it for vulnerabilities.

}
let items = agentLib.scoreAtom(rulesMask, value, itemType, preferWW);
if (!items && !isMongoQueryType) {

@@ -645,2 +657,62 @@ return;

/**
* Reads the source context's policy and compares to result item to check whether to ignore it.
* @param {ProtectMessage} sourceContext
* @param {Result} result
* @returns {boolean} whether result should be excluded
*/
function isResultExcluded(sourceContext, result) {
const { policy: { exclusions } } = sourceContext;
const { ruleId, path, inputType, value } = result;
const inputName = path ? path[path.length - 1] : null;
let checkCookiesInHeader = false;
let inputExclusions;
switch (inputType) {
case 'JsonKey':
case 'JsonValue':
case 'MultipartName': {
return exclusions.ignoreBody || exclusions.bodyPolicy?.[ruleId] === OFF;
}
case 'ParameterKey':
case 'ParameterValue': {
const qsExcluded = exclusions.ignoreQuerystring || exclusions.querystringPolicy?.[ruleId] === OFF;
if (qsExcluded) return true;
inputExclusions = exclusions.parameter;
break;
}
case 'CookieValue': {
inputExclusions = exclusions.cookie;
break;
}
case 'HeaderKey':
case 'HeaderValue': {
if (path?.[0]?.toLowerCase() === 'cookie') {
inputExclusions = exclusions.cookie;
checkCookiesInHeader = true;
} else {
inputExclusions = exclusions.header;
}
break;
}
}
if (!inputName || !inputExclusions) return false;
for (const excl of inputExclusions) {
let nameCheck = false;
if (checkCookiesInHeader) {
nameCheck = excl.checkCookiesInHeader(value);
} else {
nameCheck = excl.matchesInputName(inputName);
}
if (!nameCheck) continue;
if (!excl.policy || excl.policy[ruleId] === OFF) {
return true;
}
}
return false;
}
/**
* merge new findings into the existing findings

@@ -658,2 +730,7 @@ *

}
newFindings.resultsList = newFindings.resultsList.filter(
(result) => !isResultExcluded(sourceContext, result)
);
normalizeFindings(policy, newFindings);

@@ -660,0 +737,0 @@

@@ -179,4 +179,9 @@ /*

}
store.protect = this.makeSourceContext(req, res);
store.protect = this.makeSourceContext(req, res);
if (store.protect.allowed) {
setImmediate(() => method.call(instance, ...args));
return;
}
const { reqData } = store.protect;

@@ -183,0 +188,0 @@

@@ -29,3 +29,3 @@ /*

messages.on(Event.SERVER_SETTINGS_UPDATE, (serverUpdate) => {
const virtualPatches = serverUpdate.settings?.defend.virtualPatches;
const virtualPatches = serverUpdate.settings?.defend?.virtualPatches;
if (virtualPatches) {

@@ -32,0 +32,0 @@ buildVPEvaluators(virtualPatches, virtualPatchesEvaluators);

@@ -19,3 +19,5 @@ /*

module.exports = function(core) {
const { protect } = core;
const {
protect: { getPolicy }
} = core;

@@ -31,4 +33,6 @@ function makeSourceContext(req, res) {

// separate path and search params
let uriPath, queries;
const ix = req.url.indexOf('?');
let uriPath, queries;
if (ix >= 0) {

@@ -41,2 +45,10 @@ uriPath = req.url.slice(0, ix);

}
const policy = getPolicy({ uriPath });
// URL exclusions can disable all rules
if (!policy) {
return { allowed: true };
}
// lowercase header keys and capture content-type

@@ -86,3 +98,3 @@ let contentType = '';

policy: protect.getPolicy(),
policy,

@@ -89,0 +101,0 @@ exclusions: [],

@@ -30,10 +30,68 @@ /*

module.exports = function(core) {
const { config, logger, messages, protect } = core;
const policy = protect.policy = {};
const {
config,
logger,
messages,
protect,
protect: { agentLib }
} = core;
const compiled = {
url: [],
querystring: [],
header: [],
body: [],
cookie: [],
parameter: [],
};
const policy = protect.policy = {
exclusions: compiled
};
function regExpCheck(str) {
return str.indexOf('*') > 0 ||
str.indexOf('.') > 0 ||
str.indexOf('+') > 0 ||
str.indexOf('?') > 0 ||
str.indexOf('\\') > 0;
}
function buildUriPathRegExp(urls) {
let regExpNeeded = false;
for (const url of urls) {
if (regExpCheck(url)) {
regExpNeeded = true;
}
}
if (regExpNeeded) {
const rx = new RegExp(`^${urls.join('|')}$`);
return (uriPath) => rx ? rx.test(uriPath) : false;
}
return (uriPath) => urls.some((url) => url === uriPath);
}
function createUriPathMatcher(urls) {
if (urls.length) {
return buildUriPathRegExp(urls);
} else {
return () => true;
}
}
function createInputNameMatcher(dtmInputName) {
if (regExpCheck(dtmInputName)) {
const rx = new RegExp(`^${dtmInputName}$`);
return (inputName) => rx ? rx.test(inputName) : false;
}
return (inputName) => inputName === dtmInputName;
}
function getModeFromConfig(ruleId) {
if (config.protect.disabled_rules.includes(ruleId)) {
return 'off';
return OFF;
}

@@ -88,3 +146,11 @@ return config.protect.rules?.[ruleId]?.mode;

let rulesMask = 0;
for (const [ruleId, mode] of Object.entries(policy)) {
for (const entry of Object.entries(policy)) {
let [ruleId] = entry;
const [, mode] = entry;
if (ruleId === 'nosql-injection') {
ruleId = 'nosql-injection-mongo';
}
if (protect.agentLib.RuleType[ruleId] && mode !== OFF) {

@@ -94,2 +160,3 @@ rulesMask = rulesMask | protect.agentLib.RuleType[ruleId];

}
policy.rulesMask = rulesMask;

@@ -102,9 +169,79 @@ }

*/
function getPolicy() {
return { ...policy };
function getPolicy({ uriPath } = {}) {
const requestPolicy = {
exclusions: {
ignoreQuerystring: false,
querystringPolicy: null,
ignoreBody: false,
bodyPolicy: null,
header: [],
cookie: [],
parameter: [],
},
rulesMask: policy.rulesMask,
};
for (const ruleId of Object.values(Rule)) {
requestPolicy[ruleId] = policy[ruleId];
}
// handle exclusions
for (const [inputType, exclusions] of Object.entries(compiled)) {
for (const e of exclusions) {
if (!e.matchesUriPath(uriPath)) continue;
// url exclusions
if (inputType === 'url') {
// if applies to all rules, there is no policy for the request i.e. disable protect
if (!e.policy) {
return null;
}
// merge exclusion's policy into the request's policy
for (const key of Object.keys(e.policy)) {
const value = e.policy[key];
if (key === 'rulesMask') {
// this is how to disable rules bitwise
requestPolicy.rulesMask = requestPolicy.rulesMask & ~value;
} else {
requestPolicy[key] = value;
}
}
} else if (inputType === 'querystring') {
if (!e.policy) {
requestPolicy.exclusions.ignoreQuerystring = true;
} else {
// merge exclusion's policy into the querystring's policy
requestPolicy.exclusions.querystringPolicy = requestPolicy.exclusions.querystringPolicy || {};
for (const key of Object.keys(e.policy)) {
const value = e.policy[key];
if (key !== 'rulesMask') {
requestPolicy.exclusions.querystringPolicy[key] = value;
}
}
}
} else if (inputType === 'body') {
if (!e.policy) {
requestPolicy.exclusions.ignoreBody = true;
} else {
// merge exclusion's policy into the querystring's policy
requestPolicy.exclusions.bodyPolicy = requestPolicy.exclusions.bodyPolicy || {};
for (const key of Object.keys(e.policy)) {
const value = e.policy[key];
if (key !== 'rulesMask') {
requestPolicy.exclusions.bodyPolicy[key] = value;
}
}
}
} else {
// copy matching input exclusions into request policy
requestPolicy.exclusions[inputType].push(e);
}
}
}
return requestPolicy;
}
initPolicy();
messages.on(SERVER_SETTINGS_UPDATE, (remoteSettings) => {
function updateGlobalPolicy(remoteSettings) {
let update;

@@ -135,5 +272,77 @@

}
}
function updateExclusions(serverUpdate) {
const exclusions = [
...(serverUpdate.settings?.exceptions?.inputExceptions || []),
...(serverUpdate.settings?.exceptions?.urlExceptions || [])
].filter((exclusion) => exclusion.modes.includes('defend'));
if (!exclusions.length) return;
for (const exclusionDtm of exclusions) {
exclusionDtm.inputType = exclusionDtm.inputType || 'URL';
const { name, rules, inputName, urls, inputType } = exclusionDtm;
const key = inputType.toLowerCase();
if (!compiled[key]) continue;
try {
const e = { name };
e.matchesUriPath = createUriPathMatcher(urls);
if (inputName) {
e.matchesInputName = createInputNameMatcher(inputName);
}
if (rules.length) {
let rulesMask = 0;
const exclusionPolicy = {};
for (let ruleId of rules) {
// todo: this doesn't seem to make a difference?
if (ruleId === 'nosql-injection') {
ruleId = 'nosql-injection-mongo';
}
if (agentLib.RuleType[ruleId]) {
Object.assign(exclusionPolicy, { [ruleId]: OFF });
if (inputType === 'URL') {
rulesMask = rulesMask | agentLib.RuleType[ruleId];
exclusionPolicy.rulesMask = rulesMask;
}
}
}
e.policy = exclusionPolicy;
}
if (key === 'cookie') {
e.checkCookieInHeader = (cookieHeader) => {
for (const cookiePair of cookieHeader.split(';')) {
const cookieKey = cookiePair.split('=')[0];
if (e.matchesInputName(cookieKey)) {
return true;
}
}
return false;
};
}
compiled[key].push(e);
} catch (err) {
logger.error({ err, exclusionDtm }, 'failed to process exclusion');
}
}
}
messages.on(SERVER_SETTINGS_UPDATE, (msg) => {
updateGlobalPolicy(msg);
updateExclusions(msg);
});
initPolicy();
return protect.getPolicy = getPolicy;
};
{
"name": "@contrast/protect",
"version": "1.7.0",
"version": "1.8.0",
"description": "Contrast service providing framework-agnostic Protect support",

@@ -5,0 +5,0 @@ "license": "SEE LICENSE IN LICENSE",

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc