Skip to content

Commit f8cd75d

Browse files
committed
Detect X-Content-Type-Options missing header
1 parent 30ca014 commit f8cd75d

File tree

7 files changed

+119
-7
lines changed

7 files changed

+119
-7
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ module.exports = {
1111
'SSRF': require('./ssrf-analyzer'),
1212
'UNVALIDATED_REDIRECT_ANALYZER': require('./unvalidated-redirect-analyzer'),
1313
'WEAK_CIPHER_ANALYZER': require('./weak-cipher-analyzer'),
14-
'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer')
14+
'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer'),
15+
'XCONTENTTYPE_HEADER_MISSING': require('./xcontenttype-header-missing-analyzer')
1516
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
const analyzers = require('./analyzers')
44
const setCookiesHeaderInterceptor = require('./set-cookies-header-interceptor')
55

6-
function enableAllAnalyzers () {
7-
setCookiesHeaderInterceptor.configure(true)
6+
function enableAllAnalyzers (tracerConfig) {
7+
setCookiesHeaderInterceptor.configure({ enabled: true, tracerConfig })
88
for (const analyzer in analyzers) {
9-
analyzers[analyzer].configure(true)
9+
analyzers[analyzer].configure({ enabled: true, tracerConfig })
1010
}
1111
}
1212

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict'
2+
3+
const Analyzer = require('./vulnerability-analyzer')
4+
const { XCONTENTTYPE_HEADER_MISSING } = require('../vulnerabilities')
5+
6+
const htmlContentTypes = ['text/html', 'application/xhtml+xml']
7+
function isResponseHtml (res) {
8+
const contentType = res.getHeader('content-type')
9+
return contentType && htmlContentTypes.some(htmlContentType => {
10+
return htmlContentType === contentType || contentType.indexOf(htmlContentType + ';') === 0
11+
})
12+
}
13+
14+
const XCONTENTTYPEOPTIONS_HEADER_NAME = 'X-Content-Type-Options'
15+
16+
class XcontenttypeHeaderMissingAnalyzer extends Analyzer {
17+
constructor () {
18+
super(XCONTENTTYPE_HEADER_MISSING)
19+
20+
this.addSub('datadog:iast:response-end', (data) => this.analyze(data))
21+
}
22+
23+
_isVulnerable ({ req, res }, context) {
24+
if (isResponseHtml(res)) {
25+
const headerToCheck = res.getHeader(XCONTENTTYPEOPTIONS_HEADER_NAME)
26+
return !headerToCheck || headerToCheck.trim().toLowerCase() !== 'nosniff'
27+
}
28+
return false
29+
}
30+
31+
_getEvidence ({ res }) {
32+
return { value: res.getHeader(XCONTENTTYPEOPTIONS_HEADER_NAME) }
33+
}
34+
35+
_getLocation () {
36+
return undefined
37+
}
38+
39+
_checkOCE (context) {
40+
return true
41+
}
42+
43+
_createHashSource (type, evidence, location) {
44+
return `${type}:${this.config.tracerConfig.service}`
45+
}
46+
}
47+
48+
module.exports = new XcontenttypeHeaderMissingAnalyzer()

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ const telemetryLogs = require('./telemetry/logs')
2020
// order of the callbacks can be enforce
2121
const requestStart = dc.channel('dd-trace:incomingHttpRequestStart')
2222
const requestClose = dc.channel('dd-trace:incomingHttpRequestEnd')
23+
const iastResponseEnd = dc.channel('datadog:iast:response-end')
2324

2425
function enable (config, _tracer) {
25-
enableAllAnalyzers()
26+
enableAllAnalyzers(config)
2627
enableTaintTracking(config.iast)
2728
requestStart.subscribe(onIncomingHttpRequestStart)
2829
requestClose.subscribe(onIncomingHttpRequestEnd)
@@ -72,6 +73,8 @@ function onIncomingHttpRequestEnd (data) {
7273
const topContext = web.getContext(data.req)
7374
const iastContext = iastContextFunctions.getIastContext(store, topContext)
7475
if (iastContext && iastContext.rootSpan) {
76+
iastResponseEnd.publish(data)
77+
7578
const vulnerabilities = iastContext.vulnerabilities
7679
const rootSpan = iastContext.rootSpan
7780
vulnerabilityReporter.sendVulnerabilities(vulnerabilities, rootSpan)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ module.exports = {
99
SSRF: 'SSRF',
1010
UNVALIDATED_REDIRECT: 'UNVALIDATED_REDIRECT',
1111
WEAK_CIPHER: 'WEAK_CIPHER',
12-
WEAK_HASH: 'WEAK_HASH'
12+
WEAK_HASH: 'WEAK_HASH',
13+
XCONTENTTYPE_HEADER_MISSING: 'XCONTENTTYPE_HEADER_MISSING'
1314
}

packages/dd-trace/src/appsec/iast/vulnerability-reporter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function addVulnerability (iastContext, vulnerability) {
2828

2929
function isValidVulnerability (vulnerability) {
3030
return vulnerability && vulnerability.type &&
31-
vulnerability.evidence && vulnerability.evidence.value &&
31+
vulnerability.evidence &&
3232
vulnerability.location && vulnerability.location.spanId
3333
}
3434

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict'
2+
3+
const { prepareTestServerForIast } = require('../utils')
4+
const Analyzer = require('../../../../src/appsec/iast/analyzers/vulnerability-analyzer')
5+
const { XCONTENTTYPE_HEADER_MISSING } = require('../../../../src/appsec/iast/vulnerabilities')
6+
const analyzer = new Analyzer()
7+
8+
describe('xcontenttype header missing analyzer', () => {
9+
it('Expected vulnerability identifier', () => {
10+
expect(XCONTENTTYPE_HEADER_MISSING).to.be.equals('XCONTENTTYPE_HEADER_MISSING')
11+
})
12+
13+
prepareTestServerForIast('xcontenttype header missing analyzer',
14+
(testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => {
15+
testThatRequestHasVulnerability((req, res) => {
16+
res.setHeader('content-type', 'text/html')
17+
res.end('<html><body><h1>Test</h1></body></html>')
18+
}, XCONTENTTYPE_HEADER_MISSING, 1, function (vulnerabilities) {
19+
expect(vulnerabilities[0].evidence.value).to.be.undefined
20+
expect(vulnerabilities[0].hash).to.be.equals(analyzer._createHash('XCONTENTTYPE_HEADER_MISSING:mocha'))
21+
})
22+
23+
testThatRequestHasVulnerability((req, res) => {
24+
res.setHeader('content-type', 'text/html;charset=utf-8')
25+
res.end('<html><body><h1>Test</h1></body></html>')
26+
}, XCONTENTTYPE_HEADER_MISSING, 1, function (vulnerabilities) {
27+
expect(vulnerabilities[0].evidence.value).to.be.undefined
28+
expect(vulnerabilities[0].hash).to.be.equals(analyzer._createHash('XCONTENTTYPE_HEADER_MISSING:mocha'))
29+
})
30+
31+
testThatRequestHasVulnerability((req, res) => {
32+
res.setHeader('content-type', 'application/xhtml+xml')
33+
res.end('<html><body><h1>Test</h1></body></html>')
34+
}, XCONTENTTYPE_HEADER_MISSING, 1, function (vulnerabilities) {
35+
expect(vulnerabilities[0].evidence.value).to.be.undefined
36+
expect(vulnerabilities[0].hash).to.be.equals(analyzer._createHash('XCONTENTTYPE_HEADER_MISSING:mocha'))
37+
})
38+
39+
testThatRequestHasVulnerability((req, res) => {
40+
res.setHeader('content-type', 'text/html')
41+
res.setHeader('X-Content-Type-Options', 'whatever')
42+
res.end('<html><body><h1>Test</h1></body></html>')
43+
}, XCONTENTTYPE_HEADER_MISSING, 1, function (vulnerabilities) {
44+
expect(vulnerabilities[0].evidence.value).to.be.equal('whatever')
45+
expect(vulnerabilities[0].hash).to.be.equals(analyzer._createHash('XCONTENTTYPE_HEADER_MISSING:mocha'))
46+
})
47+
48+
testThatRequestHasNoVulnerability((req, res) => {
49+
res.setHeader('content-type', 'application/json')
50+
res.end('{"key": "test}')
51+
}, XCONTENTTYPE_HEADER_MISSING)
52+
53+
testThatRequestHasNoVulnerability((req, res) => {
54+
res.setHeader('content-type', 'text/html')
55+
res.setHeader('X-Content-Type-Options', 'nosniff')
56+
res.end('{"key": "test}')
57+
}, XCONTENTTYPE_HEADER_MISSING)
58+
})
59+
})

0 commit comments

Comments
 (0)