diff --git a/.github/workflows/appsec.yml b/.github/workflows/appsec.yml index f41b18f9d53..f7b07a02692 100644 --- a/.github/workflows/appsec.yml +++ b/.github/workflows/appsec.yml @@ -122,7 +122,7 @@ jobs: express: runs-on: ubuntu-latest env: - PLUGINS: express|body-parser|cookie-parser + PLUGINS: express|body-parser|cookie-parser|handlebars steps: - uses: actions/checkout@v4 - uses: ./.github/actions/node/setup diff --git a/packages/datadog-instrumentations/src/handlebars.js b/packages/datadog-instrumentations/src/handlebars.js new file mode 100644 index 00000000000..4acb1a6c89c --- /dev/null +++ b/packages/datadog-instrumentations/src/handlebars.js @@ -0,0 +1,22 @@ +'use strict' + +const shimmer = require('../../datadog-shimmer') +const { channel, addHook } = require('./helpers/instrument') + +const handlebarsReadCh = channel('datadog:handlebars:compile:start') + +function wrapCompile (compile) { + return function () { + if (handlebarsReadCh.hasSubscribers) { + const source = arguments[0] + handlebarsReadCh.publish({ source }) + } + return compile.apply(this, arguments) + } +} + +addHook({ name: 'handlebars', file: 'dist/cjs/handlebars/compiler/compiler.js', versions: ['>=4.0.0'] }, compiler => { + shimmer.wrap(compiler, 'compile', wrapCompile) + shimmer.wrap(compiler, 'precompile', wrapCompile) + return compiler +}) diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index 62d45e37008..5d8eed7292e 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -51,6 +51,7 @@ module.exports = { 'generic-pool': () => require('../generic-pool'), graphql: () => require('../graphql'), grpc: () => require('../grpc'), + handlebars: () => require('../handlebars'), hapi: () => require('../hapi'), http: () => require('../http'), http2: () => require('../http2'), diff --git a/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js b/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js index 36f6036cf54..c1608ae1261 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js @@ -15,6 +15,7 @@ module.exports = { PATH_TRAVERSAL_ANALYZER: require('./path-traversal-analyzer'), SQL_INJECTION_ANALYZER: require('./sql-injection-analyzer'), SSRF: require('./ssrf-analyzer'), + TEMPLATE_INJECTION_ANALYZER: require('./template-injection-analyzer'), UNVALIDATED_REDIRECT_ANALYZER: require('./unvalidated-redirect-analyzer'), WEAK_CIPHER_ANALYZER: require('./weak-cipher-analyzer'), WEAK_HASH_ANALYZER: require('./weak-hash-analyzer'), diff --git a/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js new file mode 100644 index 00000000000..b28eca710c3 --- /dev/null +++ b/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js @@ -0,0 +1,16 @@ +'use strict' + +const InjectionAnalyzer = require('./injection-analyzer') +const { TEMPLATE_INJECTION } = require('../vulnerabilities') + +class TemplateInjectionAnalyzer extends InjectionAnalyzer { + constructor () { + super(TEMPLATE_INJECTION) + } + + onConfigure () { + this.addSub('datadog:handlebars:compile:start', ({ source }) => this.analyze(source)) + } +} + +module.exports = new TemplateInjectionAnalyzer() diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities.js b/packages/dd-trace/src/appsec/iast/vulnerabilities.js index 790ec6c5db9..90287c27d91 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities.js @@ -13,6 +13,7 @@ module.exports = { PATH_TRAVERSAL: 'PATH_TRAVERSAL', SQL_INJECTION: 'SQL_INJECTION', SSRF: 'SSRF', + TEMPLATE_INJECTION: 'TEMPLATE_INJECTION', UNVALIDATED_REDIRECT: 'UNVALIDATED_REDIRECT', WEAK_CIPHER: 'WEAK_CIPHER', WEAK_HASH: 'WEAK_HASH', diff --git a/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.handlebars.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.handlebars.plugin.spec.js new file mode 100644 index 00000000000..029572b4c61 --- /dev/null +++ b/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.handlebars.plugin.spec.js @@ -0,0 +1,71 @@ +'use strict' + +const { prepareTestServerForIast } = require('../utils') +const { storage } = require('../../../../../datadog-core') +const iastContextFunctions = require('../../../../src/appsec/iast/iast-context') +const { newTaintedString } = require('../../../../src/appsec/iast/taint-tracking/operations') + +describe('template-injection-analyzer with handlebars', () => { + withVersions('handlebars', 'handlebars', version => { + let lib, badSource, goodSource + before(() => { + lib = require(`../../../../../../versions/handlebars@${version}`).get() + badSource = ` + {{#with "s" as |string|}} + {{#with "e"}} + {{#with split as |conslist|}} + {{this.pop}} + {{this.push (lookup string.sub "constructor")}} + {{this.pop}} + {{#with string.split as |codelist|}} + {{this.pop}} + {{this.push "return JSON.stringify(process.env);"}} + {{this.pop}} + {{#each conslist}} + {{#with (string.sub.apply 0 codelist)}} + {{this}} + {{/with}} + {{/each}} + {{/with}} + {{/with}} + {{/with}} + {{/with}} + ` + goodSource = '

{{name}}

' + }) + + describe('compile', () => { + prepareTestServerForIast('template injection analyzer', + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const command = newTaintedString(iastContext, badSource, 'param', 'Request') + const template = lib.compile(command) + template() + }, 'TEMPLATE_INJECTION') + + testThatRequestHasNoVulnerability(() => { + const template = lib.compile(goodSource) + template() + }, 'TEMPLATE_INJECTION') + }) + }) + + describe('precompile', () => { + prepareTestServerForIast('template injection analyzer', + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const command = newTaintedString(iastContext, badSource, 'param', 'Request') + lib.precompile(command) + }, 'TEMPLATE_INJECTION') + + testThatRequestHasNoVulnerability(() => { + lib.precompile(goodSource) + }, 'TEMPLATE_INJECTION') + }) + }) + }) +})