Skip to content

Commit 79e88a3

Browse files
authored
SONARPY-1498 Rule S6786: Python GraphQL introspection should be disabled (#1637)
1 parent 43d1bc8 commit 79e88a3

File tree

7 files changed

+827
-222
lines changed

7 files changed

+827
-222
lines changed

python-checks/src/main/java/org/sonar/python/checks/CheckList.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.sonar.python.checks.hotspots.DynamicCodeExecutionCheck;
5757
import org.sonar.python.checks.hotspots.EmailSendingCheck;
5858
import org.sonar.python.checks.hotspots.ExpandingArchiveCheck;
59+
import org.sonar.python.checks.hotspots.GraphQLIntrospectionCheck;
5960
import org.sonar.python.checks.hotspots.HardCodedCredentialsCheck;
6061
import org.sonar.python.checks.hotspots.HashingDataCheck;
6162
import org.sonar.python.checks.hotspots.HttpOnlyCookieCheck;
@@ -211,6 +212,7 @@ public static Iterable<Class> getChecks() {
211212
GenericFunctionTypeParameterCheck.class,
212213
GenericTypeWithoutArgumentCheck.class,
213214
GraphemeClustersInClassesCheck.class,
215+
GraphQLIntrospectionCheck.class,
214216
GroupReplacementCheck.class,
215217
HardCodedCredentialsCheck.class,
216218
HardcodedIPCheck.class,
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2023 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.checks.hotspots;
21+
22+
import java.util.List;
23+
import java.util.Objects;
24+
import java.util.Optional;
25+
import java.util.Set;
26+
import java.util.stream.Stream;
27+
28+
import org.sonar.check.Rule;
29+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
30+
import org.sonar.plugins.python.api.SubscriptionContext;
31+
import org.sonar.plugins.python.api.symbols.Symbol;
32+
import org.sonar.plugins.python.api.tree.Argument;
33+
import org.sonar.plugins.python.api.tree.CallExpression;
34+
import org.sonar.plugins.python.api.tree.Expression;
35+
import org.sonar.plugins.python.api.tree.ExpressionList;
36+
import org.sonar.plugins.python.api.tree.ListLiteral;
37+
import org.sonar.plugins.python.api.tree.RegularArgument;
38+
import org.sonar.plugins.python.api.tree.Tree;
39+
import org.sonar.plugins.python.api.tree.Tuple;
40+
import org.sonar.plugins.python.api.types.InferredType;
41+
import org.sonar.python.tree.NameImpl;
42+
import org.sonar.python.tree.TreeUtils;
43+
44+
@Rule(key = "S6786")
45+
public class GraphQLIntrospectionCheck extends PythonSubscriptionCheck {
46+
47+
private static final Set<String> GRAPHQL_VIEWS_FQNS = Set.of(
48+
"flask_graphql.GraphQLView.as_view",
49+
"graphql_server.flask.GraphQLView.as_view");
50+
51+
private static final Set<String> SAFE_VALIDATION_RULE_FQNS = Set.of(
52+
"graphene.validation.DisableIntrospection",
53+
"graphql.validation.NoSchemaIntrospectionCustomRule");
54+
55+
private static final String MESSAGE = "Disable introspection on this \"GraphQL\" server endpoint.";
56+
57+
@Override
58+
public void initialize(Context context) {
59+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, GraphQLIntrospectionCheck::checkGraphQLIntrospection);
60+
}
61+
62+
private static void checkGraphQLIntrospection(SubscriptionContext ctx) {
63+
CallExpression callExpression = (CallExpression) ctx.syntaxNode();
64+
Optional.ofNullable(callExpression.calleeSymbol())
65+
.map(Symbol::fullyQualifiedName)
66+
.filter(GRAPHQL_VIEWS_FQNS::contains)
67+
.filter(fqn -> !hasSafeMiddlewares(callExpression.arguments()))
68+
.filter(fqn -> !hasSafeValidationRules(callExpression.arguments()))
69+
.ifPresent(fqn -> ctx.addIssue(callExpression.callee(), MESSAGE));
70+
}
71+
72+
private static boolean hasSafeMiddlewares(List<Argument> arguments) {
73+
RegularArgument argument = TreeUtils.argumentByKeyword("middleware", arguments);
74+
if (argument == null) {
75+
return false;
76+
}
77+
78+
return extractArgumentValues(argument)
79+
.map(values -> !values.isEmpty() && expressionsNameContainIntrospection(values))
80+
.orElse(true);
81+
}
82+
83+
private static boolean hasSafeValidationRules(List<Argument> arguments) {
84+
RegularArgument argument = TreeUtils.argumentByKeyword("validation_rules", arguments);
85+
if (argument == null) {
86+
return false;
87+
}
88+
89+
return extractArgumentValues(argument)
90+
.map(values -> !values.isEmpty() &&
91+
(expressionsNameContainIntrospection(values) || expressionsContainsSafeRuleFQN(values)))
92+
.orElse(true);
93+
}
94+
95+
private static Optional<List<Expression>> extractArgumentValues(RegularArgument argument) {
96+
return Optional.of(argument)
97+
.map(RegularArgument::expression)
98+
.flatMap(GraphQLIntrospectionCheck::expressionsFromListOrTuple);
99+
}
100+
101+
private static Optional<List<Expression>> expressionsFromListOrTuple(Expression expression) {
102+
return TreeUtils.toOptionalInstanceOf(ListLiteral.class, expression)
103+
.map(ListLiteral::elements)
104+
.map(ExpressionList::expressions)
105+
.or(() -> TreeUtils.toOptionalInstanceOf(Tuple.class, expression)
106+
.map(Tuple::elements));
107+
}
108+
109+
private static boolean expressionsNameContainIntrospection(List<Expression> expressions) {
110+
Stream<Optional<String>> expressionsNameAndType = Stream.concat(expressions.stream()
111+
.map(GraphQLIntrospectionCheck::nameFromIdentifierOrCallExpression),
112+
expressions.stream().map(GraphQLIntrospectionCheck::nameOfType));
113+
114+
return expressionsNameAndType
115+
.filter(Optional::isPresent)
116+
.map(Optional::get)
117+
.map(String::toUpperCase)
118+
.anyMatch(name -> name.contains("INTROSPECTION"));
119+
}
120+
121+
private static boolean expressionsContainsSafeRuleFQN(List<Expression> expressions) {
122+
return expressions.stream()
123+
.map(TreeUtils::getSymbolFromTree)
124+
.filter(Optional::isPresent)
125+
.map(Optional::get)
126+
.map(Symbol::fullyQualifiedName)
127+
.filter(Objects::nonNull)
128+
.anyMatch(SAFE_VALIDATION_RULE_FQNS::contains);
129+
}
130+
131+
private static Optional<String> nameOfType(Expression expression) {
132+
return TreeUtils.toOptionalInstanceOf(NameImpl.class, expression)
133+
.map(NameImpl::type)
134+
.map(InferredType::runtimeTypeSymbol)
135+
.map(Symbol::name);
136+
}
137+
138+
private static Optional<String> nameFromIdentifierOrCallExpression(Expression expression) {
139+
return Optional.ofNullable(TreeUtils.nameFromExpression(expression))
140+
.or(() -> TreeUtils.toOptionalInstanceOf(CallExpression.class, expression)
141+
.map(CallExpression::callee)
142+
.map(TreeUtils::nameFromExpression));
143+
}
144+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<p>This vulnerability exposes information about all the APIs available on a GraphQL API server. This information can be used to discover weaknesses in
2+
the API that can be exploited.</p>
3+
<h2>Why is this an issue?</h2>
4+
<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
5+
developers to explore and understand the available data and operations supported by the API.</p>
6+
<p>While this feature is useful, it also creates risks if not properly secured.</p>
7+
<h3>What is the potential impact?</h3>
8+
<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
9+
identify potential targets for attacks.</p>
10+
<h4>Exploitation of private APIs</h4>
11+
<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.
12+
Introspection allows these private APIs to be discovered.</p>
13+
<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
14+
only expected to be called from trusted applications. This can create avenues for attack that are not present on public APIs.</p>
15+
<h4>Exposure of sensitive data</h4>
16+
<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
17+
a client application.</p>
18+
<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
19+
data that the developer did not intentionally make available.</p>
20+
<h2>How to fix it</h2>
21+
<h3>Code examples</h3>
22+
<h4>Noncompliant code example</h4>
23+
<pre data-diff-id="1" data-diff-type="noncompliant">
24+
from graphql_server.flask import GraphQLView
25+
26+
app.add_url_rule("/api",
27+
view_func=GraphQLView.as_view( # Noncompliant
28+
name="api",
29+
schema=schema,
30+
)
31+
)
32+
</pre>
33+
<h4>Compliant solution</h4>
34+
<pre data-diff-id="1" data-diff-type="compliant">
35+
from graphql_server.flask import GraphQLView
36+
# Only one of the following needs to be used
37+
from graphql.validation import NoSchemaIntrospectionCustomRule # graphql-core v3
38+
from graphene.validation import DisableIntrospection # graphene v3
39+
40+
app.add_url_rule("/api",
41+
view_func=GraphQLView.as_view(
42+
name="api",
43+
schema=schema,
44+
validation_rules=[
45+
NoSchemaIntrospectionCustomRule,
46+
DisableIntrospection,
47+
]
48+
)
49+
)
50+
</pre>
51+
<h3>How does this work?</h3>
52+
<h4>Disabling introspection</h4>
53+
<p>The GraphQL server framework should be instructed to disable introspection. This prevents any attempt to retrieve schema information from the
54+
server at runtime.</p>
55+
<p>Each GraphQL framework will have a different method of doing this, possibly including:</p>
56+
<ul>
57+
<li> Changing a simple boolean setting. </li>
58+
<li> Adding a middleware module to the request processing chain. </li>
59+
<li> Adding a GraphQL validator that rejects introspection keywords. </li>
60+
</ul>
61+
<p>If introspection is required, it should only be made available to the smallest possible audience. This could include development environments,
62+
users with a specific right, or requests from a specific set of IP addresses.</p>
63+
<h2>Resources</h2>
64+
<h3>Articles &amp; blog posts</h3>
65+
<ul>
66+
<li> OWASP Web Security Testing Guide - <a
67+
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>
68+
</ul>
69+
<h3>Standards</h3>
70+
<ul>
71+
<li> OWASP Top 10 - <a href="https://owasp.org/Top10/A05_2021-Security_Misconfiguration/">2021:A5 - Security Misconfiguration</a> </li>
72+
<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>
73+
</li>
74+
<li> OWASP Top 10 - <a href="https://owasp.org/www-project-top-ten/2017/A6_2017-Security_Misconfiguration.html">2017:A6 - Security
75+
Misconfiguration</a> </li>
76+
</ul>
77+
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"title": "GraphQL introspection should not be allowed",
3+
"type": "VULNERABILITY",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "1h"
8+
},
9+
"tags": [
10+
"cwe"
11+
],
12+
"defaultSeverity": "Major",
13+
"ruleSpecification": "RSPEC-6786",
14+
"sqKey": "S6786",
15+
"scope": "All",
16+
"quickfix": "unknown",
17+
"code": {
18+
"impacts": {
19+
"SECURITY": "MEDIUM"
20+
},
21+
"attribute": "TRUSTWORTHY"
22+
},
23+
"securityStandards": {
24+
"CWE": [
25+
200
26+
],
27+
"OWASP": [
28+
"A3",
29+
"A6"
30+
],
31+
"OWASP Top 10 2021": [
32+
"A5"
33+
],
34+
"PCI DSS 3.2": [
35+
"6.5"
36+
],
37+
"PCI DSS 4.0": [
38+
"6.2.4"
39+
],
40+
"ASVS 4.0": [
41+
"13.1.3",
42+
"14.3.2"
43+
]
44+
}
45+
}

0 commit comments

Comments
 (0)