diff --git a/.github/workflows/appsec.yml b/.github/workflows/appsec.yml index 17a4e66f15c..04a0c61ac00 100644 --- a/.github/workflows/appsec.yml +++ b/.github/workflows/appsec.yml @@ -279,3 +279,17 @@ jobs: - uses: ./.github/actions/node/latest - run: yarn test:appsec:plugins:ci - uses: codecov/codecov-action@v3 + + template: + runs-on: ubuntu-latest + env: + PLUGINS: node-serialize + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/node/setup + - uses: ./.github/actions/install + - uses: ./.github/actions/node/oldest + - run: yarn test:appsec:plugins:ci + - uses: ./.github/actions/node/latest + - run: yarn test:appsec:plugins:ci + - uses: codecov/codecov-action@v3 diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index 4ea35f50218..5a27eebb9d7 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -88,6 +88,7 @@ module.exports = { mysql2: () => require('../mysql2'), net: () => require('../net'), next: () => require('../next'), + 'node-serialize': () => require('../node-serialize'), 'node:child_process': () => require('../child_process'), 'node:crypto': () => require('../crypto'), 'node:dns': () => require('../dns'), diff --git a/packages/datadog-instrumentations/src/node-serialize.js b/packages/datadog-instrumentations/src/node-serialize.js new file mode 100644 index 00000000000..ae7f8191cd7 --- /dev/null +++ b/packages/datadog-instrumentations/src/node-serialize.js @@ -0,0 +1,22 @@ +'use strict' + +const shimmer = require('../../datadog-shimmer') +const { channel, addHook } = require('./helpers/instrument') + +const nodeUnserializeCh = channel('datadog:node-serialize:unserialize:start') + +function wrapUnserialize (serialize) { + return function wrappedUnserialize (obj) { + if (nodeUnserializeCh.hasSubscribers) { + nodeUnserializeCh.publish({ obj }) + } + + return serialize.apply(this, arguments) + } +} + +addHook({ name: 'node-serialize', versions: ['0'] }, serialize => { + shimmer.wrap(serialize, 'unserialize', wrapUnserialize) + + return serialize +}) diff --git a/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js b/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js index c1608ae1261..0cc8fbfc274 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js @@ -17,6 +17,7 @@ module.exports = { SSRF: require('./ssrf-analyzer'), TEMPLATE_INJECTION_ANALYZER: require('./template-injection-analyzer'), UNVALIDATED_REDIRECT_ANALYZER: require('./unvalidated-redirect-analyzer'), + UNTRUSTED_DESERIALIZATION_ANALYZER: require('./untrusted-deserialization-analyzer'), WEAK_CIPHER_ANALYZER: require('./weak-cipher-analyzer'), WEAK_HASH_ANALYZER: require('./weak-hash-analyzer'), WEAK_RANDOMNESS_ANALYZER: require('./weak-randomness-analyzer'), diff --git a/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js new file mode 100644 index 00000000000..08ff0d025d3 --- /dev/null +++ b/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js @@ -0,0 +1,20 @@ +'use strict' + +const InjectionAnalyzer = require('./injection-analyzer') +const { UNTRUSTED_DESERIALIZATION } = require('../vulnerabilities') + +class UntrustedDeserializationAnalyzer extends InjectionAnalyzer { + constructor () { + super(UNTRUSTED_DESERIALIZATION) + } + + onConfigure () { + this.addSub('datadog:node-serialize:unserialize:start', ({ obj }) => this.analyze(obj)) + } + + _areRangesVulnerable () { + return true + } +} + +module.exports = new UntrustedDeserializationAnalyzer() diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js index 9c6c48dbf54..219db3bf54f 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js @@ -25,19 +25,20 @@ class SensitiveHandler { this._sensitiveAnalyzers = new Map() this._sensitiveAnalyzers.set(vulnerabilities.CODE_INJECTION, taintedRangeBasedSensitiveAnalyzer) - this._sensitiveAnalyzers.set(vulnerabilities.TEMPLATE_INJECTION, taintedRangeBasedSensitiveAnalyzer) this._sensitiveAnalyzers.set(vulnerabilities.COMMAND_INJECTION, commandSensitiveAnalyzer) - this._sensitiveAnalyzers.set(vulnerabilities.NOSQL_MONGODB_INJECTION, jsonSensitiveAnalyzer) + this._sensitiveAnalyzers.set(vulnerabilities.HARDCODED_PASSWORD, (evidence) => { + return hardcodedPasswordAnalyzer(evidence, this._valuePattern) + }) + this._sensitiveAnalyzers.set(vulnerabilities.HEADER_INJECTION, (evidence) => { + return headerSensitiveAnalyzer(evidence, this._namePattern, this._valuePattern) + }) this._sensitiveAnalyzers.set(vulnerabilities.LDAP_INJECTION, ldapSensitiveAnalyzer) + this._sensitiveAnalyzers.set(vulnerabilities.NOSQL_MONGODB_INJECTION, jsonSensitiveAnalyzer) this._sensitiveAnalyzers.set(vulnerabilities.SQL_INJECTION, sqlSensitiveAnalyzer) this._sensitiveAnalyzers.set(vulnerabilities.SSRF, urlSensitiveAnalyzer) + this._sensitiveAnalyzers.set(vulnerabilities.TEMPLATE_INJECTION, taintedRangeBasedSensitiveAnalyzer) this._sensitiveAnalyzers.set(vulnerabilities.UNVALIDATED_REDIRECT, urlSensitiveAnalyzer) - this._sensitiveAnalyzers.set(vulnerabilities.HEADER_INJECTION, (evidence) => { - return headerSensitiveAnalyzer(evidence, this._namePattern, this._valuePattern) - }) - this._sensitiveAnalyzers.set(vulnerabilities.HARDCODED_PASSWORD, (evidence) => { - return hardcodedPasswordAnalyzer(evidence, this._valuePattern) - }) + this._sensitiveAnalyzers.set(vulnerabilities.UNTRUSTED_DESERIALIZATION, taintedRangeBasedSensitiveAnalyzer) } isSensibleName (name) { diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities.js b/packages/dd-trace/src/appsec/iast/vulnerabilities.js index 90287c27d91..b504742d63b 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities.js @@ -15,6 +15,7 @@ module.exports = { SSRF: 'SSRF', TEMPLATE_INJECTION: 'TEMPLATE_INJECTION', UNVALIDATED_REDIRECT: 'UNVALIDATED_REDIRECT', + UNTRUSTED_DESERIALIZATION: 'UNTRUSTED_DESERIALIZATION', WEAK_CIPHER: 'WEAK_CIPHER', WEAK_HASH: 'WEAK_HASH', WEAK_RANDOMNESS: 'WEAK_RANDOMNESS', diff --git a/packages/dd-trace/test/appsec/iast/analyzers/untrusted-deserialization-analyzer.node-serialize.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/untrusted-deserialization-analyzer.node-serialize.plugin.spec.js new file mode 100644 index 00000000000..77fc33fff9f --- /dev/null +++ b/packages/dd-trace/test/appsec/iast/analyzers/untrusted-deserialization-analyzer.node-serialize.plugin.spec.js @@ -0,0 +1,45 @@ +'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') +const { SQL_ROW_VALUE } = require('../../../../src/appsec/iast/taint-tracking/source-types') + +describe('untrusted-deserialization-analyzer with node-serialize', () => { + withVersions('node-serialize', 'node-serialize', version => { + let obj + before(() => { + obj = JSON.stringify({ name: 'example' }) + }) + + describe('unserialize', () => { + prepareTestServerForIast('untrusted deserialization analyzer', + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + let lib + beforeEach(() => { + lib = require(`../../../../../../versions/node-serialize@${version}`).get() + }) + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, obj, 'query', 'Request') + lib.unserialize(str) + }, 'UNTRUSTED_DESERIALIZATION') + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, obj, 'query', SQL_ROW_VALUE) + lib.unserialize(str) + }, 'UNTRUSTED_DESERIALIZATION', undefined, undefined, undefined, + 'Should detect UNTRUSTED_DESERIALIZATION vulnerability with DB source') + + testThatRequestHasNoVulnerability(() => { + lib.unserialize(obj) + }, 'UNTRUSTED_DESERIALIZATION') + }) + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/iast/vulnerability-formatter/index.spec.js b/packages/dd-trace/test/appsec/iast/vulnerability-formatter/index.spec.js index 884df6ebb3d..8996a29fba7 100644 --- a/packages/dd-trace/test/appsec/iast/vulnerability-formatter/index.spec.js +++ b/packages/dd-trace/test/appsec/iast/vulnerability-formatter/index.spec.js @@ -10,7 +10,8 @@ const excludedTests = [ 'Query with single quoted string literal and null source', // does not apply 'Redacted source that needs to be truncated', // not implemented yet 'CODE_INJECTION - Tainted range based redaction - with null source ', // does not apply - 'TEMPLATE_INJECTION - Tainted range based redaction - with null source ' // does not apply + 'TEMPLATE_INJECTION - Tainted range based redaction - with null source ', // does not apply + 'UNTRUSTED_DESERIALIZATION - Tainted range based redaction - with null source ' // does not apply ] function doTest (testCase, parameters) { diff --git a/packages/dd-trace/test/appsec/iast/vulnerability-formatter/resources/evidence-redaction-suite.json b/packages/dd-trace/test/appsec/iast/vulnerability-formatter/resources/evidence-redaction-suite.json index 945c676a688..028217f54f9 100644 --- a/packages/dd-trace/test/appsec/iast/vulnerability-formatter/resources/evidence-redaction-suite.json +++ b/packages/dd-trace/test/appsec/iast/vulnerability-formatter/resources/evidence-redaction-suite.json @@ -2912,7 +2912,8 @@ "XSS", "CODE_INJECTION", "EMAIL_HTML_INJECTION", - "TEMPLATE_INJECTION" + "TEMPLATE_INJECTION", + "UNTRUSTED_DESERIALIZATION" ] }, "input": [ @@ -2971,7 +2972,8 @@ "XSS", "CODE_INJECTION", "EMAIL_HTML_INJECTION", - "TEMPLATE_INJECTION" + "TEMPLATE_INJECTION", + "UNTRUSTED_DESERIALIZATION" ] }, "input": [ @@ -3032,7 +3034,8 @@ "XSS", "CODE_INJECTION", "EMAIL_HTML_INJECTION", - "TEMPLATE_INJECTION" + "TEMPLATE_INJECTION", + "UNTRUSTED_DESERIALIZATION" ] }, "input": [ @@ -3087,7 +3090,8 @@ "XSS", "CODE_INJECTION", "EMAIL_HTML_INJECTION", - "TEMPLATE_INJECTION" + "TEMPLATE_INJECTION", + "UNTRUSTED_DESERIALIZATION" ] }, "input": [ @@ -3167,7 +3171,8 @@ "XSS", "CODE_INJECTION", "EMAIL_HTML_INJECTION", - "TEMPLATE_INJECTION" + "TEMPLATE_INJECTION", + "UNTRUSTED_DESERIALIZATION" ] }, "input": [ @@ -3244,7 +3249,8 @@ "XSS", "CODE_INJECTION", "EMAIL_HTML_INJECTION", - "TEMPLATE_INJECTION" + "TEMPLATE_INJECTION", + "UNTRUSTED_DESERIALIZATION" ] }, "input": [ @@ -3318,7 +3324,8 @@ "XSS", "CODE_INJECTION", "EMAIL_HTML_INJECTION", - "TEMPLATE_INJECTION" + "TEMPLATE_INJECTION", + "UNTRUSTED_DESERIALIZATION" ] }, "input": [