-
Notifications
You must be signed in to change notification settings - Fork 312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[asm] IAST security controls #5117
base: master
Are you sure you want to change the base?
Changes from all commits
d5a52ea
c4c398c
9a1229c
dce2f18
ac1d502
4cc7895
6fe83f6
4e4d69e
90b9ff8
2d86aee
5ed8aa2
57548b0
610e216
ca0bbe5
b4a217e
0b0c292
af61bf9
fb1de25
384d526
fce89df
c324c58
767d2db
082ac75
4b948ad
d9c1393
1fdaf86
3b697d1
07dcc02
5556875
3f57dea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
'use strict' | ||
|
||
import childProcess from 'node:child_process' | ||
import express from 'express' | ||
import { sanitize } from './sanitizer.mjs' | ||
import sanitizeDefault from './sanitizer-default.mjs' | ||
import { validate, validateNotConfigured } from './validator.mjs' | ||
|
||
const app = express() | ||
const port = process.env.APP_PORT || 3000 | ||
|
||
app.get('/cmdi-s-secure', (req, res) => { | ||
const command = sanitize(req.query.command) | ||
try { | ||
childProcess.execSync(command) | ||
} catch (e) { | ||
// ignore | ||
} | ||
|
||
res.end() | ||
}) | ||
|
||
app.get('/cmdi-s-secure-default', (req, res) => { | ||
const command = sanitizeDefault(req.query.command) | ||
try { | ||
childProcess.execSync(command) | ||
} catch (e) { | ||
// ignore | ||
} | ||
|
||
res.end() | ||
}) | ||
|
||
app.get('/cmdi-iv-insecure', (req, res) => { | ||
if (validateNotConfigured(req.query.command)) { | ||
childProcess.execSync(req.query.command) | ||
} | ||
|
||
res.end() | ||
}) | ||
|
||
app.get('/cmdi-iv-secure', (req, res) => { | ||
if (validate(req.query.command)) { | ||
childProcess.execSync(req.query.command) | ||
} | ||
|
||
res.end() | ||
}) | ||
|
||
app.listen(port, () => { | ||
process.send({ port }) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
'use strict' | ||
|
||
function sanitizeDefault (input) { | ||
return input | ||
} | ||
|
||
export default sanitizeDefault |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
'use strict' | ||
|
||
export function sanitize (input) { | ||
return input | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
'use strict' | ||
|
||
export function validate (input) { | ||
return true | ||
} | ||
|
||
export function validateNotConfigured (input) { | ||
return true | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
'use strict' | ||
|
||
const { createSandbox, spawnProc, FakeAgent } = require('../helpers') | ||
const path = require('path') | ||
const getPort = require('get-port') | ||
const Axios = require('axios') | ||
const { assert } = require('chai') | ||
|
||
describe('ESM Security controls', () => { | ||
let axios, sandbox, cwd, appPort, appFile, agent, proc | ||
|
||
before(async function () { | ||
this.timeout(process.platform === 'win32' ? 90000 : 30000) | ||
sandbox = await createSandbox(['express']) | ||
appPort = await getPort() | ||
cwd = sandbox.folder | ||
appFile = path.join(cwd, 'appsec', 'esm-security-controls', 'index.mjs') | ||
|
||
axios = Axios.create({ | ||
baseURL: `http://localhost:${appPort}` | ||
}) | ||
}) | ||
|
||
after(async function () { | ||
await sandbox.remove() | ||
}) | ||
const nodeOptions = '--import dd-trace/initialize.mjs' | ||
|
||
describe('with --import', () => { | ||
beforeEach(async () => { | ||
agent = await new FakeAgent().start() | ||
|
||
proc = await spawnProc(appFile, { | ||
cwd, | ||
env: { | ||
DD_TRACE_AGENT_PORT: agent.port, | ||
APP_PORT: appPort, | ||
DD_IAST_ENABLED: 'true', | ||
DD_IAST_REQUEST_SAMPLING: '100', | ||
// eslint-disable-next-line no-multi-str | ||
DD_IAST_SECURITY_CONTROLS_CONFIGURATION: '\ | ||
SANITIZER:COMMAND_INJECTION:appsec/esm-security-controls/sanitizer.mjs:sanitize;\ | ||
SANITIZER:COMMAND_INJECTION:appsec/esm-security-controls/sanitizer-default.mjs;\ | ||
INPUT_VALIDATOR:*:appsec/esm-security-controls/validator.mjs:validate', | ||
NODE_OPTIONS: nodeOptions | ||
} | ||
}) | ||
}) | ||
|
||
afterEach(async () => { | ||
proc.kill() | ||
await agent.stop() | ||
}) | ||
|
||
it('test endpoint with iv not configured have COMMAND_INJECTION vulnerability', async function () { | ||
await axios.get('/cmdi-iv-insecure?command=ls -la') | ||
|
||
await agent.assertMessageReceived(({ payload }) => { | ||
const spans = payload.flatMap(p => p.filter(span => span.name === 'express.request')) | ||
spans.forEach(span => { | ||
assert.property(span.meta, '_dd.iast.json') | ||
assert.include(span.meta['_dd.iast.json'], '"COMMAND_INJECTION"') | ||
}) | ||
}, null, 1, true) | ||
}) | ||
|
||
it('test endpoint sanitizer do not have COMMAND_INJECTION vulnerability', async () => { | ||
await axios.get('/cmdi-s-secure?command=ls -la') | ||
|
||
await agent.assertMessageReceived(({ payload }) => { | ||
const spans = payload.flatMap(p => p.filter(span => span.name === 'express.request')) | ||
spans.forEach(span => { | ||
assert.notProperty(span.meta, '_dd.iast.json') | ||
assert.property(span.metrics, '_dd.iast.telemetry.suppressed.vulnerabilities.command_injection') | ||
}) | ||
}, null, 1, true) | ||
}) | ||
|
||
it('test endpoint with default sanitizer do not have COMMAND_INJECTION vulnerability', async () => { | ||
await axios.get('/cmdi-s-secure-default?command=ls -la') | ||
|
||
await agent.assertMessageReceived(({ payload }) => { | ||
const spans = payload.flatMap(p => p.filter(span => span.name === 'express.request')) | ||
spans.forEach(span => { | ||
assert.notProperty(span.meta, '_dd.iast.json') | ||
assert.property(span.metrics, '_dd.iast.telemetry.suppressed.vulnerabilities.command_injection') | ||
}) | ||
}, null, 1, true) | ||
}) | ||
|
||
it('test endpoint with iv do not have COMMAND_INJECTION vulnerability', async () => { | ||
await axios.get('/cmdi-iv-secure?command=ls -la') | ||
|
||
await agent.assertMessageReceived(({ payload }) => { | ||
const spans = payload.flatMap(p => p.filter(span => span.name === 'express.request')) | ||
spans.forEach(span => { | ||
assert.notProperty(span.meta, '_dd.iast.json') | ||
assert.property(span.metrics, '_dd.iast.telemetry.suppressed.vulnerabilities.command_injection') | ||
}) | ||
}, null, 1, true) | ||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -83,8 +83,8 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer { | |
} | ||
} | ||
|
||
_areRangesVulnerable () { | ||
return true | ||
_areRangesVulnerable (ranges) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we have this method implemented in more analyzers, like code injection analyzer. I think that this change is necessary for all of them. |
||
return ranges?.length > 0 | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,11 +10,14 @@ const { | |
getVulnerabilityCallSiteFrames, | ||
replaceCallSiteFromSourceMap | ||
} = require('../vulnerability-reporter') | ||
const { getMarkFromVulnerabilityType } = require('../taint-tracking/secure-marks') | ||
const { SUPPRESSED_VULNERABILITIES } = require('../telemetry/iast-metric') | ||
|
||
class Analyzer extends SinkIastPlugin { | ||
constructor (type) { | ||
super() | ||
this._type = type | ||
this._secureMark = getMarkFromVulnerabilityType(type) | ||
} | ||
|
||
_isVulnerable (value, context) { | ||
|
@@ -25,6 +28,11 @@ class Analyzer extends SinkIastPlugin { | |
return false | ||
} | ||
|
||
_isRangeSecure (range) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does checking the secure marks out of injection-analizer make sense? |
||
const { secureMarks } = range | ||
return (secureMarks & this._secureMark) === this._secureMark | ||
} | ||
|
||
_report (value, context, meta) { | ||
const evidence = this._getEvidence(value, context, meta) | ||
this._reportEvidence(value, context, evidence) | ||
|
@@ -149,6 +157,17 @@ class Analyzer extends SinkIastPlugin { | |
return hash | ||
} | ||
|
||
_getSuppressedMetricTag () { | ||
if (!this._suppressedMetricTag) { | ||
this._suppressedMetricTag = SUPPRESSED_VULNERABILITIES.formatTags(this._type)[0] | ||
} | ||
return this._suppressedMetricTag | ||
} | ||
|
||
_incrementSuppressedMetric (iastContext) { | ||
SUPPRESSED_VULNERABILITIES.inc(iastContext, this._getSuppressedMetricTag()) | ||
} | ||
|
||
addSub (iastSubOrChannelName, handler) { | ||
const iastSub = typeof iastSubOrChannelName === 'string' | ||
? { channelName: iastSubOrChannelName } | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.