diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index 4ea35f50218..13dda1145bf 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -96,6 +96,7 @@ module.exports = { 'node:https': () => require('../http'), 'node:net': () => require('../net'), 'node:url': () => require('../url'), + 'node:vm': () => require('../vm'), nyc: () => require('../nyc'), oracledb: () => require('../oracledb'), openai: () => require('../openai'), @@ -122,6 +123,7 @@ module.exports = { undici: () => require('../undici'), url: () => require('../url'), vitest: { esmFirst: true, fn: () => require('../vitest') }, + vm: () => require('../vm'), when: () => require('../when'), winston: () => require('../winston'), workerpool: () => require('../mocha') diff --git a/packages/datadog-instrumentations/src/vm.js b/packages/datadog-instrumentations/src/vm.js new file mode 100644 index 00000000000..157cdac35f5 --- /dev/null +++ b/packages/datadog-instrumentations/src/vm.js @@ -0,0 +1,41 @@ +'use strict' + +const { channel, addHook } = require('./helpers/instrument') +const shimmer = require('../../datadog-shimmer') +const names = ['vm', 'node:vm'] + +const createScriptStartChannel = channel('datadog:vm:run-script:start') + +addHook({ name: names }, function (vm) { + vm.Script = class extends vm.Script { + constructor (code) { + super(...arguments) + this.code = code + } + } + + shimmer.wrap(vm.Script.prototype, 'runInContext', wrapVMMethod(1)) + shimmer.wrap(vm.Script.prototype, 'runInNewContext', wrapVMMethod()) + shimmer.wrap(vm.Script.prototype, 'runInThisContext', wrapVMMethod()) + + shimmer.wrap(vm, 'runInContext', wrapVMMethod()) + shimmer.wrap(vm, 'runInNewContext', wrapVMMethod()) + shimmer.wrap(vm, 'runInThisContext', wrapVMMethod()) + shimmer.wrap(vm, 'compileFunction', wrapVMMethod()) + + return vm +}) + +function wrapVMMethod (codeIndex = 0) { + return function wrap (original) { + return function wrapped () { + const code = arguments[codeIndex] ? arguments[codeIndex] : this.code + + if (createScriptStartChannel.hasSubscribers && code) { + createScriptStartChannel.publish({ code }) + } + + return original.apply(this, arguments) + } + } +} diff --git a/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js index 3741c12ef8f..c379c2ae8fe 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js @@ -10,6 +10,7 @@ class CodeInjectionAnalyzer extends InjectionAnalyzer { onConfigure () { this.addSub('datadog:eval:call', ({ script }) => this.analyze(script)) + this.addSub('datadog:vm:run-script:start', ({ code }) => this.analyze(code)) } _areRangesVulnerable () { diff --git a/packages/dd-trace/test/appsec/iast/analyzers/code-injection-analyzer.express.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/code-injection-analyzer.express.plugin.spec.js index 64e15b9161b..44cfd113c48 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/code-injection-analyzer.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/code-injection-analyzer.express.plugin.spec.js @@ -13,64 +13,364 @@ const iastContextFunctions = require('../../../../src/appsec/iast/iast-context') describe('Code injection vulnerability', () => { withVersions('express', 'express', '>4.18.0', version => { - let i = 0 - let evalFunctionsPath - - beforeEach(() => { - evalFunctionsPath = path.join(os.tmpdir(), `eval-methods-${i++}.js`) - fs.copyFileSync( - path.join(__dirname, 'resources', 'eval-methods.js'), - evalFunctionsPath - ) - }) + describe('Eval', () => { + let i = 0 + let evalFunctionsPath + + beforeEach(() => { + evalFunctionsPath = path.join(os.tmpdir(), `eval-methods-${i++}.js`) + fs.copyFileSync( + path.join(__dirname, 'resources', 'eval-methods.js'), + evalFunctionsPath + ) + }) + + afterEach(() => { + fs.unlinkSync(evalFunctionsPath) + clearCache() + }) + + prepareTestServerForIastInExpress('in express', version, + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + testThatRequestHasVulnerability({ + fn: (req, res) => { + res.send(require(evalFunctionsPath).runEval(req.query.script, 'test-result')) + }, + vulnerability: 'CODE_INJECTION', + makeRequest: (done, config) => { + axios.get(`http://localhost:${config.port}/?script=1%2B2`) + .then(res => { + expect(res.data).to.equal('test-result') + }) + .catch(done) + } + }) + + testThatRequestHasVulnerability({ + fn: (req, res) => { + const source = '1 + 2' + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + + res.send(require(evalFunctionsPath).runEval(str, 'test-result')) + }, + vulnerability: 'CODE_INJECTION', + testDescription: 'Should detect CODE_INJECTION vulnerability with DB source' + }) - afterEach(() => { - fs.unlinkSync(evalFunctionsPath) - clearCache() + testThatRequestHasNoVulnerability({ + fn: (req, res) => { + res.send('' + require(evalFunctionsPath).runFakeEval(req.query.script)) + }, + vulnerability: 'CODE_INJECTION', + makeRequest: (done, config) => { + axios.get(`http://localhost:${config.port}/?script=1%2B2`).catch(done) + } + }) + + testThatRequestHasNoVulnerability((req, res) => { + res.send('' + require(evalFunctionsPath).runEval('1 + 2')) + }, 'CODE_INJECTION') + }) }) - prepareTestServerForIastInExpress('in express', version, - (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { - testThatRequestHasVulnerability({ - fn: (req, res) => { - res.send(require(evalFunctionsPath).runEval(req.query.script, 'test-result')) - }, - vulnerability: 'CODE_INJECTION', - makeRequest: (done, config) => { - axios.get(`http://localhost:${config.port}/?script=1%2B2`) - .then(res => { - expect(res.data).to.equal('test-result') - }) - .catch(done) - } + describe('Node:vm', () => { + let context, vm + + beforeEach(() => { + vm = require('vm') + context = {} + vm.createContext(context) + }) + + afterEach(() => { + vm = null + context = null + }) + + prepareTestServerForIastInExpress('runInContext in express', version, + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + testThatRequestHasVulnerability({ + fn: (req, res) => { + const result = vm.runInContext(req.query.script, context) + + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + makeRequest: (done, config) => { + axios.get(`http://localhost:${config.port}/?script=1%2B2`) + .then(res => { + expect(res.data).to.equal(3) + }) + .catch(done) + } + }) + + testThatRequestHasVulnerability({ + fn: (req, res) => { + const source = '1 + 2' + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + + const result = vm.runInContext(str, context) + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + testDescription: 'Should detect CODE_INJECTION vulnerability with DB source' + }) + + testThatRequestHasNoVulnerability((req, res) => { + const result = vm.runInContext('1 + 2', context) + + res.send(`${result}`) + }, 'CODE_INJECTION') }) - testThatRequestHasVulnerability({ - fn: (req, res) => { - const source = '1 + 2' - const store = storage.getStore() - const iastContext = iastContextFunctions.getIastContext(store) - const str = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) - - res.send(require(evalFunctionsPath).runEval(str, 'test-result')) - }, - vulnerability: 'CODE_INJECTION', - testDescription: 'Should detect CODE_INJECTION vulnerability with DB source' + prepareTestServerForIastInExpress('Script runInContext in express', version, + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + testThatRequestHasVulnerability({ + fn: (req, res) => { + const script = new vm.Script(req.query.script) + const result = script.runInContext(context) + + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + makeRequest: (done, config) => { + axios.get(`http://localhost:${config.port}/?script=1%2B2`) + .then(res => { + expect(res.data).to.equal(3) + }) + .catch(done) + } + }) + + testThatRequestHasVulnerability({ + fn: (req, res) => { + const source = '1 + 2' + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + + const script = new vm.Script(str) + const result = script.runInContext(context) + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + testDescription: 'Should detect CODE_INJECTION vulnerability with DB source' + }) + + testThatRequestHasNoVulnerability((req, res) => { + const script = new vm.Script('1 + 2') + const result = script.runInContext(context) + + res.send(`${result}`) + }, 'CODE_INJECTION') }) - testThatRequestHasNoVulnerability({ - fn: (req, res) => { - res.send('' + require(evalFunctionsPath).runFakeEval(req.query.script)) - }, - vulnerability: 'CODE_INJECTION', - makeRequest: (done, config) => { - axios.get(`http://localhost:${config.port}/?script=1%2B2`).catch(done) - } + prepareTestServerForIastInExpress('runInNewContext in express', version, + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + testThatRequestHasVulnerability({ + fn: (req, res) => { + const result = vm.runInNewContext(req.query.script) + + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + makeRequest: (done, config) => { + axios.get(`http://localhost:${config.port}/?script=1%2B2`) + .then(res => { + expect(res.data).to.equal(3) + }) + .catch(done) + } + }) + + testThatRequestHasVulnerability({ + fn: (req, res) => { + const source = '1 + 2' + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + + const result = vm.runInNewContext(str) + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + testDescription: 'Should detect CODE_INJECTION vulnerability with DB source' + }) + + testThatRequestHasNoVulnerability((req, res) => { + const result = vm.runInNewContext('1 + 2') + + res.send(`${result}`) + }, 'CODE_INJECTION') }) - testThatRequestHasNoVulnerability((req, res) => { - res.send('' + require(evalFunctionsPath).runEval('1 + 2')) - }, 'CODE_INJECTION') - }) + prepareTestServerForIastInExpress('Script runInNewContext in express', version, + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + testThatRequestHasVulnerability({ + fn: (req, res) => { + const script = new vm.Script(req.query.script) + const result = script.runInNewContext() + + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + makeRequest: (done, config) => { + axios.get(`http://localhost:${config.port}/?script=1%2B2`) + .then(res => { + expect(res.data).to.equal(3) + }) + .catch(done) + } + }) + + testThatRequestHasVulnerability({ + fn: (req, res) => { + const source = '1 + 2' + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + + const script = new vm.Script(str) + const result = script.runInNewContext() + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + testDescription: 'Should detect CODE_INJECTION vulnerability with DB source' + }) + + testThatRequestHasNoVulnerability((req, res) => { + const script = new vm.Script('1 + 2') + const result = script.runInNewContext() + + res.send(`${result}`) + }, 'CODE_INJECTION') + }) + + prepareTestServerForIastInExpress('runInThisContext in express', version, + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + testThatRequestHasVulnerability({ + fn: (req, res) => { + const result = vm.runInThisContext(req.query.script) + + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + makeRequest: (done, config) => { + axios.get(`http://localhost:${config.port}/?script=1%2B2`) + .then(res => { + expect(res.data).to.equal(3) + }) + .catch(done) + } + }) + + testThatRequestHasVulnerability({ + fn: (req, res) => { + const source = '1 + 2' + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + + const result = vm.runInThisContext(str) + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + testDescription: 'Should detect CODE_INJECTION vulnerability with DB source' + }) + + testThatRequestHasNoVulnerability((req, res) => { + const result = vm.runInThisContext('1 + 2') + + res.send(`${result}`) + }, 'CODE_INJECTION') + }) + + prepareTestServerForIastInExpress('Script runInThisContext in express', version, + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + testThatRequestHasVulnerability({ + fn: (req, res) => { + const script = new vm.Script(req.query.script) + const result = script.runInThisContext() + + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + makeRequest: (done, config) => { + axios.get(`http://localhost:${config.port}/?script=1%2B2`) + .then(res => { + expect(res.data).to.equal(3) + }) + .catch(done) + } + }) + + testThatRequestHasVulnerability({ + fn: (req, res) => { + const source = '1 + 2' + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + + const script = new vm.Script(str) + const result = script.runInThisContext() + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + testDescription: 'Should detect CODE_INJECTION vulnerability with DB source' + }) + + testThatRequestHasNoVulnerability((req, res) => { + const script = new vm.Script('1 + 2') + const result = script.runInThisContext() + + res.send(`${result}`) + }, 'CODE_INJECTION') + }) + + prepareTestServerForIastInExpress('compileFunction in express', version, + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + testThatRequestHasVulnerability({ + fn: (req, res) => { + const fn = vm.compileFunction(req.query.script) + const result = fn() + + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + makeRequest: (done, config) => { + axios.get(`http://localhost:${config.port}/?script=return%201%2B2`) + .then(res => { + expect(res.data).to.equal(3) + }) + .catch(done) + } + }) + + testThatRequestHasVulnerability({ + fn: (req, res) => { + const source = '1 + 2' + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + + const result = vm.runInThisContext(str) + res.send(`${result}`) + }, + vulnerability: 'CODE_INJECTION', + testDescription: 'Should detect CODE_INJECTION vulnerability with DB source' + }) + + testThatRequestHasNoVulnerability((req, res) => { + const result = vm.runInThisContext('1 + 2') + + res.send(`${result}`) + }, 'CODE_INJECTION') + }) + }) }) })