-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SONARPY-1498 Rule S6786: Python GraphQL introspection should be disab…
…led (#1637)
- Loading branch information
Showing
7 changed files
with
827 additions
and
222 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
144 changes: 144 additions & 0 deletions
144
python-checks/src/main/java/org/sonar/python/checks/hotspots/GraphQLIntrospectionCheck.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
/* | ||
* SonarQube Python Plugin | ||
* Copyright (C) 2011-2023 SonarSource SA | ||
* mailto:info AT sonarsource DOT com | ||
* | ||
* This program is free software; you can redistribute it and/or | ||
* modify it under the terms of the GNU Lesser General Public | ||
* License as published by the Free Software Foundation; either | ||
* version 3 of the License, or (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
* Lesser General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Lesser General Public License | ||
* along with this program; if not, write to the Free Software Foundation, | ||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
*/ | ||
package org.sonar.python.checks.hotspots; | ||
|
||
import java.util.List; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.stream.Stream; | ||
|
||
import org.sonar.check.Rule; | ||
import org.sonar.plugins.python.api.PythonSubscriptionCheck; | ||
import org.sonar.plugins.python.api.SubscriptionContext; | ||
import org.sonar.plugins.python.api.symbols.Symbol; | ||
import org.sonar.plugins.python.api.tree.Argument; | ||
import org.sonar.plugins.python.api.tree.CallExpression; | ||
import org.sonar.plugins.python.api.tree.Expression; | ||
import org.sonar.plugins.python.api.tree.ExpressionList; | ||
import org.sonar.plugins.python.api.tree.ListLiteral; | ||
import org.sonar.plugins.python.api.tree.RegularArgument; | ||
import org.sonar.plugins.python.api.tree.Tree; | ||
import org.sonar.plugins.python.api.tree.Tuple; | ||
import org.sonar.plugins.python.api.types.InferredType; | ||
import org.sonar.python.tree.NameImpl; | ||
import org.sonar.python.tree.TreeUtils; | ||
|
||
@Rule(key = "S6786") | ||
public class GraphQLIntrospectionCheck extends PythonSubscriptionCheck { | ||
|
||
private static final Set<String> GRAPHQL_VIEWS_FQNS = Set.of( | ||
"flask_graphql.GraphQLView.as_view", | ||
"graphql_server.flask.GraphQLView.as_view"); | ||
|
||
private static final Set<String> SAFE_VALIDATION_RULE_FQNS = Set.of( | ||
"graphene.validation.DisableIntrospection", | ||
"graphql.validation.NoSchemaIntrospectionCustomRule"); | ||
|
||
private static final String MESSAGE = "Disable introspection on this \"GraphQL\" server endpoint."; | ||
|
||
@Override | ||
public void initialize(Context context) { | ||
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, GraphQLIntrospectionCheck::checkGraphQLIntrospection); | ||
} | ||
|
||
private static void checkGraphQLIntrospection(SubscriptionContext ctx) { | ||
CallExpression callExpression = (CallExpression) ctx.syntaxNode(); | ||
Optional.ofNullable(callExpression.calleeSymbol()) | ||
.map(Symbol::fullyQualifiedName) | ||
.filter(GRAPHQL_VIEWS_FQNS::contains) | ||
.filter(fqn -> !hasSafeMiddlewares(callExpression.arguments())) | ||
.filter(fqn -> !hasSafeValidationRules(callExpression.arguments())) | ||
.ifPresent(fqn -> ctx.addIssue(callExpression.callee(), MESSAGE)); | ||
} | ||
|
||
private static boolean hasSafeMiddlewares(List<Argument> arguments) { | ||
RegularArgument argument = TreeUtils.argumentByKeyword("middleware", arguments); | ||
if (argument == null) { | ||
return false; | ||
} | ||
|
||
return extractArgumentValues(argument) | ||
.map(values -> !values.isEmpty() && expressionsNameContainIntrospection(values)) | ||
.orElse(true); | ||
} | ||
|
||
private static boolean hasSafeValidationRules(List<Argument> arguments) { | ||
RegularArgument argument = TreeUtils.argumentByKeyword("validation_rules", arguments); | ||
if (argument == null) { | ||
return false; | ||
} | ||
|
||
return extractArgumentValues(argument) | ||
.map(values -> !values.isEmpty() && | ||
(expressionsNameContainIntrospection(values) || expressionsContainsSafeRuleFQN(values))) | ||
.orElse(true); | ||
} | ||
|
||
private static Optional<List<Expression>> extractArgumentValues(RegularArgument argument) { | ||
return Optional.of(argument) | ||
.map(RegularArgument::expression) | ||
.flatMap(GraphQLIntrospectionCheck::expressionsFromListOrTuple); | ||
} | ||
|
||
private static Optional<List<Expression>> expressionsFromListOrTuple(Expression expression) { | ||
return TreeUtils.toOptionalInstanceOf(ListLiteral.class, expression) | ||
.map(ListLiteral::elements) | ||
.map(ExpressionList::expressions) | ||
.or(() -> TreeUtils.toOptionalInstanceOf(Tuple.class, expression) | ||
.map(Tuple::elements)); | ||
} | ||
|
||
private static boolean expressionsNameContainIntrospection(List<Expression> expressions) { | ||
Stream<Optional<String>> expressionsNameAndType = Stream.concat(expressions.stream() | ||
.map(GraphQLIntrospectionCheck::nameFromIdentifierOrCallExpression), | ||
expressions.stream().map(GraphQLIntrospectionCheck::nameOfType)); | ||
|
||
return expressionsNameAndType | ||
.filter(Optional::isPresent) | ||
.map(Optional::get) | ||
.map(String::toUpperCase) | ||
.anyMatch(name -> name.contains("INTROSPECTION")); | ||
} | ||
|
||
private static boolean expressionsContainsSafeRuleFQN(List<Expression> expressions) { | ||
return expressions.stream() | ||
.map(TreeUtils::getSymbolFromTree) | ||
.filter(Optional::isPresent) | ||
.map(Optional::get) | ||
.map(Symbol::fullyQualifiedName) | ||
.filter(Objects::nonNull) | ||
.anyMatch(SAFE_VALIDATION_RULE_FQNS::contains); | ||
} | ||
|
||
private static Optional<String> nameOfType(Expression expression) { | ||
return TreeUtils.toOptionalInstanceOf(NameImpl.class, expression) | ||
.map(NameImpl::type) | ||
.map(InferredType::runtimeTypeSymbol) | ||
.map(Symbol::name); | ||
} | ||
|
||
private static Optional<String> nameFromIdentifierOrCallExpression(Expression expression) { | ||
return Optional.ofNullable(TreeUtils.nameFromExpression(expression)) | ||
.or(() -> TreeUtils.toOptionalInstanceOf(CallExpression.class, expression) | ||
.map(CallExpression::callee) | ||
.map(TreeUtils::nameFromExpression)); | ||
} | ||
} |
77 changes: 77 additions & 0 deletions
77
python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S6786.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
<p>This vulnerability exposes information about all the APIs available on a GraphQL API server. This information can be used to discover weaknesses in | ||
the API that can be exploited.</p> | ||
<h2>Why is this an issue?</h2> | ||
<p>GraphQL introspection is a feature that allows client applications to query the schema of a GraphQL API at runtime. It provides a way for | ||
developers to explore and understand the available data and operations supported by the API.</p> | ||
<p>While this feature is useful, it also creates risks if not properly secured.</p> | ||
<h3>What is the potential impact?</h3> | ||
<p>An attacker can use introspection to identify all of the operations and data types supported by the server. This information can then be used to | ||
identify potential targets for attacks.</p> | ||
<h4>Exploitation of private APIs</h4> | ||
<p>Even when a GraphQL API server is open to access by third-party applications, it may contain APIs that are intended only for private use. | ||
Introspection allows these private APIs to be discovered.</p> | ||
<p>Private APIs often do not receive the same level of security rigor as public APIs. For example, they may skip input validation because the API is | ||
only expected to be called from trusted applications. This can create avenues for attack that are not present on public APIs.</p> | ||
<h4>Exposure of sensitive data</h4> | ||
<p>GraphQL allows for multiple related objects to be retrieved using a single API call. This provides an efficient method of obtaining data for use in | ||
a client application.</p> | ||
<p>An attacker may be able to use these relationships between objects to traverse the data structure. They may be able to find a link to sensitive | ||
data that the developer did not intentionally make available.</p> | ||
<h2>How to fix it</h2> | ||
<h3>Code examples</h3> | ||
<h4>Noncompliant code example</h4> | ||
<pre data-diff-id="1" data-diff-type="noncompliant"> | ||
from graphql_server.flask import GraphQLView | ||
|
||
app.add_url_rule("/api", | ||
view_func=GraphQLView.as_view( # Noncompliant | ||
name="api", | ||
schema=schema, | ||
) | ||
) | ||
</pre> | ||
<h4>Compliant solution</h4> | ||
<pre data-diff-id="1" data-diff-type="compliant"> | ||
from graphql_server.flask import GraphQLView | ||
# Only one of the following needs to be used | ||
from graphql.validation import NoSchemaIntrospectionCustomRule # graphql-core v3 | ||
from graphene.validation import DisableIntrospection # graphene v3 | ||
|
||
app.add_url_rule("/api", | ||
view_func=GraphQLView.as_view( | ||
name="api", | ||
schema=schema, | ||
validation_rules=[ | ||
NoSchemaIntrospectionCustomRule, | ||
DisableIntrospection, | ||
] | ||
) | ||
) | ||
</pre> | ||
<h3>How does this work?</h3> | ||
<h4>Disabling introspection</h4> | ||
<p>The GraphQL server framework should be instructed to disable introspection. This prevents any attempt to retrieve schema information from the | ||
server at runtime.</p> | ||
<p>Each GraphQL framework will have a different method of doing this, possibly including:</p> | ||
<ul> | ||
<li> Changing a simple boolean setting. </li> | ||
<li> Adding a middleware module to the request processing chain. </li> | ||
<li> Adding a GraphQL validator that rejects introspection keywords. </li> | ||
</ul> | ||
<p>If introspection is required, it should only be made available to the smallest possible audience. This could include development environments, | ||
users with a specific right, or requests from a specific set of IP addresses.</p> | ||
<h2>Resources</h2> | ||
<h3>Articles & blog posts</h3> | ||
<ul> | ||
<li> OWASP Web Security Testing Guide - <a | ||
href="https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/12-API_Testing/01-Testing_GraphQL#introspection-queries">Testing GraphQL</a> </li> | ||
</ul> | ||
<h3>Standards</h3> | ||
<ul> | ||
<li> OWASP Top 10 - <a href="https://owasp.org/Top10/A05_2021-Security_Misconfiguration/">2021:A5 - Security Misconfiguration</a> </li> | ||
<li> OWASP Top 10 - <a href="https://owasp.org/www-project-top-ten/2017/A3_2017-Sensitive_Data_Exposure.html">2017:A3 - Sensitive Data Exposure</a> | ||
</li> | ||
<li> OWASP Top 10 - <a href="https://owasp.org/www-project-top-ten/2017/A6_2017-Security_Misconfiguration.html">2017:A6 - Security | ||
Misconfiguration</a> </li> | ||
</ul> | ||
|
45 changes: 45 additions & 0 deletions
45
python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S6786.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
{ | ||
"title": "GraphQL introspection should not be allowed", | ||
"type": "VULNERABILITY", | ||
"status": "ready", | ||
"remediation": { | ||
"func": "Constant\/Issue", | ||
"constantCost": "1h" | ||
}, | ||
"tags": [ | ||
"cwe" | ||
], | ||
"defaultSeverity": "Major", | ||
"ruleSpecification": "RSPEC-6786", | ||
"sqKey": "S6786", | ||
"scope": "All", | ||
"quickfix": "unknown", | ||
"code": { | ||
"impacts": { | ||
"SECURITY": "MEDIUM" | ||
}, | ||
"attribute": "TRUSTWORTHY" | ||
}, | ||
"securityStandards": { | ||
"CWE": [ | ||
200 | ||
], | ||
"OWASP": [ | ||
"A3", | ||
"A6" | ||
], | ||
"OWASP Top 10 2021": [ | ||
"A5" | ||
], | ||
"PCI DSS 3.2": [ | ||
"6.5" | ||
], | ||
"PCI DSS 4.0": [ | ||
"6.2.4" | ||
], | ||
"ASVS 4.0": [ | ||
"13.1.3", | ||
"14.3.2" | ||
] | ||
} | ||
} |
Oops, something went wrong.