@contrast/protect
Advanced tools
Comparing version 1.5.0 to 1.6.0
@@ -41,3 +41,3 @@ /* | ||
protect.install = function() { | ||
installChildComponentsSync(protect) | ||
installChildComponentsSync(protect); | ||
}; | ||
@@ -44,0 +44,0 @@ |
@@ -20,2 +20,3 @@ /* | ||
const address = require('ipaddr.js'); | ||
const { Rule } = require('@contrast/common'); | ||
@@ -58,2 +59,3 @@ // | ||
}, | ||
config, | ||
} = core; | ||
@@ -132,8 +134,64 @@ | ||
// this is called before the request goes away. this is where probe detection takes | ||
// place. basically loop through findings.resultsList and, for all those that weren't | ||
// blocked, run scoreAtom() with preferWorthWatching: false. for any that have a score | ||
// >= 90 report them as probes. | ||
/** | ||
* handleRequestEnd() | ||
* | ||
* Invoked when the request is complete. | ||
* | ||
* @param {Object} sourceContext | ||
*/ | ||
inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) { | ||
throw new Error('nyi', 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); | ||
}); | ||
}; | ||
@@ -140,0 +198,0 @@ |
@@ -39,44 +39,42 @@ /* | ||
function install() { | ||
depHooks.resolve({ name: 'fastify', version: '>=3 <5' }, (fastify) => { | ||
return patcher.patch(fastify, { | ||
name: 'fastify.build', | ||
patchType, | ||
post({ result: server }) { | ||
server.addHook('preValidation', function(request, reply, done) { | ||
let securityException; | ||
const sourceContext = protect.getSourceContext('Fastify.preValidationHook'); | ||
depHooks.resolve({ name: 'fastify', version: '>=3 <5' }, (fastify) => patcher.patch(fastify, { | ||
name: 'fastify.build', | ||
patchType, | ||
post({ result: server }) { | ||
server.addHook('preValidation', function(request, reply, done) { | ||
let securityException; | ||
const sourceContext = protect.getSourceContext('Fastify.preValidationHook'); | ||
if (sourceContext) { | ||
try { | ||
if (request.params) { | ||
sourceContext.parsedParams = request.params; | ||
inputAnalysis.handleUrlParams(sourceContext, request.params); | ||
} | ||
if (request.cookies) { | ||
sourceContext.parsedCookies = request.cookies; | ||
inputAnalysis.handleCookies(sourceContext, request.cookies); | ||
} | ||
if (request.body) { | ||
sourceContext.parsedBody = request.body; | ||
inputAnalysis.handleParsedBody(sourceContext, request.body); | ||
} | ||
if (sourceContext) { | ||
try { | ||
if (request.params) { | ||
sourceContext.parsedParams = request.params; | ||
inputAnalysis.handleUrlParams(sourceContext, request.params); | ||
} | ||
if (request.cookies) { | ||
sourceContext.parsedCookies = request.cookies; | ||
inputAnalysis.handleCookies(sourceContext, request.cookies); | ||
} | ||
if (request.body) { | ||
sourceContext.parsedBody = request.body; | ||
inputAnalysis.handleParsedBody(sourceContext, request.body); | ||
} | ||
if (request.query) { | ||
sourceContext.parsedQuery = request.query; | ||
inputAnalysis.handleQueryParams(sourceContext, request.query); | ||
} | ||
} catch (err) { | ||
if (isSecurityException(err)) { | ||
securityException = err; | ||
} else { | ||
logger.error({ err }, 'Unexpected error during input analysis'); | ||
} | ||
if (request.query) { | ||
sourceContext.parsedQuery = request.query; | ||
inputAnalysis.handleQueryParams(sourceContext, request.query); | ||
} | ||
} catch (err) { | ||
if (isSecurityException(err)) { | ||
securityException = err; | ||
} else { | ||
logger.error({ err }, 'Unexpected error during input analysis'); | ||
} | ||
} | ||
} | ||
done(securityException); | ||
}); | ||
}, | ||
}); | ||
}); | ||
done(securityException); | ||
}); | ||
}, | ||
})); | ||
} | ||
@@ -83,0 +81,0 @@ |
@@ -30,7 +30,6 @@ /* | ||
constructor(core) { | ||
const { logger } = core; | ||
this.messages = core.messages; | ||
this.scope = core.scopes.sources; | ||
this.config = core.config; | ||
this.logger = logger.child({ name: 'contrast:protect:input-analysis' }); | ||
this.logger = core.logger.child('contrast:protect:input-analysis'); | ||
this.depHooks = core.depHooks; | ||
@@ -185,3 +184,6 @@ this.protect = core.protect; | ||
res.on('finish', () => messages.emit(Event.PROTECT, store)); | ||
res.on('finish', () => { | ||
this.protect.inputAnalysis.handleRequestEnd(store.protect); | ||
messages.emit(Event.PROTECT, store); | ||
}); | ||
@@ -188,0 +190,0 @@ // don't put inputs in the store; they are a param to each handler. findings |
@@ -57,2 +57,3 @@ /* | ||
core.protect.semanticAnalysis.handleCmdInjectionSemanticDangerous(sourceContext, sinkContext); | ||
core.protect.semanticAnalysis.handlePathTraversalFileSecurityBypass(sourceContext, sinkContext); | ||
} | ||
@@ -59,0 +60,0 @@ }); |
@@ -109,2 +109,3 @@ /* | ||
inputTracing.handlePathTraversal(sourceContext, sinkContext); | ||
core.protect.semanticAnalysis.handlePathTraversalFileSecurityBypass(sourceContext, sinkContext); | ||
} | ||
@@ -111,0 +112,0 @@ } |
@@ -37,19 +37,21 @@ /* | ||
patcher.patch(global.ContrastMethods, 'Function', { | ||
name: 'global.ContrastMethods.Function', | ||
patchType, | ||
pre: ({ args, hooked, orig }) => { | ||
if (instrumentation.isLocked()) return; | ||
Object.assign(global.ContrastMethods, { | ||
Function: patcher.patch(global.ContrastMethods.Function, { | ||
name: 'global.ContrastMethods.Function', | ||
patchType, | ||
pre: ({ args, hooked, orig }) => { | ||
if (instrumentation.isLocked()) return; | ||
const sourceContext = protect.getSourceContext('Function'); | ||
const fnBody = args[args.length - 1]; | ||
const sourceContext = protect.getSourceContext('Function'); | ||
const fnBody = args[args.length - 1]; | ||
if (!sourceContext || !fnBody || !isString(fnBody)) return; | ||
if (!sourceContext || !fnBody || !isString(fnBody)) return; | ||
const sinkContext = captureStacktrace( | ||
{ name: 'Function', value: fnBody }, | ||
{ constructorOpt: hooked, prependFrames: [orig] } | ||
); | ||
inputTracing.ssjsInjection(sourceContext, sinkContext); | ||
} | ||
const sinkContext = captureStacktrace( | ||
{ name: 'Function', value: fnBody }, | ||
{ constructorOpt: hooked, prependFrames: [orig] } | ||
); | ||
inputTracing.ssjsInjection(sourceContext, sinkContext); | ||
} | ||
}) | ||
}); | ||
@@ -56,0 +58,0 @@ } |
@@ -108,3 +108,3 @@ /* | ||
const protectionRules = remoteSettings?.settings?.defend?.protectionRules | ||
const protectionRules = remoteSettings?.settings?.defend?.protectionRules; | ||
if (protectionRules) { | ||
@@ -111,0 +111,0 @@ updateFromProtectionRules(protectionRules); |
@@ -19,2 +19,3 @@ /* | ||
const { | ||
Rule, | ||
BLOCKING_MODES, | ||
@@ -30,3 +31,9 @@ ProtectRuleMode: { OFF }, | ||
// The sink instrumentation for this rule is in `protect/lib/input-tracing/install/child-process.js | ||
const getRuleResults = function(obj, prop) { | ||
return obj[prop] || (obj[prop] = []); | ||
}; | ||
// Semantic analysis currently shares instrumentation with the input-tracing sinks. | ||
// See files in protect/lib/input-tracing/install/. | ||
module.exports = function(core) { | ||
@@ -36,3 +43,2 @@ const { protect: { agentLib, semanticAnalysis, throwSecurityException } } = core; | ||
function handleResult(sourceContext, sinkContext, ruleId, mode, finding) { | ||
const sinkResults = sourceContext.findings.semanticResultsMap[ruleId]; | ||
const result = { | ||
@@ -45,3 +51,3 @@ blocked: false, | ||
sourceContext.findings.semanticResultsMap[ruleId] = sinkResults ? [...sinkResults, result] : [result]; | ||
getRuleResults(sourceContext.findings.semanticResultsMap, ruleId).push(result); | ||
@@ -57,4 +63,3 @@ if (BLOCKING_MODES.includes(mode)) { | ||
semanticAnalysis.handleCmdInjectionSemanticDangerous = function(sourceContext, sinkContext) { | ||
const ruleId = 'cmd-injection-semantic-dangerous-paths'; | ||
const mode = sourceContext.policy[ruleId]; | ||
const mode = sourceContext.policy[Rule.CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS]; | ||
@@ -66,3 +71,3 @@ if (mode == OFF) return; | ||
if (result) { | ||
handleResult(sourceContext, sinkContext, ruleId, mode); | ||
handleResult(sourceContext, sinkContext, Rule.CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS, mode); | ||
} | ||
@@ -72,4 +77,3 @@ }; | ||
semanticAnalysis.handleCmdInjectionSemanticChainedCommands = function(sourceContext, sinkContext) { | ||
const ruleId = 'cmd-injection-semantic-chained-commands'; | ||
const mode = sourceContext.policy[ruleId]; | ||
const mode = sourceContext.policy[Rule.CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS]; | ||
@@ -81,3 +85,3 @@ if (mode == OFF) return; | ||
if (indexOfChaining != -1) { | ||
handleResult(sourceContext, sinkContext, ruleId, mode); | ||
handleResult(sourceContext, sinkContext, Rule.CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS, mode); | ||
} | ||
@@ -87,4 +91,3 @@ }; | ||
semanticAnalysis.handleCommandInjectionCommandBackdoors = function(sourceContext, sinkContext) { | ||
const ruleId = 'cmd-injection-command-backdoors'; | ||
const mode = sourceContext.policy[ruleId]; | ||
const mode = sourceContext.policy[Rule.CMD_INJECTION_COMMAND_BACKDOORS]; | ||
@@ -96,6 +99,18 @@ if (mode == OFF) return; | ||
if (finding) { | ||
handleResult(sourceContext, sinkContext, ruleId, mode, finding); | ||
handleResult(sourceContext, sinkContext, Rule.CMD_INJECTION_COMMAND_BACKDOORS, mode, finding); | ||
} | ||
}; | ||
semanticAnalysis.handlePathTraversalFileSecurityBypass = function(sourceContext, sinkContext) { | ||
const mode = sourceContext.policy[Rule.PATH_TRAVERSAL_SEMANTIC_FILE_SECURITY_BYPASS]; | ||
if (mode == OFF) return; | ||
if (agentLib.isDangerousPath(sinkContext.value, true)) { | ||
handleResult(sourceContext, sinkContext, Rule.PATH_TRAVERSAL_SEMANTIC_FILE_SECURITY_BYPASS, mode, { | ||
findings: { path: sinkContext.value } | ||
}); | ||
} | ||
}; | ||
return semanticAnalysis; | ||
@@ -132,16 +147,18 @@ }; | ||
!found && | ||
value && | ||
type === 'Value' && | ||
isString(value) && | ||
isBackdoorDetected(value, command) | ||
type === 'Value' && | ||
isBackdoorDetected(value, command) | ||
) { | ||
let key; | ||
key = inputType === InputType.HEADER ? obj.indexOf(command) - 1 : path[path.length - 1]; | ||
if (Number.isInteger(key) && obj[key]) { | ||
key = obj[key]; | ||
if (inputType === InputType.HEADER) { | ||
key = obj[path[0] - 1] | ||
} else { | ||
key = path[path.length - 1]; | ||
} | ||
path = path.length === 1 ? [] : Array.from(path).slice(0, path.length - 1); | ||
inputType = path.length > 1 ? InputType.JSON_VALUE : inputType; | ||
found = { key, inputType, path, value: command }; | ||
found = { | ||
key, | ||
inputType: path.length > 1 ? InputType.JSON_VALUE : inputType, | ||
path: path.slice(0, -1), | ||
value: command | ||
}; | ||
} | ||
@@ -148,0 +165,0 @@ }); |
{ | ||
"name": "@contrast/protect", | ||
"version": "1.5.0", | ||
"version": "1.6.0", | ||
"description": "Contrast service providing framework-agnostic Protect support", | ||
@@ -23,5 +23,5 @@ "license": "SEE LICENSE IN LICENSE", | ||
"@contrast/agent-lib": "^5.1.0", | ||
"@contrast/common": "1.1.1", | ||
"@contrast/core": "1.4.0", | ||
"@contrast/esm-hooks": "1.1.5", | ||
"@contrast/common": "1.1.2", | ||
"@contrast/core": "1.5.0", | ||
"@contrast/esm-hooks": "1.1.6", | ||
"@contrast/scopes": "1.1.1", | ||
@@ -28,0 +28,0 @@ "builtin-modules": "^3.2.0", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
165695
4213
+ Added@contrast/agentify@1.1.1(transitive)
+ Added@contrast/common@1.1.2(transitive)
+ Added@contrast/config@1.2.0(transitive)
+ Added@contrast/core@1.5.0(transitive)
+ Added@contrast/dep-hooks@1.0.5(transitive)
+ Added@contrast/esm-hooks@1.1.6(transitive)
+ Added@contrast/logger@1.1.0(transitive)
+ Added@contrast/reporter@1.4.0(transitive)
+ Added@contrast/rewriter@1.2.0(transitive)
- Removed@contrast/agentify@1.1.0(transitive)
- Removed@contrast/common@1.1.1(transitive)
- Removed@contrast/config@1.1.5(transitive)
- Removed@contrast/core@1.4.0(transitive)
- Removed@contrast/dep-hooks@1.0.4(transitive)
- Removed@contrast/esm-hooks@1.1.5(transitive)
- Removed@contrast/logger@1.0.4(transitive)
- Removed@contrast/reporter@1.3.0(transitive)
- Removed@contrast/rewriter@1.1.0(transitive)
Updated@contrast/common@1.1.2
Updated@contrast/core@1.5.0
Updated@contrast/esm-hooks@1.1.6