Skip to content

Commit 6a54cc2

Browse files
Update header injection exclusions (#7821)
1 parent 0cf47ed commit 6a54cc2

File tree

5 files changed

+175
-111
lines changed

5 files changed

+175
-111
lines changed

dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/HeaderInjectionModuleImpl.java

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22

33
import static com.datadog.iast.taint.Ranges.allRangesFromAnyHeader;
44
import static com.datadog.iast.taint.Ranges.allRangesFromHeader;
5+
import static com.datadog.iast.taint.Ranges.allRangesFromSource;
56
import static com.datadog.iast.taint.Ranges.rangeFromHeader;
7+
import static com.datadog.iast.util.HttpHeader.ACCEPT_ENCODING;
8+
import static com.datadog.iast.util.HttpHeader.CACHE_CONTROL;
69
import static com.datadog.iast.util.HttpHeader.CONNECTION;
710
import static com.datadog.iast.util.HttpHeader.COOKIE;
811
import static com.datadog.iast.util.HttpHeader.LOCATION;
912
import static com.datadog.iast.util.HttpHeader.SEC_WEBSOCKET_ACCEPT;
1013
import static com.datadog.iast.util.HttpHeader.SEC_WEBSOCKET_LOCATION;
11-
import static com.datadog.iast.util.HttpHeader.SET_COOKIE;
1214
import static com.datadog.iast.util.HttpHeader.UPGRADE;
15+
import static datadog.trace.api.iast.SourceTypes.REQUEST_HEADER_NAME;
1316

1417
import com.datadog.iast.Dependencies;
1518
import com.datadog.iast.model.Range;
@@ -66,26 +69,81 @@ public void tainted(
6669
final RangeBuilder ranges,
6770
final Object value,
6871
final Range[] valueRanges) {
69-
// Exclude access-control-allow-*: when the header starts with access-control-allow- and
70-
// the source of the tainted range is a request header
71-
if (name.regionMatches(
72-
true, 0, ACCESS_CONTROL_ALLOW_PREFIX, 0, ACCESS_CONTROL_ALLOW_PREFIX.length())
73-
&& allRangesFromAnyHeader(valueRanges)) {
72+
if (shouldIgnoreHeader(valueRanges)) {
7473
return;
7574
}
75+
evidence.append(name).append(": ").append(value);
76+
ranges.add(valueRanges, name.length() + 2);
77+
}
7678

77-
// Exclude set-cookie header if the source of all the tainted ranges are cookies
78-
if ((header == SET_COOKIE) && allRangesFromHeader(COOKIE, valueRanges)) {
79-
return;
79+
private boolean shouldIgnoreHeader(final Range[] valueRanges) {
80+
if (header != null) {
81+
switch (header) {
82+
case SET_COOKIE:
83+
case SET_COOKIE2:
84+
if (ignoreSetCookieHeader(valueRanges)) {
85+
return true;
86+
}
87+
break;
88+
case PRAGMA:
89+
if (ignorePragmaHeader(valueRanges)) {
90+
return true;
91+
}
92+
break;
93+
case TRANSFER_ENCODING:
94+
case CONTENT_ENCODING:
95+
if (ignoreEncodingHeader(valueRanges)) {
96+
return true;
97+
}
98+
break;
99+
case VARY:
100+
if (ignoreVaryHeader(valueRanges)) {
101+
return true;
102+
}
103+
break;
104+
}
80105
}
81106

82-
// Exclude when the header is reflected from the request
83-
if (valueRanges.length == 1 && rangeFromHeader(name, valueRanges[0])) {
84-
return;
85-
}
107+
return ignoreAccessControlAllow(valueRanges) || ignoreReflectedHeader(valueRanges);
108+
}
86109

87-
evidence.append(name).append(": ").append(value);
88-
ranges.add(valueRanges, name.length() + 2);
110+
/** Ignore pragma headers when the source is the cache control header. */
111+
private boolean ignorePragmaHeader(final Range[] ranges) {
112+
return allRangesFromHeader(CACHE_CONTROL, ranges);
113+
}
114+
115+
/**
116+
* Ignore transfer and content encoding headers when the source is the accept encoding header.
117+
*/
118+
private boolean ignoreEncodingHeader(final Range[] ranges) {
119+
return allRangesFromHeader(ACCEPT_ENCODING, ranges);
120+
}
121+
122+
/** Ignore vary header when the sources are only header names */
123+
private boolean ignoreVaryHeader(final Range[] ranges) {
124+
return allRangesFromSource(REQUEST_HEADER_NAME, ranges);
125+
}
126+
127+
/**
128+
* Exclude access-control-allow-*: when the header starts with access-control-allow- and the
129+
* source of the tainted range is a request header
130+
*/
131+
private boolean ignoreAccessControlAllow(final Range[] ranges) {
132+
return nameMatchesPrefix(ACCESS_CONTROL_ALLOW_PREFIX) && allRangesFromAnyHeader(ranges);
133+
}
134+
135+
/** Exclude set-cookie header if the source of all the tainted ranges are cookies */
136+
private boolean ignoreSetCookieHeader(final Range[] ranges) {
137+
return allRangesFromHeader(COOKIE, ranges);
138+
}
139+
140+
/** Exclude when the header is reflected from the request */
141+
private boolean ignoreReflectedHeader(final Range[] ranges) {
142+
return ranges.length == 1 && rangeFromHeader(name, ranges[0]);
143+
}
144+
145+
private boolean nameMatchesPrefix(final String prefix) {
146+
return name.regionMatches(true, 0, prefix, 0, prefix.length());
89147
}
90148
}
91149
}

dd-java-agent/agent-iast/src/main/java/com/datadog/iast/taint/Ranges.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,20 @@ public static boolean rangeFromHeader(@Nonnull final String header, @Nonnull fin
170170
return true;
171171
}
172172

173+
/**
174+
* Checks if all ranges are coming from a specify source type, in case no ranges are provided it
175+
* will return {@code true}
176+
*/
177+
public static boolean allRangesFromSource(final byte origin, @Nonnull final Range[] ranges) {
178+
for (Range range : ranges) {
179+
final Source current = range.getSource();
180+
if (current.getOrigin() != origin) {
181+
return false;
182+
}
183+
}
184+
return true;
185+
}
186+
173187
/**
174188
* Checks if all ranges are coming from the header, in case no ranges are provided it will return
175189
* {@code true}

dd-java-agent/agent-iast/src/main/java/com/datadog/iast/util/HttpHeader.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ public void addToContext(final IastRequestContext ctx, final String value) {
4343
SEC_WEBSOCKET_ACCEPT("Sec-WebSocket-Accept"),
4444
UPGRADE("Upgrade"),
4545
CONNECTION("Connection"),
46-
ORIGIN("Origin");
46+
ORIGIN("Origin"),
47+
CONTENT_ENCODING("Content-Encoding"),
48+
TRANSFER_ENCODING("Transfer-Encoding"),
49+
ACCEPT_ENCODING("Accept-Encoding"),
50+
VARY("Vary"),
51+
PRAGMA("Pragma"),
52+
CACHE_CONTROL("Cache-Control");
4753

4854
/** Faster lookup for headers */
4955
private static final HttpHeaderMap<HttpHeader> HEADERS;

dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/HeaderInjectionModuleTest.groovy

Lines changed: 81 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,26 @@ package com.datadog.iast.sink
22

33
import com.datadog.iast.IastModuleImplTestBase
44
import com.datadog.iast.Reporter
5+
import com.datadog.iast.model.Range
6+
import com.datadog.iast.model.Source
57
import com.datadog.iast.model.Vulnerability
68
import com.datadog.iast.model.VulnerabilityType
7-
import datadog.trace.api.iast.SourceTypes
89
import datadog.trace.api.iast.VulnerabilityMarks
910
import datadog.trace.api.iast.sink.HeaderInjectionModule
1011

11-
import static com.datadog.iast.taint.TaintUtils.addFromRangeList
1212
import static com.datadog.iast.taint.TaintUtils.addFromTaintFormat
1313
import static com.datadog.iast.taint.TaintUtils.taintFormat
1414
import static com.datadog.iast.util.HttpHeader.CONNECTION
1515
import static com.datadog.iast.util.HttpHeader.LOCATION
1616
import static com.datadog.iast.util.HttpHeader.SEC_WEBSOCKET_ACCEPT
1717
import static com.datadog.iast.util.HttpHeader.SEC_WEBSOCKET_LOCATION
18-
import static com.datadog.iast.util.HttpHeader.SET_COOKIE
1918
import static com.datadog.iast.util.HttpHeader.UPGRADE
20-
import static com.datadog.iast.util.HttpHeader.COOKIE
19+
import static datadog.trace.api.iast.SourceTypes.REQUEST_HEADER_NAME
20+
import static datadog.trace.api.iast.SourceTypes.REQUEST_HEADER_VALUE
21+
import static datadog.trace.api.iast.SourceTypes.REQUEST_PARAMETER_VALUE
2122
import static datadog.trace.api.iast.VulnerabilityMarks.NOT_MARKED
2223

23-
class HeaderInjectionModuleTest extends IastModuleImplTestBase{
24+
class HeaderInjectionModuleTest extends IastModuleImplTestBase {
2425

2526
private HeaderInjectionModule module
2627

@@ -48,11 +49,11 @@ class HeaderInjectionModuleTest extends IastModuleImplTestBase{
4849
}
4950

5051
where:
51-
headerValue | mark | headerName | expected
52-
'/==>var<==' | NOT_MARKED | 'headerName' | "headerName: /==>var<=="
53-
'/==>var<==' | VulnerabilityMarks.XPATH_INJECTION_MARK | 'headerName' | "headerName: /==>var<=="
54-
'var' | NOT_MARKED | 'headerName' | null
55-
'/==>var<==' | VulnerabilityMarks.HEADER_INJECTION_MARK | 'headerName' | null
52+
headerValue | mark | headerName | expected
53+
'/==>var<==' | NOT_MARKED | 'headerName' | "headerName: /==>var<=="
54+
'/==>var<==' | VulnerabilityMarks.XPATH_INJECTION_MARK | 'headerName' | "headerName: /==>var<=="
55+
'var' | NOT_MARKED | 'headerName' | null
56+
'/==>var<==' | VulnerabilityMarks.HEADER_INJECTION_MARK | 'headerName' | null
5657
}
5758

5859
void 'check excluded headers'() {
@@ -66,96 +67,94 @@ class HeaderInjectionModuleTest extends IastModuleImplTestBase{
6667
header << [SEC_WEBSOCKET_LOCATION, SEC_WEBSOCKET_ACCEPT, UPGRADE, CONNECTION, LOCATION]
6768
}
6869

69-
void 'check set-cookie exclusion'(){
70+
void 'check exclusion for #header (excluded: #excluded)'() {
7071
given:
7172
final headerValue = 'headerValue'
72-
addFromRangeList(ctx.taintedObjects, 'headerValue', ranges)
73+
ctx.taintedObjects.taint(headerValue, sources as Range[])
7374

7475
when:
75-
module.onHeader(SET_COOKIE.name, headerValue)
76+
module.onHeader(header, headerValue)
7677

7778
then:
78-
expected * reporter.report(_, _ as Vulnerability)
79+
if (excluded) {
80+
0 * reporter._
81+
} else {
82+
1 * reporter.report(_, _) >> { it -> assertVulnerability(it[1] as Vulnerability, VulnerabilityType.HEADER_INJECTION) }
83+
}
7984

8085
where:
81-
ranges | expected
82-
[[0, 2, COOKIE, 'sourceValue', SourceTypes.REQUEST_HEADER_VALUE]] | 0
83-
[
84-
[0, 2, COOKIE, 'sourceValue', SourceTypes.REQUEST_HEADER_VALUE],
85-
[3, 4, COOKIE, 'sourceValue2', SourceTypes.REQUEST_HEADER_VALUE]
86-
] | 0
87-
[
88-
[0, 2, COOKIE, 'sourceValue', SourceTypes.REQUEST_HEADER_VALUE],
89-
[0, 2, 'sourceName', 'sourceValue', SourceTypes.REQUEST_HEADER_VALUE]
90-
] | 1
91-
[[0, 2, 'sourceName', 'sourceValue', SourceTypes.REQUEST_HEADER_VALUE]] | 1
92-
[[0, 2, 'sourceName', 'sourceValue', SourceTypes.GRPC_BODY]] | 1
93-
[[0, 2, SET_COOKIE, 'sourceValue', SourceTypes.REQUEST_HEADER_VALUE]] | 1
94-
}
95-
96-
void 'check reflected header exclusion'(){
97-
given:
98-
final headerName = 'headerName'
99-
final headerValue = 'headerValue'
100-
addFromRangeList(ctx.taintedObjects, 'headerValue', ranges)
101-
102-
when:
103-
module.onHeader(headerName, headerValue)
86+
header | sources | excluded
87+
// exclude if sources coming only from headers
88+
'Access-Control-Allow-Origin' | [requestParam()] | false
89+
'Access-Control-Allow-Origin' | [requestParam(), contentType()] | false
90+
'Access-Control-Allow-Origin' | [contentType()] | true
91+
92+
// exclude if sources coming only from headers
93+
'Access-Control-Allow-Methods' | [requestParam()] | false
94+
'Access-Control-Allow-Methods' | [requestParam(), contentType()] | false
95+
'Access-Control-Allow-Methods' | [contentType()] | true
96+
97+
// exclude if sources coming only from 'Cookie' headers
98+
'Set-Cookie' | [requestParam()] | false
99+
'Set-Cookie' | [requestParam(), contentType()] | false
100+
'Set-Cookie' | [cookie()] | true
101+
102+
// exclude if reflected from header with the same name
103+
'Reflected' | contentType() | false
104+
'Reflected' | [contentType(), requestHeader('Reflected', 'value')] | false
105+
'Reflected' | [requestHeader('Reflected', 'value')] | true
106+
107+
// exclude if reflected from 'Cache-Control' header
108+
'Pragma' | [contentType()] | false
109+
'Pragma' | [contentType(), cacheControl()] | false
110+
'Pragma' | [cacheControl()] | true
111+
112+
// exclude if reflected from 'Accept-Encoding' header
113+
'Transfer-Encoding' | [contentType()] | false
114+
'Transfer-Encoding' | [contentType(), acceptEncoding()] | false
115+
'Transfer-Encoding' | [acceptEncoding()] | true
116+
117+
// exclude if reflected from 'Accept-Encoding' header
118+
'Content-Encoding' | [contentType()] | false
119+
'Content-Encoding' | [contentType(), acceptEncoding()] | false
120+
'Content-Encoding' | [acceptEncoding()] | true
121+
122+
// exclude if sources coming only from request header names
123+
'Vary' | [contentType()] | false
124+
'Vary' | [contentType(), headerName('Accept')] | false
125+
'Vary' | [headerName('Content-Type'), headerName('Authorization')] | true
126+
}
104127

105-
then:
106-
expected * reporter.report(_, _ as Vulnerability)
128+
private static Range headerName(String name) {
129+
return source(REQUEST_HEADER_NAME, name, name)
130+
}
107131

108-
where:
109-
ranges | expected
110-
[[0, 2, COOKIE, 'sourceValue', SourceTypes.REQUEST_HEADER_VALUE]] | 1
111-
[
112-
[0, 2, 'headerName', 'sourceValue', SourceTypes.REQUEST_HEADER_VALUE],
113-
[3, 4, 'headerName', 'sourceValue2', SourceTypes.REQUEST_HEADER_VALUE]
114-
] | 1
115-
[[0, 2, 'headerName', 'sourceValue', SourceTypes.REQUEST_HEADER_VALUE]] | 0
132+
private static Range cookie(String name = 'p', String value = 'value') {
133+
return source(REQUEST_HEADER_VALUE, 'Cookie', "$name=$value")
116134
}
117135

118-
void 'check access-control-allow-* exclusion'(){
119-
given:
120-
final headerValue = 'headerValue'
121-
addFromRangeList(ctx.taintedObjects, 'headerValue', suite.ranges)
136+
private static Range requestParam(String name = 'p', String value = 'value') {
137+
return source(REQUEST_PARAMETER_VALUE, name, value)
138+
}
122139

123-
when:
124-
module.onHeader(suite.header, headerValue)
140+
private static Range acceptEncoding(String value = 'gzip') {
141+
return requestHeader('Accept-Encoding', value)
142+
}
125143

126-
then:
127-
suite.expected * reporter.report(_, _ as Vulnerability)
144+
private static Range cacheControl(String value = 'no-cache') {
145+
return requestHeader('Cache-Control', value)
146+
}
128147

129-
where:
130-
suite << createTestSuite()
131-
}
132-
133-
private Iterable<TestSuite> createTestSuite() {
134-
final result = []
135-
final headerNames = ['Access-Control-Allow-Origin', 'access-control-allow-origin', 'ACCESS-CONTROL-ALLOW-METHODS']
136-
for (headerName in headerNames){
137-
result.add(createTestSuite(headerName, [[0, 2, 'sourceName', 'sourceValue', SourceTypes.REQUEST_HEADER_VALUE]], 0))
138-
result.add(createTestSuite(headerName, [
139-
[0, 2, 'sourceName', 'sourceValue', SourceTypes.REQUEST_HEADER_VALUE],
140-
[3, 4, 'sourceName2', 'sourceValue2', SourceTypes.REQUEST_HEADER_VALUE]
141-
], 0))
142-
result.add(createTestSuite(headerName, [[0, 2, 'sourceName', 'sourceValue', SourceTypes.GRPC_BODY]], 1))
143-
result.add(createTestSuite(headerName, [
144-
[0, 2, 'sourceName', 'sourceValue', SourceTypes.GRPC_BODY],
145-
[3, 4, 'sourceName2', 'sourceValue2', SourceTypes.REQUEST_HEADER_VALUE]
146-
], 1))
147-
}
148-
return result as Iterable<TestSuite>
148+
private static Range contentType(String value = 'text/html') {
149+
return requestHeader('Content-Type', value)
149150
}
150151

151-
private createTestSuite(header, ranges, expected) {
152-
return new TestSuite('header': header as String, 'ranges': ranges as List<List<Object>>, 'expected': expected as Integer)
152+
private static Range requestHeader(String name, String value = 'text/html') {
153+
return source(REQUEST_HEADER_VALUE, name, value)
153154
}
154155

155-
private static class TestSuite {
156-
String header
157-
List<List<Object>> ranges
158-
Integer expected
156+
private static Range source(byte origin, String name, String value) {
157+
return new Range(0, value.length(), new Source(origin, name, value), NOT_MARKED)
159158
}
160159

161160
private String mapTainted(final String value, final int mark) {
@@ -164,7 +163,7 @@ class HeaderInjectionModuleTest extends IastModuleImplTestBase{
164163
return result
165164
}
166165

167-
private static void assertVulnerability(final Vulnerability vuln, final VulnerabilityType type ) {
166+
private static void assertVulnerability(final Vulnerability vuln, final VulnerabilityType type) {
168167
assert vuln != null
169168
assert vuln.getType() == type
170169
assert vuln.getLocation() != null

dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/TaintUtils.groovy

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,19 +96,6 @@ class TaintUtils {
9696
return resultString
9797
}
9898

99-
static void addFromRangeList(final TaintedObjects tos, final String s, final List<List<Object>> spockRangeList) {
100-
Vector<Range> rangesVector = new Vector<>()
101-
for (List<Object> range : spockRangeList) {
102-
int start = (int)range.get(0)
103-
int length = (int)range.get(1)
104-
byte sourceType = (byte)range.get(4)
105-
rangesVector.add(new Range(start, length, new Source(sourceType, (String)range.get(2), (String)range.get(3)), NOT_MARKED))
106-
}
107-
108-
Range[] ranges = (Range[])rangesVector.toArray()
109-
tos.taint(s, ranges)
110-
}
111-
11299
static StringBuilder addFromTaintFormat(final TaintedObjects tos, final StringBuilder sb) {
113100
final String s = sb.toString()
114101
final ranges = fromTaintFormat(s)

0 commit comments

Comments
 (0)