Skip to content

Commit 4f16ba4

Browse files
iunanuansavoire
authored andcommitted
Unvalidated redirect analyzer (#3204)
* Unvalidated redirect analyzer * Ignore tainteds from Referer header
1 parent 3f82dc5 commit 4f16ba4

File tree

14 files changed

+412
-191
lines changed

14 files changed

+412
-191
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module.exports = {
77
'PATH_TRAVERSAL_ANALYZER': require('./path-traversal-analyzer'),
88
'SQL_INJECTION_ANALYZER': require('./sql-injection-analyzer'),
99
'SSRF': require('./ssrf-analyzer'),
10+
'UNVALIDATED_REDIRECT_ANALYZER': require('./unvalidated-redirect-analyzer'),
1011
'WEAK_CIPHER_ANALYZER': require('./weak-cipher-analyzer'),
1112
'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer')
1213
}

packages/dd-trace/src/appsec/iast/analyzers/insecure-cookie-analyzer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
const Analyzer = require('./vulnerability-analyzer')
44
const { INSECURE_COOKIE } = require('../vulnerabilities')
5+
const { getNodeModulesPaths } = require('../path-line')
56

6-
const EXCLUDED_PATHS = ['node_modules/express/lib/response.js', 'node_modules\\express\\lib\\response.js']
7+
const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
78

89
class InsecureCookieAnalyzer extends Analyzer {
910
constructor () {

packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ const { getRanges } = require('../taint-tracking/operations')
66
const { storage } = require('../../../../../datadog-core')
77
const { getIastContext } = require('../iast-context')
88
const { addVulnerability } = require('../vulnerability-reporter')
9+
const { getNodeModulesPaths } = require('../path-line')
910

10-
const EXCLUDED_PATHS = ['node_modules/mysql2', 'node_modules/sequelize', 'node_modules\\mysql2',
11-
'node_modules\\sequelize']
11+
const EXCLUDED_PATHS = getNodeModulesPaths('mysql2', 'sequelize')
1212

1313
class SqlInjectionAnalyzer extends InjectionAnalyzer {
1414
constructor () {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict'
2+
3+
const InjectionAnalyzer = require('./injection-analyzer')
4+
const { UNVALIDATED_REDIRECT } = require('../vulnerabilities')
5+
const { getNodeModulesPaths } = require('../path-line')
6+
const { getRanges } = require('../taint-tracking/operations')
7+
const { HTTP_REQUEST_HEADER_VALUE } = require('../taint-tracking/origin-types')
8+
9+
const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
10+
11+
class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer {
12+
constructor () {
13+
super(UNVALIDATED_REDIRECT)
14+
15+
this.addSub('datadog:http:server:response:set-header:finish', ({ name, value }) => this.analyze(name, value))
16+
}
17+
18+
// TODO: In case the location header value is tainted, this analyzer should check the ranges of the tainted.
19+
// And do not report a vulnerability if source of the ranges (range.iinfo.type) are exclusively url or path params
20+
// to avoid false positives.
21+
analyze (name, value) {
22+
if (!this.isLocationHeader(name) || typeof value !== 'string') return
23+
24+
super.analyze(value)
25+
}
26+
27+
isLocationHeader (name) {
28+
return name && name.trim().toLowerCase() === 'location'
29+
}
30+
31+
_isVulnerable (value, iastContext) {
32+
if (!value) return false
33+
34+
const ranges = getRanges(iastContext, value)
35+
return !this._isRefererHeader(ranges)
36+
}
37+
38+
_isRefererHeader (ranges) {
39+
return ranges && ranges.every(range => range.iinfo.type === HTTP_REQUEST_HEADER_VALUE &&
40+
range.iinfo.parameterName && range.iinfo.parameterName.toLowerCase() === 'referer')
41+
}
42+
43+
_getExcludedPaths () {
44+
return EXCLUDED_PATHS
45+
}
46+
}
47+
48+
module.exports = new UnvalidatedRedirectAnalyzer()

packages/dd-trace/src/appsec/iast/path-line.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const process = require('process')
55
const { calculateDDBasePath } = require('../../util')
66
const pathLine = {
77
getFirstNonDDPathAndLine,
8+
getNodeModulesPaths,
89
getFirstNonDDPathAndLineFromCallsites, // Exported only for test purposes
910
calculateDDBasePath, // Exported only for test purposes
1011
ddBasePath: calculateDDBasePath(__dirname) // Only for test purposes
@@ -83,4 +84,16 @@ function isExcluded (callsite, externallyExcludedPaths) {
8384
function getFirstNonDDPathAndLine (externallyExcludedPaths) {
8485
return getFirstNonDDPathAndLineFromCallsites(getCallSiteInfo(), externallyExcludedPaths)
8586
}
87+
88+
function getNodeModulesPaths (...paths) {
89+
const nodeModulesPaths = []
90+
91+
paths.forEach(p => {
92+
const pathParts = p.split('/')
93+
nodeModulesPaths.push(path.join('node_modules', ...pathParts))
94+
})
95+
96+
return nodeModulesPaths
97+
}
98+
8699
module.exports = pathLine

packages/dd-trace/src/appsec/iast/taint-tracking/origin-types.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
module.exports = {
44
HTTP_REQUEST_BODY: 'http.request.body',
5-
HTTP_REQUEST_PARAMETER: 'http.request.parameter'
5+
HTTP_REQUEST_PARAMETER: 'http.request.parameter',
6+
HTTP_REQUEST_HEADER_VALUE: 'http.request.header'
67
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ class SensitiveHandler {
2323
this._sensitiveAnalyzers.set(vulnerabilities.COMMAND_INJECTION, new CommandSensitiveAnalyzer())
2424
this._sensitiveAnalyzers.set(vulnerabilities.LDAP_INJECTION, new LdapSensitiveAnalyzer())
2525
this._sensitiveAnalyzers.set(vulnerabilities.SQL_INJECTION, new SqlSensitiveAnalyzer())
26-
this._sensitiveAnalyzers.set(vulnerabilities.SSRF, new UrlSensitiveAnalyzer())
26+
const urlSensitiveAnalyzer = new UrlSensitiveAnalyzer()
27+
this._sensitiveAnalyzers.set(vulnerabilities.SSRF, urlSensitiveAnalyzer)
28+
this._sensitiveAnalyzers.set(vulnerabilities.UNVALIDATED_REDIRECT, urlSensitiveAnalyzer)
2729
}
2830

2931
isSensibleName (name) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module.exports = {
55
PATH_TRAVERSAL: 'PATH_TRAVERSAL',
66
SQL_INJECTION: 'SQL_INJECTION',
77
SSRF: 'SSRF',
8+
UNVALIDATED_REDIRECT: 'UNVALIDATED_REDIRECT',
89
WEAK_CIPHER: 'WEAK_CIPHER',
910
WEAK_HASH: 'WEAK_HASH'
1011
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict'
2+
3+
function insecureWithResHeaderMethod (headerName, value, res) {
4+
res.header(headerName, value)
5+
}
6+
7+
function insecureWithResRedirectMethod (value, res) {
8+
res.redirect(200, value)
9+
}
10+
11+
function insecureWithResLocationMethod (value, res) {
12+
res.location(value)
13+
}
14+
15+
module.exports = {
16+
insecureWithResHeaderMethod,
17+
insecureWithResRedirectMethod,
18+
insecureWithResLocationMethod
19+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use strict'
2+
3+
const fs = require('fs')
4+
const os = require('os')
5+
const path = require('path')
6+
7+
const { UNVALIDATED_REDIRECT } = require('../../../../src/appsec/iast/vulnerabilities')
8+
const { prepareTestServerForIastInExpress } = require('../utils')
9+
const { storage } = require('../../../../../datadog-core')
10+
const iastContextFunctions = require('../../../../src/appsec/iast/iast-context')
11+
const { newTaintedString } = require('../../../../src/appsec/iast/taint-tracking/operations')
12+
13+
describe('Unvalidated Redirect vulnerability', () => {
14+
let redirectFunctions
15+
const redirectFunctionsFilename = 'redirect-express-functions.js'
16+
const redirectFunctionsPath = path.join(os.tmpdir(), redirectFunctionsFilename)
17+
18+
before(() => {
19+
fs.copyFileSync(path.join(__dirname, 'resources', redirectFunctionsFilename), redirectFunctionsPath)
20+
redirectFunctions = require(redirectFunctionsPath)
21+
})
22+
23+
after(() => {
24+
fs.unlinkSync(redirectFunctionsPath)
25+
})
26+
27+
withVersions('express', 'express', version => {
28+
prepareTestServerForIastInExpress('in express', version,
29+
(testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => {
30+
testThatRequestHasVulnerability((req, res) => {
31+
const store = storage.getStore()
32+
const iastCtx = iastContextFunctions.getIastContext(store)
33+
const location = newTaintedString(iastCtx, 'https://app.com?id=tron', 'param', 'Request')
34+
redirectFunctions.insecureWithResHeaderMethod('location', location, res)
35+
}, UNVALIDATED_REDIRECT, {
36+
occurrences: 1,
37+
location: {
38+
path: redirectFunctionsFilename,
39+
line: 4
40+
}
41+
})
42+
43+
testThatRequestHasVulnerability((req, res) => {
44+
const store = storage.getStore()
45+
const iastCtx = iastContextFunctions.getIastContext(store)
46+
const location = newTaintedString(iastCtx, 'http://[email protected]/', 'param', 'Request')
47+
redirectFunctions.insecureWithResRedirectMethod(location, res)
48+
}, UNVALIDATED_REDIRECT, {
49+
occurrences: 1,
50+
location: {
51+
path: redirectFunctionsFilename,
52+
line: 8
53+
}
54+
})
55+
56+
testThatRequestHasVulnerability((req, res) => {
57+
const store = storage.getStore()
58+
const iastCtx = iastContextFunctions.getIastContext(store)
59+
const location = newTaintedString(iastCtx, 'http://[email protected]/', 'param', 'Request')
60+
redirectFunctions.insecureWithResLocationMethod(location, res)
61+
}, UNVALIDATED_REDIRECT, {
62+
occurrences: 1,
63+
location: {
64+
path: redirectFunctionsFilename,
65+
line: 12
66+
}
67+
})
68+
69+
testThatRequestHasNoVulnerability((req, res) => {
70+
const store = storage.getStore()
71+
const iastCtx = iastContextFunctions.getIastContext(store)
72+
const location = newTaintedString(iastCtx, 'http://[email protected]/', 'pathParam', 'Request')
73+
res.header('X-test', location)
74+
}, UNVALIDATED_REDIRECT)
75+
})
76+
})
77+
})

0 commit comments

Comments
 (0)