Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ public static List<Class<?>> getSecurityChecks() {
OAR085OpenAPIVersionCheck.class,
OAR096ForbiddenResponseCheck.class,
OAR045DefinedResponseCheck.class,
OAR049NoContentIn204Check.class
OAR049NoContentIn204Check.class,
OAR114HttpResponseHeadersChecks.class
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package apiaddicts.sonar.openapi.checks.security;

import apiaddicts.sonar.openapi.checks.BaseCheck;
import com.google.common.collect.ImmutableSet;
import com.sonar.sslr.api.AstNodeType;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apiaddicts.apitools.dosonarapi.api.v2.OpenApi2Grammar;
import org.apiaddicts.apitools.dosonarapi.api.v3.OpenApi3Grammar;
import org.apiaddicts.apitools.dosonarapi.api.v31.OpenApi31Grammar;
import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode;
import org.sonar.check.Rule;
import org.sonar.check.RuleProperty;

@Rule(key = OAR114HttpResponseHeadersChecks.KEY)
public class OAR114HttpResponseHeadersChecks extends BaseCheck{

public static final String KEY = "OAR114";
private static final String MANDATORY_HEADERS = "x-api-key";
private static final String ALLOWED_HEADERS = "x-api-key, traceId, dateTime";

@RuleProperty(
key = "mandatory-headers",
description = "List of mandatory headers. Comma separated",
defaultValue = MANDATORY_HEADERS
)
private String mandatoryHeadersStr = MANDATORY_HEADERS;

@RuleProperty(
key = "allowed-headers",
description = "List of allowed headers. Comma separated",
defaultValue = ALLOWED_HEADERS
)
private String allowedHeadersStr = ALLOWED_HEADERS;


private Set<String> mandatoryHeaders = new HashSet<>();
private Set<String> allowedHeaders = new HashSet<>();

@Override
protected void visitFile(JsonNode root) {
if (!mandatoryHeadersStr.trim().isEmpty()) mandatoryHeaders.addAll(Stream.of(mandatoryHeadersStr.split(",")).map(header -> header.toLowerCase().trim()).collect(Collectors.toSet()));
if (!allowedHeadersStr.trim().isEmpty()) allowedHeaders.addAll(Stream.of(allowedHeadersStr.split(",")).map(header -> header.toLowerCase().trim()).collect(Collectors.toSet()));
}

@Override
public Set<AstNodeType> subscribedKinds() {
return ImmutableSet.of(OpenApi2Grammar.RESPONSE, OpenApi3Grammar.RESPONSE, OpenApi31Grammar.RESPONSE);
}

@Override
public void visitNode(JsonNode node) {
validateResponseHeaders(node);
}


private void validateResponseHeaders(JsonNode node) {
JsonNode headersNode = node.get("headers");

if (headersNode == null || headersNode.isMissing() || headersNode.isNull()) return;

List<JsonNode> headerDefinitions = new ArrayList<>(headersNode.properties());
List<String> headerNames = new ArrayList<>();

for (JsonNode headerDef : headerDefinitions) {
String headerName = headerDef.key().getTokenValue().toLowerCase().trim();
headerNames.add(headerName);

if (!allowedHeaders.isEmpty() && !allowedHeaders.contains(headerName)) {
addIssue(KEY, translate("generic.not-allowed-header", headerName), headerDef.key());
}
}
if (mandatoryHeaders != null && !mandatoryHeaders.isEmpty() &&
!headerNames.containsAll(mandatoryHeaders)) {
addIssue(KEY, translate("generic.mandatory-headers", mandatoryHeadersStr), node.key());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<h2>Normative - API Definition</h2>
<p>Overriding certain headers or allowing any headers to be set and not specifying required headers can cause some vulnerabilities in the API.</p>
<h2>Noncompliant Code Example (OpenAPI 2)</h2>
<pre>
swagger: "2.0"
info:
version: 1.0.0
title: Swagger Petstore
paths:
/pets:
get:
responses:
200:
description: Ok
headers:
Authorization: <span class="error-info" style="color: #FD8E18;"># Noncompliant {{OAR033: Header not allowed}}</span>
description: Forbidden header
schema:
type: string
</pre>
<h2>Compliant Solution (OpenAPI 2)</h2>
<pre>
swagger: "2.0"
info:
version: 1.0.0
title: Swagger Petstore
paths:
/pets:
get:
responses:
200:
description: Ok
headers:
x-api-key:
description: Mandatory header
schema:
type: string
traceId:
description: Optional but allowed
schema:
type: string
</pre>
<h2>Noncompliant Code Example (OpenAPI 3)</h2>
<pre>
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
paths:
/pets:
get:
responses:
200:
description: Ok
headers:
Authorization: <span class="error-info" style="color: #FD8E18;"># Noncompliant {{OAR033: Header not allowed}}</span>
description: Forbidden header
schema:
type: string
</pre>
<h2>Compliant Solution (OpenAPI 3)</h2>
<pre>
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
paths:
/pets:
get:
responses:
200:
description: Ok
headers:
x-api-key:
description: Mandatory header
schema:
type: string
traceId:
description: Optional but allowed
schema:
type: string
</pre>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"title": "OAR114 - HttpResponseHeaders - There are mandatory request headers and others that are not allowed",
"type": "VULNERABILITY",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "15min"
},
"tags": [
"safety"
],
"defaultSeverity": "CRITICAL"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.sonar.samples.openapi.checks.security;
import apiaddicts.sonar.openapi.checks.security.OAR114HttpResponseHeadersChecks;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.rule.Severity;
import org.sonar.api.rules.RuleType;
import org.sonar.api.server.rule.RuleParamType;
import org.sonar.samples.openapi.BaseCheckTest;

public class OAR114HttpResponseHeadersChecksTest extends BaseCheckTest {
@Before
public void init() {
ruleName = "OAR114";
check = new OAR114HttpResponseHeadersChecks();
v2Path = getV2Path("security");
v3Path = getV3Path("security");
}

@Test
public void verifyInV2() {
verifyV2("valid");
}

@Test
public void verifyInV2WithForbiddenParams() {
verifyV2("with-forbidden-params");
}

@Test
public void verifyInV2WithoutRequiredParams() {
verifyV2("without-required-params");
}

@Test
public void verifyInV3() {
verifyV3("valid");
}

@Test
public void verifyInV3WithForbiddenParams() {
verifyV3("with-forbidden-params");
}

@Test
public void verifyInV3WithoutRequiredParams() {
verifyV3("without-required-params");
}

@Override
public void verifyRule() {
assertRuleProperties("OAR114 - HttpResponseHeaders - There are mandatory request headers and others that are not allowed", RuleType.VULNERABILITY, Severity.CRITICAL, tags("safety"));
}

@Override
public void verifyParameters() {
assertNumberOfParameters(2);
assertParameterProperties("mandatory-headers", "x-api-key", RuleParamType.STRING);
assertParameterProperties("allowed-headers", "x-api-key, traceId, dateTime", RuleParamType.STRING);
}
}
28 changes: 28 additions & 0 deletions src/test/resources/checks/v2/security/OAR114/valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"swagger": "2.0",
"info": {
"title": "Valid Response Headers Test",
"version": "1.0"
},
"paths": {
"/example": {
"get": {
"responses": {
"200": {
"description": "OK",
"headers": {
"x-api-key": {
"type": "string",
"description": "Mandatory header"
},
"traceId": {
"type": "string",
"description": "Optional but allowed"
}
}
}
}
}
}
}
}
17 changes: 17 additions & 0 deletions src/test/resources/checks/v2/security/OAR114/valid.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
swagger: "2.0"
info:
title: Valid Response Headers Test
version: "1.0"
paths:
/example:
get:
responses:
"200":
description: OK
headers:
x-api-key:
type: string
description: Mandatory header
traceId:
type: string
description: Optional but allowed
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"swagger": "2.0",
"info": {
"title": "Forbidden Header Test",
"version": "1.0"
},
"paths": {
"/example": {
"get": {
"responses": {
"200": { # Noncompliant {{OAR114: Headers [x-api-key] are required}}
"description": "OK",
"headers": {
"Authorization": { # Noncompliant {{OAR114: Header not allowed}}
"type": "string",
"description": "Forbidden header"
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
swagger: "2.0"
info:
title: Forbidden Header Test
version: "1.0"
paths:
/example:
get:
responses:
"200": # Noncompliant {{OAR114: Headers [x-api-key] are required}}
description: OK
headers:
Authorization: # Noncompliant {{OAR114: Header not allowed}}
type: string
description: Forbidden header
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"swagger": "2.0",
"info": {
"title": "Missing Mandatory Header Test",
"version": "1.0"
},
"paths": {
"/example": {
"get": {
"responses": {
"200": { # Noncompliant {{OAR114: Headers [x-api-key] are required}}
"description": "OK",
"headers": {
"traceId": {
"type": "string",
"description": "Allowed header",
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
swagger: "2.0"
info:
title: Missing Mandatory Header Test
version: "1.0"
paths:
/example:
get:
responses:
"200": # Noncompliant {{OAR114: Headers [x-api-key] are required}}
description: OK
headers:
traceId:
type: string
description: Allowed header
Loading
Loading