Skip to content

Commit 985cb1d

Browse files
authored
Template injection vulnerability detection in handlebars and pug (#4827)
* Template injection vulnerability detection in handlebars * template injection vulnerability detection in pug * fix lint and naming issues * create separate job for template injection * add support to registerPartial function * add tests for pug render function
1 parent 59e9a2a commit 985cb1d

13 files changed

Lines changed: 297 additions & 10 deletions

File tree

.github/workflows/appsec.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,17 @@ jobs:
264264
- uses: ./.github/actions/node/latest
265265
- run: yarn test:appsec:plugins:ci
266266
- uses: codecov/codecov-action@v3
267+
268+
template:
269+
runs-on: ubuntu-latest
270+
env:
271+
PLUGINS: handlebars|pug
272+
steps:
273+
- uses: actions/checkout@v4
274+
- uses: ./.github/actions/node/setup
275+
- uses: ./.github/actions/install
276+
- uses: ./.github/actions/node/oldest
277+
- run: yarn test:appsec:plugins:ci
278+
- uses: ./.github/actions/node/latest
279+
- run: yarn test:appsec:plugins:ci
280+
- uses: codecov/codecov-action@v3
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use strict'
2+
3+
const shimmer = require('../../datadog-shimmer')
4+
const { channel, addHook } = require('./helpers/instrument')
5+
6+
const handlebarsCompileCh = channel('datadog:handlebars:compile:start')
7+
const handlebarsRegisterPartialCh = channel('datadog:handlebars:register-partial:start')
8+
9+
function wrapCompile (compile) {
10+
return function wrappedCompile (source) {
11+
if (handlebarsCompileCh.hasSubscribers) {
12+
handlebarsCompileCh.publish({ source })
13+
}
14+
15+
return compile.apply(this, arguments)
16+
}
17+
}
18+
19+
function wrapRegisterPartial (registerPartial) {
20+
return function wrappedRegisterPartial (name, partial) {
21+
if (handlebarsRegisterPartialCh.hasSubscribers) {
22+
handlebarsRegisterPartialCh.publish({ partial })
23+
}
24+
25+
return registerPartial.apply(this, arguments)
26+
}
27+
}
28+
29+
addHook({ name: 'handlebars', file: 'dist/cjs/handlebars/compiler/compiler.js', versions: ['>=4.0.0'] }, compiler => {
30+
shimmer.wrap(compiler, 'compile', wrapCompile)
31+
shimmer.wrap(compiler, 'precompile', wrapCompile)
32+
33+
return compiler
34+
})
35+
36+
addHook({ name: 'handlebars', file: 'dist/cjs/handlebars/base.js', versions: ['>=4.0.0'] }, base => {
37+
shimmer.wrap(base.HandlebarsEnvironment.prototype, 'registerPartial', wrapRegisterPartial)
38+
39+
return base
40+
})

packages/datadog-instrumentations/src/helpers/hooks.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ module.exports = {
5151
'generic-pool': () => require('../generic-pool'),
5252
graphql: () => require('../graphql'),
5353
grpc: () => require('../grpc'),
54+
handlebars: () => require('../handlebars'),
5455
hapi: () => require('../hapi'),
5556
http: () => require('../http'),
5657
http2: () => require('../http2'),
@@ -105,6 +106,7 @@ module.exports = {
105106
'promise-js': () => require('../promise-js'),
106107
promise: () => require('../promise'),
107108
protobufjs: () => require('../protobufjs'),
109+
pug: () => require('../pug'),
108110
q: () => require('../q'),
109111
qs: () => require('../qs'),
110112
redis: () => require('../redis'),
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict'
2+
3+
const shimmer = require('../../datadog-shimmer')
4+
const { channel, addHook } = require('./helpers/instrument')
5+
6+
const pugCompileCh = channel('datadog:pug:compile:start')
7+
8+
function wrapCompile (compile) {
9+
return function wrappedCompile (source) {
10+
if (pugCompileCh.hasSubscribers) {
11+
pugCompileCh.publish({ source })
12+
}
13+
14+
return compile.apply(this, arguments)
15+
}
16+
}
17+
18+
addHook({ name: 'pug', versions: ['>=2.0.4'] }, compiler => {
19+
shimmer.wrap(compiler, 'compile', wrapCompile)
20+
shimmer.wrap(compiler, 'compileClientWithDependenciesTracked', wrapCompile)
21+
22+
return compiler
23+
})

packages/dd-trace/src/appsec/iast/analyzers/analyzers.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module.exports = {
1515
PATH_TRAVERSAL_ANALYZER: require('./path-traversal-analyzer'),
1616
SQL_INJECTION_ANALYZER: require('./sql-injection-analyzer'),
1717
SSRF: require('./ssrf-analyzer'),
18+
TEMPLATE_INJECTION_ANALYZER: require('./template-injection-analyzer'),
1819
UNVALIDATED_REDIRECT_ANALYZER: require('./unvalidated-redirect-analyzer'),
1920
WEAK_CIPHER_ANALYZER: require('./weak-cipher-analyzer'),
2021
WEAK_HASH_ANALYZER: require('./weak-hash-analyzer'),
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict'
2+
3+
const InjectionAnalyzer = require('./injection-analyzer')
4+
const { TEMPLATE_INJECTION } = require('../vulnerabilities')
5+
6+
class TemplateInjectionAnalyzer extends InjectionAnalyzer {
7+
constructor () {
8+
super(TEMPLATE_INJECTION)
9+
}
10+
11+
onConfigure () {
12+
this.addSub('datadog:handlebars:compile:start', ({ source }) => this.analyze(source))
13+
this.addSub('datadog:handlebars:register-partial:start', ({ partial }) => this.analyze(partial))
14+
this.addSub('datadog:pug:compile:start', ({ source }) => this.analyze(source))
15+
}
16+
}
17+
18+
module.exports = new TemplateInjectionAnalyzer()

packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/code-injection-sensitive-analyzer.js renamed to packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/tainted-range-based-sensitive-analyzer.js

File renamed without changes.

packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ const vulnerabilities = require('../../vulnerabilities')
55

66
const { contains, intersects, remove } = require('./range-utils')
77

8-
const codeInjectionSensitiveAnalyzer = require('./sensitive-analyzers/code-injection-sensitive-analyzer')
98
const commandSensitiveAnalyzer = require('./sensitive-analyzers/command-sensitive-analyzer')
109
const hardcodedPasswordAnalyzer = require('./sensitive-analyzers/hardcoded-password-analyzer')
1110
const headerSensitiveAnalyzer = require('./sensitive-analyzers/header-sensitive-analyzer')
1211
const jsonSensitiveAnalyzer = require('./sensitive-analyzers/json-sensitive-analyzer')
1312
const ldapSensitiveAnalyzer = require('./sensitive-analyzers/ldap-sensitive-analyzer')
1413
const sqlSensitiveAnalyzer = require('./sensitive-analyzers/sql-sensitive-analyzer')
14+
const taintedRangeBasedSensitiveAnalyzer = require('./sensitive-analyzers/tainted-range-based-sensitive-analyzer')
1515
const urlSensitiveAnalyzer = require('./sensitive-analyzers/url-sensitive-analyzer')
1616

1717
const { DEFAULT_IAST_REDACTION_NAME_PATTERN, DEFAULT_IAST_REDACTION_VALUE_PATTERN } = require('./sensitive-regex')
@@ -24,7 +24,8 @@ class SensitiveHandler {
2424
this._valuePattern = new RegExp(DEFAULT_IAST_REDACTION_VALUE_PATTERN, 'gmi')
2525

2626
this._sensitiveAnalyzers = new Map()
27-
this._sensitiveAnalyzers.set(vulnerabilities.CODE_INJECTION, codeInjectionSensitiveAnalyzer)
27+
this._sensitiveAnalyzers.set(vulnerabilities.CODE_INJECTION, taintedRangeBasedSensitiveAnalyzer)
28+
this._sensitiveAnalyzers.set(vulnerabilities.TEMPLATE_INJECTION, taintedRangeBasedSensitiveAnalyzer)
2829
this._sensitiveAnalyzers.set(vulnerabilities.COMMAND_INJECTION, commandSensitiveAnalyzer)
2930
this._sensitiveAnalyzers.set(vulnerabilities.NOSQL_MONGODB_INJECTION, jsonSensitiveAnalyzer)
3031
this._sensitiveAnalyzers.set(vulnerabilities.LDAP_INJECTION, ldapSensitiveAnalyzer)

packages/dd-trace/src/appsec/iast/vulnerabilities.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = {
1313
PATH_TRAVERSAL: 'PATH_TRAVERSAL',
1414
SQL_INJECTION: 'SQL_INJECTION',
1515
SSRF: 'SSRF',
16+
TEMPLATE_INJECTION: 'TEMPLATE_INJECTION',
1617
UNVALIDATED_REDIRECT: 'UNVALIDATED_REDIRECT',
1718
WEAK_CIPHER: 'WEAK_CIPHER',
1819
WEAK_HASH: 'WEAK_HASH',
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use strict'
2+
3+
const { prepareTestServerForIast } = require('../utils')
4+
const { storage } = require('../../../../../datadog-core')
5+
const iastContextFunctions = require('../../../../src/appsec/iast/iast-context')
6+
const { newTaintedString } = require('../../../../src/appsec/iast/taint-tracking/operations')
7+
8+
describe('template-injection-analyzer with handlebars', () => {
9+
withVersions('handlebars', 'handlebars', version => {
10+
let source
11+
before(() => {
12+
source = '<p>{{name}}</p>'
13+
})
14+
15+
describe('compile', () => {
16+
prepareTestServerForIast('template injection analyzer',
17+
(testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => {
18+
let lib
19+
beforeEach(() => {
20+
lib = require(`../../../../../../versions/handlebars@${version}`).get()
21+
})
22+
23+
testThatRequestHasVulnerability(() => {
24+
const store = storage.getStore()
25+
const iastContext = iastContextFunctions.getIastContext(store)
26+
const template = newTaintedString(iastContext, source, 'param', 'Request')
27+
lib.compile(template)
28+
}, 'TEMPLATE_INJECTION')
29+
30+
testThatRequestHasNoVulnerability(() => {
31+
lib.compile(source)
32+
}, 'TEMPLATE_INJECTION')
33+
})
34+
})
35+
36+
describe('precompile', () => {
37+
prepareTestServerForIast('template injection analyzer',
38+
(testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => {
39+
let lib
40+
beforeEach(() => {
41+
lib = require(`../../../../../../versions/handlebars@${version}`).get()
42+
})
43+
44+
testThatRequestHasVulnerability(() => {
45+
const store = storage.getStore()
46+
const iastContext = iastContextFunctions.getIastContext(store)
47+
const template = newTaintedString(iastContext, source, 'param', 'Request')
48+
lib.precompile(template)
49+
}, 'TEMPLATE_INJECTION')
50+
51+
testThatRequestHasNoVulnerability(() => {
52+
lib.precompile(source)
53+
}, 'TEMPLATE_INJECTION')
54+
})
55+
})
56+
57+
describe('registerPartial', () => {
58+
prepareTestServerForIast('template injection analyzer',
59+
(testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => {
60+
let lib
61+
beforeEach(() => {
62+
lib = require(`../../../../../../versions/handlebars@${version}`).get()
63+
})
64+
65+
testThatRequestHasVulnerability(() => {
66+
const store = storage.getStore()
67+
const iastContext = iastContextFunctions.getIastContext(store)
68+
const partial = newTaintedString(iastContext, source, 'param', 'Request')
69+
70+
lib.registerPartial('vulnerablePartial', partial)
71+
}, 'TEMPLATE_INJECTION')
72+
73+
testThatRequestHasNoVulnerability(() => {
74+
lib.registerPartial('vulnerablePartial', source)
75+
}, 'TEMPLATE_INJECTION')
76+
})
77+
})
78+
})
79+
})

0 commit comments

Comments
 (0)