-
Notifications
You must be signed in to change notification settings - Fork 306
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Unvalidated redirect analyzer (#3204)
* Unvalidated redirect analyzer * Ignore tainteds from Referer header
- Loading branch information
Showing
14 changed files
with
412 additions
and
191 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
'use strict' | ||
|
||
const InjectionAnalyzer = require('./injection-analyzer') | ||
const { UNVALIDATED_REDIRECT } = require('../vulnerabilities') | ||
const { getNodeModulesPaths } = require('../path-line') | ||
const { getRanges } = require('../taint-tracking/operations') | ||
const { HTTP_REQUEST_HEADER_VALUE } = require('../taint-tracking/origin-types') | ||
|
||
const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js') | ||
|
||
class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer { | ||
constructor () { | ||
super(UNVALIDATED_REDIRECT) | ||
|
||
this.addSub('datadog:http:server:response:set-header:finish', ({ name, value }) => this.analyze(name, value)) | ||
} | ||
|
||
// TODO: In case the location header value is tainted, this analyzer should check the ranges of the tainted. | ||
// And do not report a vulnerability if source of the ranges (range.iinfo.type) are exclusively url or path params | ||
// to avoid false positives. | ||
analyze (name, value) { | ||
if (!this.isLocationHeader(name) || typeof value !== 'string') return | ||
|
||
super.analyze(value) | ||
} | ||
|
||
isLocationHeader (name) { | ||
return name && name.trim().toLowerCase() === 'location' | ||
} | ||
|
||
_isVulnerable (value, iastContext) { | ||
if (!value) return false | ||
|
||
const ranges = getRanges(iastContext, value) | ||
return !this._isRefererHeader(ranges) | ||
} | ||
|
||
_isRefererHeader (ranges) { | ||
return ranges && ranges.every(range => range.iinfo.type === HTTP_REQUEST_HEADER_VALUE && | ||
range.iinfo.parameterName && range.iinfo.parameterName.toLowerCase() === 'referer') | ||
} | ||
|
||
_getExcludedPaths () { | ||
return EXCLUDED_PATHS | ||
} | ||
} | ||
|
||
module.exports = new UnvalidatedRedirectAnalyzer() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
packages/dd-trace/test/appsec/iast/analyzers/resources/redirect-express-functions.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
'use strict' | ||
|
||
function insecureWithResHeaderMethod (headerName, value, res) { | ||
res.header(headerName, value) | ||
} | ||
|
||
function insecureWithResRedirectMethod (value, res) { | ||
res.redirect(200, value) | ||
} | ||
|
||
function insecureWithResLocationMethod (value, res) { | ||
res.location(value) | ||
} | ||
|
||
module.exports = { | ||
insecureWithResHeaderMethod, | ||
insecureWithResRedirectMethod, | ||
insecureWithResLocationMethod | ||
} |
77 changes: 77 additions & 0 deletions
77
.../dd-trace/test/appsec/iast/analyzers/unvalidated-redirect-analyzer.express.plugin.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
'use strict' | ||
|
||
const fs = require('fs') | ||
const os = require('os') | ||
const path = require('path') | ||
|
||
const { UNVALIDATED_REDIRECT } = require('../../../../src/appsec/iast/vulnerabilities') | ||
const { prepareTestServerForIastInExpress } = 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('Unvalidated Redirect vulnerability', () => { | ||
let redirectFunctions | ||
const redirectFunctionsFilename = 'redirect-express-functions.js' | ||
const redirectFunctionsPath = path.join(os.tmpdir(), redirectFunctionsFilename) | ||
|
||
before(() => { | ||
fs.copyFileSync(path.join(__dirname, 'resources', redirectFunctionsFilename), redirectFunctionsPath) | ||
redirectFunctions = require(redirectFunctionsPath) | ||
}) | ||
|
||
after(() => { | ||
fs.unlinkSync(redirectFunctionsPath) | ||
}) | ||
|
||
withVersions('express', 'express', version => { | ||
prepareTestServerForIastInExpress('in express', version, | ||
(testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { | ||
testThatRequestHasVulnerability((req, res) => { | ||
const store = storage.getStore() | ||
const iastCtx = iastContextFunctions.getIastContext(store) | ||
const location = newTaintedString(iastCtx, 'https://app.com?id=tron', 'param', 'Request') | ||
redirectFunctions.insecureWithResHeaderMethod('location', location, res) | ||
}, UNVALIDATED_REDIRECT, { | ||
occurrences: 1, | ||
location: { | ||
path: redirectFunctionsFilename, | ||
line: 4 | ||
} | ||
}) | ||
|
||
testThatRequestHasVulnerability((req, res) => { | ||
const store = storage.getStore() | ||
const iastCtx = iastContextFunctions.getIastContext(store) | ||
const location = newTaintedString(iastCtx, 'http://user@app.com/', 'param', 'Request') | ||
redirectFunctions.insecureWithResRedirectMethod(location, res) | ||
}, UNVALIDATED_REDIRECT, { | ||
occurrences: 1, | ||
location: { | ||
path: redirectFunctionsFilename, | ||
line: 8 | ||
} | ||
}) | ||
|
||
testThatRequestHasVulnerability((req, res) => { | ||
const store = storage.getStore() | ||
const iastCtx = iastContextFunctions.getIastContext(store) | ||
const location = newTaintedString(iastCtx, 'http://user@app.com/', 'param', 'Request') | ||
redirectFunctions.insecureWithResLocationMethod(location, res) | ||
}, UNVALIDATED_REDIRECT, { | ||
occurrences: 1, | ||
location: { | ||
path: redirectFunctionsFilename, | ||
line: 12 | ||
} | ||
}) | ||
|
||
testThatRequestHasNoVulnerability((req, res) => { | ||
const store = storage.getStore() | ||
const iastCtx = iastContextFunctions.getIastContext(store) | ||
const location = newTaintedString(iastCtx, 'http://user@app.com/', 'pathParam', 'Request') | ||
res.header('X-test', location) | ||
}, UNVALIDATED_REDIRECT) | ||
}) | ||
}) | ||
}) |
116 changes: 116 additions & 0 deletions
116
packages/dd-trace/test/appsec/iast/analyzers/unvalidated-redirect-analyzer.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
'use strict' | ||
|
||
const { expect } = require('chai') | ||
const proxyquire = require('proxyquire') | ||
const overheadController = require('../../../../src/appsec/iast/overhead-controller') | ||
const { HTTP_REQUEST_HEADER_VALUE, HTTP_REQUEST_PARAMETER } = | ||
require('../../../../src/appsec/iast/taint-tracking/origin-types') | ||
|
||
describe('unvalidated-redirect-analyzer', () => { | ||
const NOT_TAINTED_LOCATION = 'url.com' | ||
const TAINTED_LOCATION = 'evil.com' | ||
|
||
const TAINTED_HEADER_REFERER_ONLY = 'TAINTED_HEADER_REFERER_ONLY' | ||
const TAINTED_HEADER_REFERER_AMONG_OTHERS = 'TAINTED_HEADER_REFERER_ONLY_AMONG_OTHERS' | ||
|
||
const REFERER_RANGE = { | ||
iinfo: { | ||
type: HTTP_REQUEST_HEADER_VALUE, | ||
parameterName: 'Referer' | ||
} | ||
} | ||
const PARAMETER1_RANGE = { | ||
iinfo: { | ||
type: HTTP_REQUEST_PARAMETER, | ||
parameterName: 'param1' | ||
} | ||
} | ||
const PARAMETER2_RANGE = { | ||
iinfo: { | ||
type: HTTP_REQUEST_PARAMETER, | ||
parameterName: 'param2' | ||
} | ||
} | ||
|
||
const TaintTrackingMock = { | ||
isTainted: (iastContext, string) => { | ||
return string === TAINTED_LOCATION | ||
}, | ||
|
||
getRanges: (iastContext, value) => { | ||
if (value === NOT_TAINTED_LOCATION) return [] | ||
|
||
if (value === TAINTED_HEADER_REFERER_ONLY) { | ||
return [REFERER_RANGE] | ||
} else if (value === TAINTED_HEADER_REFERER_AMONG_OTHERS) { | ||
return [REFERER_RANGE, PARAMETER1_RANGE] | ||
} else { | ||
return [PARAMETER1_RANGE, PARAMETER2_RANGE] | ||
} | ||
} | ||
} | ||
|
||
let report | ||
beforeEach(() => { | ||
report = sinon.stub(unvalidatedRedirectAnalyzer, '_report') | ||
}) | ||
|
||
afterEach(sinon.restore) | ||
|
||
const InjectionAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/injection-analyzer', { | ||
'../taint-tracking/operations': TaintTrackingMock | ||
}) | ||
const unvalidatedRedirectAnalyzer = | ||
proxyquire('../../../../src/appsec/iast/analyzers/unvalidated-redirect-analyzer', { | ||
'./injection-analyzer': InjectionAnalyzer, | ||
'../taint-tracking/operations': TaintTrackingMock | ||
}) | ||
|
||
it('should subscribe to set-header:finish channel', () => { | ||
expect(unvalidatedRedirectAnalyzer._subscriptions).to.have.lengthOf(1) | ||
expect(unvalidatedRedirectAnalyzer._subscriptions[0]._channel.name).to | ||
.equals('datadog:http:server:response:set-header:finish') | ||
}) | ||
|
||
it('should not report headers other than Location', () => { | ||
unvalidatedRedirectAnalyzer.analyze('X-test', NOT_TAINTED_LOCATION) | ||
|
||
expect(report).to.not.have.been.called | ||
}) | ||
|
||
it('should not report Location header with non string values', () => { | ||
unvalidatedRedirectAnalyzer.analyze('X-test', [TAINTED_LOCATION]) | ||
|
||
expect(report).to.not.have.been.called | ||
}) | ||
|
||
it('should not report Location header with not tainted string value', () => { | ||
unvalidatedRedirectAnalyzer.analyze('Location', NOT_TAINTED_LOCATION) | ||
|
||
expect(report).to.not.have.been.called | ||
}) | ||
|
||
it('should report Location header with tainted string value', () => { | ||
sinon.stub(overheadController, 'hasQuota').returns(1) | ||
|
||
unvalidatedRedirectAnalyzer.analyze('Location', TAINTED_LOCATION) | ||
|
||
expect(report).to.be.called | ||
}) | ||
|
||
it('should not report if tainted origin is referer header exclusively', () => { | ||
sinon.stub(overheadController, 'hasQuota').returns(1) | ||
|
||
unvalidatedRedirectAnalyzer.analyze('Location', TAINTED_HEADER_REFERER_ONLY) | ||
|
||
expect(report).to.not.be.called | ||
}) | ||
|
||
it('should report if tainted origin contains referer header among others', () => { | ||
sinon.stub(overheadController, 'hasQuota').returns(1) | ||
|
||
unvalidatedRedirectAnalyzer.analyze('Location', TAINTED_HEADER_REFERER_AMONG_OTHERS) | ||
|
||
expect(report).to.be.called | ||
}) | ||
}) |
Oops, something went wrong.