Skip to content

Commit 327d25a

Browse files
Merge pull request #35 from apiaddicts/feat/oar114_response_headers
Feat/oar114 response headers
2 parents c1dc83f + 4f33f51 commit 327d25a

File tree

17 files changed

+494
-1
lines changed

17 files changed

+494
-1
lines changed

src/main/java/apiaddicts/sonar/openapi/checks/RulesLists.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ public static List<Class<?>> getSecurityChecks() {
9292
OAR085OpenAPIVersionCheck.class,
9393
OAR096ForbiddenResponseCheck.class,
9494
OAR045DefinedResponseCheck.class,
95-
OAR049NoContentIn204Check.class
95+
OAR049NoContentIn204Check.class,
96+
OAR114HttpResponseHeadersChecks.class
9697
);
9798
}
9899

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package apiaddicts.sonar.openapi.checks.security;
2+
3+
import apiaddicts.sonar.openapi.checks.BaseCheck;
4+
import com.google.common.collect.ImmutableSet;
5+
import com.sonar.sslr.api.AstNodeType;
6+
import java.util.ArrayList;
7+
import java.util.HashSet;
8+
import java.util.List;
9+
import java.util.Set;
10+
import java.util.stream.Collectors;
11+
import java.util.stream.Stream;
12+
import org.apiaddicts.apitools.dosonarapi.api.v2.OpenApi2Grammar;
13+
import org.apiaddicts.apitools.dosonarapi.api.v3.OpenApi3Grammar;
14+
import org.apiaddicts.apitools.dosonarapi.api.v31.OpenApi31Grammar;
15+
import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode;
16+
import org.sonar.check.Rule;
17+
import org.sonar.check.RuleProperty;
18+
19+
@Rule(key = OAR114HttpResponseHeadersChecks.KEY)
20+
public class OAR114HttpResponseHeadersChecks extends BaseCheck{
21+
22+
public static final String KEY = "OAR114";
23+
private static final String MANDATORY_HEADERS = "x-api-key";
24+
private static final String ALLOWED_HEADERS = "x-api-key, traceId, dateTime";
25+
26+
@RuleProperty(
27+
key = "mandatory-headers",
28+
description = "List of mandatory headers. Comma separated",
29+
defaultValue = MANDATORY_HEADERS
30+
)
31+
private String mandatoryHeadersStr = MANDATORY_HEADERS;
32+
33+
@RuleProperty(
34+
key = "allowed-headers",
35+
description = "List of allowed headers. Comma separated",
36+
defaultValue = ALLOWED_HEADERS
37+
)
38+
private String allowedHeadersStr = ALLOWED_HEADERS;
39+
40+
41+
private Set<String> mandatoryHeaders = new HashSet<>();
42+
private Set<String> allowedHeaders = new HashSet<>();
43+
44+
@Override
45+
protected void visitFile(JsonNode root) {
46+
if (!mandatoryHeadersStr.trim().isEmpty()) mandatoryHeaders.addAll(Stream.of(mandatoryHeadersStr.split(",")).map(header -> header.toLowerCase().trim()).collect(Collectors.toSet()));
47+
if (!allowedHeadersStr.trim().isEmpty()) allowedHeaders.addAll(Stream.of(allowedHeadersStr.split(",")).map(header -> header.toLowerCase().trim()).collect(Collectors.toSet()));
48+
}
49+
50+
@Override
51+
public Set<AstNodeType> subscribedKinds() {
52+
return ImmutableSet.of(OpenApi2Grammar.RESPONSE, OpenApi3Grammar.RESPONSE, OpenApi31Grammar.RESPONSE);
53+
}
54+
55+
@Override
56+
public void visitNode(JsonNode node) {
57+
validateResponseHeaders(node);
58+
}
59+
60+
61+
private void validateResponseHeaders(JsonNode node) {
62+
JsonNode headersNode = node.get("headers");
63+
64+
if (headersNode == null || headersNode.isMissing() || headersNode.isNull()) return;
65+
66+
List<JsonNode> headerDefinitions = new ArrayList<>(headersNode.properties());
67+
List<String> headerNames = new ArrayList<>();
68+
69+
for (JsonNode headerDef : headerDefinitions) {
70+
String headerName = headerDef.key().getTokenValue().toLowerCase().trim();
71+
headerNames.add(headerName);
72+
73+
if (!allowedHeaders.isEmpty() && !allowedHeaders.contains(headerName)) {
74+
addIssue(KEY, translate("generic.not-allowed-header", headerName), headerDef.key());
75+
}
76+
}
77+
if (mandatoryHeaders != null && !mandatoryHeaders.isEmpty() &&
78+
!headerNames.containsAll(mandatoryHeaders)) {
79+
addIssue(KEY, translate("generic.mandatory-headers", mandatoryHeadersStr), node.key());
80+
}
81+
}
82+
83+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<h2>Normative - API Definition</h2>
2+
<p>Overriding certain headers or allowing any headers to be set and not specifying required headers can cause some vulnerabilities in the API.</p>
3+
<h2>Noncompliant Code Example (OpenAPI 2)</h2>
4+
<pre>
5+
swagger: "2.0"
6+
info:
7+
version: 1.0.0
8+
title: Swagger Petstore
9+
paths:
10+
/pets:
11+
get:
12+
responses:
13+
200:
14+
description: Ok
15+
headers:
16+
Authorization: <span class="error-info" style="color: #FD8E18;"># Noncompliant {{OAR033: Header not allowed}}</span>
17+
description: Forbidden header
18+
schema:
19+
type: string
20+
</pre>
21+
<h2>Compliant Solution (OpenAPI 2)</h2>
22+
<pre>
23+
swagger: "2.0"
24+
info:
25+
version: 1.0.0
26+
title: Swagger Petstore
27+
paths:
28+
/pets:
29+
get:
30+
responses:
31+
200:
32+
description: Ok
33+
headers:
34+
x-api-key:
35+
description: Mandatory header
36+
schema:
37+
type: string
38+
traceId:
39+
description: Optional but allowed
40+
schema:
41+
type: string
42+
</pre>
43+
<h2>Noncompliant Code Example (OpenAPI 3)</h2>
44+
<pre>
45+
openapi: "3.0.0"
46+
info:
47+
version: 1.0.0
48+
title: Swagger Petstore
49+
paths:
50+
/pets:
51+
get:
52+
responses:
53+
200:
54+
description: Ok
55+
headers:
56+
Authorization: <span class="error-info" style="color: #FD8E18;"># Noncompliant {{OAR033: Header not allowed}}</span>
57+
description: Forbidden header
58+
schema:
59+
type: string
60+
</pre>
61+
<h2>Compliant Solution (OpenAPI 3)</h2>
62+
<pre>
63+
openapi: "3.0.0"
64+
info:
65+
version: 1.0.0
66+
title: Swagger Petstore
67+
paths:
68+
/pets:
69+
get:
70+
responses:
71+
200:
72+
description: Ok
73+
headers:
74+
x-api-key:
75+
description: Mandatory header
76+
schema:
77+
type: string
78+
traceId:
79+
description: Optional but allowed
80+
schema:
81+
type: string
82+
</pre>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"title": "OAR114 - HttpResponseHeaders - There are mandatory request headers and others that are not allowed",
3+
"type": "VULNERABILITY",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "15min"
8+
},
9+
"tags": [
10+
"safety"
11+
],
12+
"defaultSeverity": "CRITICAL"
13+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.sonar.samples.openapi.checks.security;
2+
import apiaddicts.sonar.openapi.checks.security.OAR114HttpResponseHeadersChecks;
3+
import org.junit.Before;
4+
import org.junit.Test;
5+
import org.sonar.api.rule.Severity;
6+
import org.sonar.api.rules.RuleType;
7+
import org.sonar.api.server.rule.RuleParamType;
8+
import org.sonar.samples.openapi.BaseCheckTest;
9+
10+
public class OAR114HttpResponseHeadersChecksTest extends BaseCheckTest {
11+
@Before
12+
public void init() {
13+
ruleName = "OAR114";
14+
check = new OAR114HttpResponseHeadersChecks();
15+
v2Path = getV2Path("security");
16+
v3Path = getV3Path("security");
17+
}
18+
19+
@Test
20+
public void verifyInV2() {
21+
verifyV2("valid");
22+
}
23+
24+
@Test
25+
public void verifyInV2WithForbiddenParams() {
26+
verifyV2("with-forbidden-params");
27+
}
28+
29+
@Test
30+
public void verifyInV2WithoutRequiredParams() {
31+
verifyV2("without-required-params");
32+
}
33+
34+
@Test
35+
public void verifyInV3() {
36+
verifyV3("valid");
37+
}
38+
39+
@Test
40+
public void verifyInV3WithForbiddenParams() {
41+
verifyV3("with-forbidden-params");
42+
}
43+
44+
@Test
45+
public void verifyInV3WithoutRequiredParams() {
46+
verifyV3("without-required-params");
47+
}
48+
49+
@Override
50+
public void verifyRule() {
51+
assertRuleProperties("OAR114 - HttpResponseHeaders - There are mandatory request headers and others that are not allowed", RuleType.VULNERABILITY, Severity.CRITICAL, tags("safety"));
52+
}
53+
54+
@Override
55+
public void verifyParameters() {
56+
assertNumberOfParameters(2);
57+
assertParameterProperties("mandatory-headers", "x-api-key", RuleParamType.STRING);
58+
assertParameterProperties("allowed-headers", "x-api-key, traceId, dateTime", RuleParamType.STRING);
59+
}
60+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"swagger": "2.0",
3+
"info": {
4+
"title": "Valid Response Headers Test",
5+
"version": "1.0"
6+
},
7+
"paths": {
8+
"/example": {
9+
"get": {
10+
"responses": {
11+
"200": {
12+
"description": "OK",
13+
"headers": {
14+
"x-api-key": {
15+
"type": "string",
16+
"description": "Mandatory header"
17+
},
18+
"traceId": {
19+
"type": "string",
20+
"description": "Optional but allowed"
21+
}
22+
}
23+
}
24+
}
25+
}
26+
}
27+
}
28+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
swagger: "2.0"
2+
info:
3+
title: Valid Response Headers Test
4+
version: "1.0"
5+
paths:
6+
/example:
7+
get:
8+
responses:
9+
"200":
10+
description: OK
11+
headers:
12+
x-api-key:
13+
type: string
14+
description: Mandatory header
15+
traceId:
16+
type: string
17+
description: Optional but allowed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"swagger": "2.0",
3+
"info": {
4+
"title": "Forbidden Header Test",
5+
"version": "1.0"
6+
},
7+
"paths": {
8+
"/example": {
9+
"get": {
10+
"responses": {
11+
"200": { # Noncompliant {{OAR114: Headers [x-api-key] are required}}
12+
"description": "OK",
13+
"headers": {
14+
"Authorization": { # Noncompliant {{OAR114: Header not allowed}}
15+
"type": "string",
16+
"description": "Forbidden header"
17+
}
18+
}
19+
}
20+
}
21+
}
22+
}
23+
}
24+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
swagger: "2.0"
2+
info:
3+
title: Forbidden Header Test
4+
version: "1.0"
5+
paths:
6+
/example:
7+
get:
8+
responses:
9+
"200": # Noncompliant {{OAR114: Headers [x-api-key] are required}}
10+
description: OK
11+
headers:
12+
Authorization: # Noncompliant {{OAR114: Header not allowed}}
13+
type: string
14+
description: Forbidden header
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"swagger": "2.0",
3+
"info": {
4+
"title": "Missing Mandatory Header Test",
5+
"version": "1.0"
6+
},
7+
"paths": {
8+
"/example": {
9+
"get": {
10+
"responses": {
11+
"200": { # Noncompliant {{OAR114: Headers [x-api-key] are required}}
12+
"description": "OK",
13+
"headers": {
14+
"traceId": {
15+
"type": "string",
16+
"description": "Allowed header",
17+
}
18+
}
19+
}
20+
}
21+
}
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)