diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/RulesLists.java b/src/main/java/apiaddicts/sonar/openapi/checks/RulesLists.java index 2657e87d..b61e5be8 100644 --- a/src/main/java/apiaddicts/sonar/openapi/checks/RulesLists.java +++ b/src/main/java/apiaddicts/sonar/openapi/checks/RulesLists.java @@ -92,7 +92,8 @@ public static List> getSecurityChecks() { OAR085OpenAPIVersionCheck.class, OAR096ForbiddenResponseCheck.class, OAR045DefinedResponseCheck.class, - OAR049NoContentIn204Check.class + OAR049NoContentIn204Check.class, + OAR114HttpResponseHeadersChecks.class ); } diff --git a/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR114HttpResponseHeadersChecks.java b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR114HttpResponseHeadersChecks.java new file mode 100644 index 00000000..ad4df8b0 --- /dev/null +++ b/src/main/java/apiaddicts/sonar/openapi/checks/security/OAR114HttpResponseHeadersChecks.java @@ -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 mandatoryHeaders = new HashSet<>(); + private Set 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 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 headerDefinitions = new ArrayList<>(headersNode.properties()); + List 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()); + } + } + +} diff --git a/src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.html b/src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.html new file mode 100644 index 00000000..bc606f00 --- /dev/null +++ b/src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.html @@ -0,0 +1,82 @@ +

Normative - API Definition

+

Overriding certain headers or allowing any headers to be set and not specifying required headers can cause some vulnerabilities in the API.

+

Noncompliant Code Example (OpenAPI 2)

+
+  swagger: "2.0"
+  info:
+    version: 1.0.0
+    title: Swagger Petstore
+  paths:
+    /pets:
+      get:
+        responses:
+          200:
+            description: Ok
+            headers: 
+              Authorization: # Noncompliant {{OAR033: Header not allowed}}
+                description: Forbidden header
+                schema:
+                  type: string
+
+

Compliant Solution (OpenAPI 2)

+
+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
+
+

Noncompliant Code Example (OpenAPI 3)

+
+  openapi: "3.0.0"
+  info:
+    version: 1.0.0
+    title: Swagger Petstore
+  paths:
+    /pets:
+      get:
+        responses:
+          200:
+            description: Ok
+             headers: 
+              Authorization: # Noncompliant {{OAR033: Header not allowed}}
+                description: Forbidden header
+                schema:
+                  type: string
+
+

Compliant Solution (OpenAPI 3)

+
+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
+
\ No newline at end of file diff --git a/src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.json b/src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.json new file mode 100644 index 00000000..bfb2c510 --- /dev/null +++ b/src/main/resources/org/sonar/l10n/openapi/rules/openapi/security/OAR114.json @@ -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" +} \ No newline at end of file diff --git a/src/test/java/org/sonar/samples/openapi/checks/security/OAR114HttpResponseHeadersChecksTest.java b/src/test/java/org/sonar/samples/openapi/checks/security/OAR114HttpResponseHeadersChecksTest.java new file mode 100644 index 00000000..fde1b59c --- /dev/null +++ b/src/test/java/org/sonar/samples/openapi/checks/security/OAR114HttpResponseHeadersChecksTest.java @@ -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); + } +} diff --git a/src/test/resources/checks/v2/security/OAR114/valid.json b/src/test/resources/checks/v2/security/OAR114/valid.json new file mode 100644 index 00000000..2e73c4f8 --- /dev/null +++ b/src/test/resources/checks/v2/security/OAR114/valid.json @@ -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" + } + } + } + } + } + } + } +} diff --git a/src/test/resources/checks/v2/security/OAR114/valid.yaml b/src/test/resources/checks/v2/security/OAR114/valid.yaml new file mode 100644 index 00000000..50cf1f9f --- /dev/null +++ b/src/test/resources/checks/v2/security/OAR114/valid.yaml @@ -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 diff --git a/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.json b/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.json new file mode 100644 index 00000000..6f288ccf --- /dev/null +++ b/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.json @@ -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" + } + } + } + } + } + } + } +} diff --git a/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.yaml b/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.yaml new file mode 100644 index 00000000..257c4a53 --- /dev/null +++ b/src/test/resources/checks/v2/security/OAR114/with-forbidden-params.yaml @@ -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 diff --git a/src/test/resources/checks/v2/security/OAR114/without-required-params.json b/src/test/resources/checks/v2/security/OAR114/without-required-params.json new file mode 100644 index 00000000..3714f087 --- /dev/null +++ b/src/test/resources/checks/v2/security/OAR114/without-required-params.json @@ -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", + } + } + } + } + } + } + } +} diff --git a/src/test/resources/checks/v2/security/OAR114/without-required-params.yaml b/src/test/resources/checks/v2/security/OAR114/without-required-params.yaml new file mode 100644 index 00000000..87969337 --- /dev/null +++ b/src/test/resources/checks/v2/security/OAR114/without-required-params.yaml @@ -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 diff --git a/src/test/resources/checks/v3/security/OAR114/valid.json b/src/test/resources/checks/v3/security/OAR114/valid.json new file mode 100644 index 00000000..79d9c816 --- /dev/null +++ b/src/test/resources/checks/v3/security/OAR114/valid.json @@ -0,0 +1,32 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Valid Response Headers Test", + "version": "1.0" + }, + "paths": { + "/example": { + "get": { + "responses": { + "200": { + "description": "OK", + "headers": { + "x-api-key": { + "description": "Mandatory header", + "schema": { + "type": "string" + } + }, + "traceId": { + "description": "Optional but allowed", + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/src/test/resources/checks/v3/security/OAR114/valid.yaml b/src/test/resources/checks/v3/security/OAR114/valid.yaml new file mode 100644 index 00000000..8c936a19 --- /dev/null +++ b/src/test/resources/checks/v3/security/OAR114/valid.yaml @@ -0,0 +1,19 @@ +openapi: 3.0.0 +info: + title: Valid Response Headers Test + version: "1.0" +paths: + /example: + get: + responses: + "200": + description: OK + headers: + x-api-key: + description: Mandatory header + schema: + type: string + traceId: + description: Optional but allowed + schema: + type: string diff --git a/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.json b/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.json new file mode 100644 index 00000000..975318bc --- /dev/null +++ b/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.json @@ -0,0 +1,26 @@ +{ + "openapi": "3.0.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}} + "description": "Forbidden header", + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.yaml b/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.yaml new file mode 100644 index 00000000..4f2c2bc7 --- /dev/null +++ b/src/test/resources/checks/v3/security/OAR114/with-forbidden-params.yaml @@ -0,0 +1,15 @@ +openapi: 3.0.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}} + description: Forbidden header + schema: + type: string diff --git a/src/test/resources/checks/v3/security/OAR114/without-required-params.json b/src/test/resources/checks/v3/security/OAR114/without-required-params.json new file mode 100644 index 00000000..876746f3 --- /dev/null +++ b/src/test/resources/checks/v3/security/OAR114/without-required-params.json @@ -0,0 +1,26 @@ +{ + "openapi": "3.0.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": { + "description": "Allowed header", + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/src/test/resources/checks/v3/security/OAR114/without-required-params.yaml b/src/test/resources/checks/v3/security/OAR114/without-required-params.yaml new file mode 100644 index 00000000..5603c561 --- /dev/null +++ b/src/test/resources/checks/v3/security/OAR114/without-required-params.yaml @@ -0,0 +1,15 @@ +openapi: 3.0.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: + description: Allowed header + schema: + type: string