# ✅ Security Tests
Security tests are the set of tests that are run against the API to ensure that it is secure.
On this page, you will find the list of all the security tests that are run by Escape as well as their description, parameters and remediations.
# Request Forgery
SSRF
# SSRF
Identifier | Severity |
---|---|
request_forgery/ssrf | HIGH |
SSRF (Server-Side Request Forgery) vulnerability occurs when the attacker can send any request as if the server was sending it.
Remediations
Generic
Basic blacklisting and regular expressions are a bad approach to mitigating SSRF.
The correct ways to prevent SSRF are:
- Whitelisting and DNS resolution: whitelist the hostname (DNS name) or IP address that your application needs to access. (Best method to prevent SSRF))
- Response handling: To prevent response data from leaking to the attacker, you must ensure that the received response is as expected. Under no circumstances should the raw response body from the request sent by the server be delivered to the client.
- Disabling unused URL schemas: if your application only uses HTTP or HTTPS to make requests, allow only these URL schemas. Once unused URL schemas are disabled, the attacker will be unable to exploit the web application to make requests using potentially dangerous schemas such as
file:///
,dict://
,ftp://
, andgopher://
. - Authentication on internal services.
Reference:
https://owasp.org/www-community/attacks/Server_Side_Request_Forgery (opens new window)
Partial SSRF
# Partial SSRF
Identifier | Severity |
---|---|
request_forgery/partial_ssrf | HIGH |
Partial Server-Side Request Forgery occurs when the attacker can manipulate a request made by the server.
Remediations
Generic
Basic blacklisting and regular expressions are a bad approach to mitigating SSRF.
The correct ways to prevent SSRF are:
- Whitelisting and DNS resolution: whitelist the hostname (DNS name) or IP address that your application needs to access. (Best method to prevent SSRF))
- Response handling: To prevent response data from leaking to the attacker, you must ensure that the received response is as expected. Under no circumstances should the raw response body from the request sent by the server be delivered to the client.
- Disabling unused URL schemas: if your application only uses HTTP or HTTPS to make requests, allow only these URL schemas. Once unused URL schemas are disabled, the attacker will be unable to exploit the web application to make requests using potentially dangerous schemas such as
file:///
,dict://
,ftp://
, andgopher://
. - Authentication on internal services.
Reference:
Configuration
{
"checks": {
"request_forgery/partial_ssrf": {
"options":{
"skip_objects": [],
},
"skip": False,
}
}
}
Options:
skip_objects : List of object that are to be skipped by the security test.
GET based CSRF
# GET based CSRF
Identifier | Severity |
---|---|
request_forgery/get_based_csrf | HIGH |
CSRF (Cross-Site Request Forgery) occurs when an external website has the ability to make API calls impersonating a user by visiting the website while being authenticated to your API.
Allowing API calls through GET
requests can lead to CSRF attacks because cookies are added automatically to GET requests made by the browser.
Remediations
Generic
Forbid API calls through GET
requests to prevent CSRF attacks.
Apollo
Pass csrfPrevention: true
to new ApolloServer()
.
Check out the the CSRF prevention documentation (opens new window) for the best CSRF prevention techniques.
Awsappsync
Make sure that your API does not use Cookie-based authentication.
There are many other ways to authenticate a user with AppSync:
- API Keys
- Amazon Cognito User Pools
- OpenID Connect
- AWS Identity and Access Management (IAM)
- AWS Lamba custom authentication
AppSync: Authorization and Authentication (opens new window)
Whichever method you use, verify that authentication occurs through headers because authentication headers are not automatically added by the targeted user browser (while Cookies are).
To avoid any risk, you can block every GET
request and allow only POST
requests, which are immune to this attack, but it comes at a cost. (see AWS pricing for the corresponding services)
- Block GET requests with AWS API Gateway (prefered method):
Put your AppSync API behind an API Gateway.
API Gateway Documentation (opens new window)
You can then configure the API Gateway to act as an HTTP Proxy to your AppSync endpoint and configure it to allow only POST requests.
- Block GET requests with AWS Web Application Firewall:
Add the following Web ACL rule in the AWS WAF Console to block every GET request to the API:
{
"Name": "block-get",
"Priority": 0,
"Statement": {
"ByteMatchStatement": {
"SearchString": "GET",
"FieldToMatch": {
"Method": {}
},
"TextTransformations": [
{
"Priority": 0,
"Type": "NONE"
}
],
"PositionalConstraint": "EXACTLY"
}
},
"Action": {
"Block": {}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "add-headers"
}
}
You can also configure it manually by using the following field values :
If:
- Field to match = HTTP method
- Positional constraint = Exactly matches string
- Search string = GET
Then:
- Block
See: AppSync API with AWS WAF (opens new window).
To learn more on AWS WAF, see: AWS WAF (opens new window).
Reference:
https://blog.doyensec.com/2021/05/20/graphql-csrf.html (opens new window)
POST based CSRF
# POST based CSRF
Identifier | Severity |
---|---|
request_forgery/post_based_csrf | MEDIUM |
The canonical content-type for GraphQL queries is JSON (application/json
). Other content-types should be rejected as they facilitate CSRF attacks (e.g., via form submission).
Remediations
Generic
Only allow requests with the Content-Type
header set to application/json
.
Apollo
Only allow requests with the Content-Type
header set to application/json
.
With Express.js, the enforce-content-type
middleware can be used to block unwanted content types.
const enforceContentType = require('enforce-content-type')
app.use(enforceContentType({
type: 'application/json'
}))
Reference:
https://blog.doyensec.com/2021/05/20/graphql-csrf.html (opens new window)
# Injection
XXE
# XXE
Identifier | Severity |
---|---|
injection/xxe | HIGH |
External entities is an element of XML documents, and attackers may replace the entity value with malicious data, alternate referrals, or compromise the security of the data the server/XML application has access to. Attackers may also use External Entities to have the web services download malicious code or content on the server for use in secondary or follow up attacks.
Remediations
Generic
To safely prevent XXE attacks, always disable DTDs (External Entities) completely. Depending on the parser, the method should be similar to the following:
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
Disabling DTDs also helps secure the parser against Denial of Services attacks such as Billion Laughs.
If it is not possible to disable DTDs completely, disable external entities and external document type declarations in the way that's specific to each parser.
Reference:
http://projects.webappsec.org/XML-External-Entities (opens new window)
XSS
# XSS
Identifier | Severity |
---|---|
injection/xss | HIGH |
XSS (Cross-site scripting) is an attack where malicious code (eg. JavaScript) is injected into the application and executed.
Remediations
Generic
Preventing cross-site scripting is trivial in some cases but can be much harder depending on the complexity of the application and the ways it handles user-controllable data.
To effectively prevent XSS vulnerabilities, use a combination of the following measures:
- Filter user input on arrival as strictly as possible, based on what you expect as legitimate input.
- Encode user-controllable data as soons as it is outputted in HTTP reponses to prevent it from being interpreted as active content (ie. code). Depending on the output context, this might require applying combinations of HTML, URL, JavaScript, and CSS encoding.
- Use the right headers to ensure that browsers interpret the responses the way you intended. For example, to prevent XSS in HTTP responses that are not supposed to contain any HTML or JavaScript, you can use the Content-Type and X-Content-Type-Options headers.
- Use Content Security Policy (CSP) to reduce the severity of any XSS vulnerability that still occurs.
Reference:
https://portswigger.net/web-security/cross-site-scripting (opens new window)
Configuration
{
"checks": {
"injection/xss": {
"options":{
"skip_objects": [],
},
"skip": False,
}
}
}
Options:
skip_objects : List of object that are to be skipped by the security test.
Stored XSS
# Stored XSS
Identifier | Severity |
---|---|
injection/stored_xss | HIGH |
Stored code injections are attacks where malicious code (such as JavaScript) is injected into the application and stored in the database to be later executed. Stored XSS is one of those attacks. Stored XSS is when an XSS vulnerability originates from the database because of malicious code previously inserted in it.
Remediations
Generic
Preventing cross-site scripting is trivial in some cases but can be much harder depending on the complexity of the application and the ways it handles user-controllable data.
To effectively prevent XSS vulnerabilities, use a combination of the following measures:
- Filter user input on arrival as strictly as possible, based on what you expect as legitimate input.
- Encode user-controllable data as soons as it is outputted in HTTP reponses to prevent it from being interpreted as active content (ie. code). Depending on the output context, this might require applying combinations of HTML, URL, JavaScript, and CSS encoding.
- Use the right headers to ensure that browsers interpret the responses the way you intended. For example, to prevent XSS in HTTP responses that are not supposed to contain any HTML or JavaScript, you can use the Content-Type and X-Content-Type-Options headers.
- Use Content Security Policy (CSP) to reduce the severity of any XSS vulnerability that still occurs.
Reference:
https://portswigger.net/web-security/cross-site-scripting (opens new window)
Configuration
{
"checks": {
"injection/stored_xss": {
"options":{
"skip_objects": [],
},
"skip": False,
}
}
}
Options:
skip_objects : List of object that are to be skipped by the security test.
SQL
# SQL
Identifier | Severity |
---|---|
injection/sql | HIGH |
A SQL injection vulnerability occurs when users can insert (or “inject”) malicious SQL code in a legit SQL query that is built from user-submitted input. A successful SQL injection exploit can read sensitive data from the database, modify database data, execute administration operations on the database (such as shutting down the DBMS), recover the content of a given file from the DBMS file system and in some cases issue commands to the operating system.
Remediations
Generic
Primary defenses:
- Use of Prepared Statements (with parameterized queries). This prevention techniques is the most effective one as it will completely shutdown any SQL injection attacks. Keep in mind that prepared statements must be used everywhere, even if no user inputted data is found in the query.
- Use of Stored Procedures.
- Allow-list Input Validation. Usage whitelist is recommended to prevent SQL injection attacks as whitelisting is more effective then black listing.
- Escaping all user supplied input.
Additional defenses:
- Enforcing Least Privilege.
- Performing Allow-list Input Validation as a secondary line of defense.
Reference:
Configuration
{
"checks": {
"injection/sql": {
"options":{
"skip_objects": [],
},
"skip": False,
}
}
}
Options:
skip_objects : List of object that are to be skipped by the security test.
NoSQL
# NoSQL
Identifier | Severity |
---|---|
injection/nosql | HIGH |
A NoSQL injection vulnerability occurs when users can insert (or “inject”) malicious NoSQL code in a legit SQL query that is built from user-submitted input. A successful NoSQL injection exploit can read sensitive data from the database, modify database data, execute administration operations on the database (such as shutting down the DBMS), recover the content of a given file from the DBMS file system and in some cases issue commands to the operating system.
Remediations
Generic
Primary defenses:
- Use a sanitization library.
- Cast the inputs to the expected type (eg: The username and password are strings so cast the variables to a string).
- Never use
where
,mapReduce
, orgroup
operators with user input: they allow the attacker to inject JavaScript and are therefore much more dangerous than others. For extra safety, setjavascriptEnabled
to false in mongod.conf (if using mongoDB). - Enforce Least Privilege.
Reference:
Configuration
{
"checks": {
"injection/nosql": {
"options":{
"skip_objects": [],
},
"skip": False,
}
}
}
Options:
skip_objects : List of object that are to be skipped by the security test.
Directory traversal
# Directory traversal
Identifier | Severity |
---|---|
injection/directory_traversal | HIGH |
Directory traversal occurs when a server allows an attacker to read a file or directories outside of the normal web server directory, and local file inclusion gives the attacker the ability to include an arbitrary local file (from the web server) in the web server's response.
Example: getProfilePicture(name: '../../../etc/password')
returns the server's /etc/password
file.
Remediations
Generic
There are multiple ways to prevent directory traversal attacks:
- Avoid using parameters entered directly by the user.
- Set up a file/folder name whitelist system: allow only certain folders and/or types of extensions, thus excluding all others.
- Compartmentalize your data and implement middlewares. These can take the form of an interface to the (potentially external) file system on which the data users may request is stored. If the attached data storage is dedicated to this purpose only and does not contain sensitive data, the risk is limited, even if a user manages to bypass the limitations that this middleware can put in place.
- Restrict access of the GraphQL worker to what is strictly necessary. By restricting as much as possible the files and folders to which the GraphQL worker has access, you reduce the range of files potentially exposed by an attack.
- Take advantage of virtualization. With virtualization, it is possible to have several virtual machines completely isolated from each other. The GraphQL worker can therefore be isolated on its own virtual machine, allowing it access only to the elements absolutely necessary for its proper execution.
Reference:
https://escape.tech/blog/file-inclusion-directory-traversal-graphql/ (opens new window)
Configuration
{
"checks": {
"injection/directory_traversal": {
"options":{
"skip_objects": [],
},
"skip": False,
}
}
}
Options:
skip_objects : List of object that are to be skipped by the security test.
Bash command
# Bash command
Identifier | Severity |
---|---|
injection/bash_command | HIGH |
A system command was successfully executed on your application's system. Command injections happen when a user manages to successfully execute arbitrary commands on the host's operating system by abusing a vulnerable endpoint.
Remediations
Generic
To prevent command injection attacks:
-Never use user-submitted input in shell commands.
-If supported by your language, add semgrep
to your development process to ensure detection of potentially vulnerable system shell calls.
-Use proper input validation techniques to detect and prevent command injection. Keep in mind the input validation should be implemented in the backend as it will be easily bypassed if done in the frontend.
Reference:
https://owasp.org/www-community/attacks/Command_Injection (opens new window)
Configuration
{
"checks": {
"injection/bash_command": {
"options":{
"skip_objects": [],
},
"skip": False,
}
}
}
Options:
skip_objects : List of objects that are to be skipped by the security test.
# Information Disclosure
Source code disclosure
# Source code disclosure
Identifier | Severity |
---|---|
information_disclosure/source_code_disclosure | HIGH |
The source code for the current page was disclosed by the web server.
Remediations
Generic
Ensure that .git
, .svn
, .htaccess
metadata files are not deployed to the web server or application server, or cannot be accessed.
Reference:
https://www.zaproxy.org/docs/alerts/41/ (opens new window)
Configuration
{
"checks": {
"information_disclosure/source_code_disclosure": {
"options":{
"size_threshold": 200,
"diff_threshold": 0.1,
"small_response_diff_threshold": 0.4,
},
"skip": False,
}
}
}
Options:
size_threshold : The threshold size indicating whether a response is small or not.
diff_threshold : The percentage by which 2 responses can differ and still be considered identical.
small_response_diff_threshold : The percentage by which 2 small responses can differ and still be considered identical.
Stack traceback
# Stack traceback
Identifier | Severity |
---|---|
information_disclosure/stack_traceback | MEDIUM |
Details about database-level or code-level errors have been found in the response. This may cause information leaks, allowing attackers to identify the exact database or dependency you are using, and can therefore lead to highly targeted attacks against your application.
Example: sending getUser(id: null)
returns { success: false, message: "SQL Error: Postgres 3.6 has encountered an error : Invalid ID."}
.
Remediations
Generic
Always avoid database or code error stack traces to be returned directly to the client.
Apollo
Never allow database or code error stack traces to be returned directly to the client.
This remediation is supported by our GraphQL Armor (opens new window) middleware.
When using Apollo you can also disable exception tracebacks in the response by either setting NODE_ENV
to production
or enforcing it:
const server = new ApolloServer({
...,
debug: false
)}
To learn more, you can read Apollo's documentation on omitting stack traces (opens new window).
*Note: if you are working in a development or staging environment, error disclosure might happen on purpose. In that case, check if this is also the case in production by:
- Using our cURL copy button to find the request that generated the stack trace.
- Changing URL and authentication tokens to match your production environment.
If the issue doesn't happen in production, you can safely ignore it.*
Graphqlyoga
Enable error masking (opens new window) to hide stack traces in production.
Since it is enabled by default, you are probably missing the variable NODE_ENV=production
in your production environment.
Hasura
Set the HASURA_GRAPHQL_DEV_MODE
env variable to false
in all user-facing environments.
Source: https://hasura.io/docs/latest/graphql/core/deployment/graphql-engine-flags/reference/
Note: if you are working in a development or staging environment, error disclosure might happen on purpose.
In that case, make sure your production environment has HASURA_GRAPHQL_DEV_MODE
set to false
Reference:
Field suggestion
# Field suggestion
Identifier | Severity |
---|---|
information_disclosure/field_suggestion | MEDIUM |
If introspection is disabled on your target, Field Suggestion still allow users infer the entire schema, with a tool like Clairvoyance (opens new window).
If you query a field with a typo, GraphQL will attempt to suggest fields close to what was requested. Example:
Error: Cannot query field "createSesion" on type "RootMutation". Did you mean "createSession", "createUser", "createFile", or "createImage"?
Remediations
Generic
Disable Field Suggestion in production.
Apollo
Block field suggestion is supported by GraphQL Armor (opens new window) middleware.
Graphqlgo
graphql-go/graphql
does not allow to disable field suggestion as of now.
However, you can filter field suggestion by discarding answers containing "Did you mean" with this middleware :
type FilterResponseWriter struct {
writer http.ResponseWriter
blacklist []string
errorPtr *bool
}
func (w FilterResponseWriter) Header() http.Header {
return w.writer.Header()
}
func (w FilterResponseWriter) Write(data []byte) (int, error) {
if *w.errorPtr {
return 0, errors.New("write error")
}
for _, s := range w.blacklist {
if bytes.Contains(data, []byte(s)) {
*w.errorPtr = true
return 0, errors.New("field not found")
}
}
return w.writer.Write(data)
}
func (w FilterResponseWriter) WriteHeader(statusCode int) {
w.writer.WriteHeader(statusCode)
}
func blockFieldSuggestion(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var error bool
newWriter := &FilterResponseWriter{writer: w, blacklist: []string{"Did you mean \\\""}, errorPtr: &error}
next.ServeHTTP(newWriter, r)
if error {
w.Write([]byte("{\"errors\":[{\"message\":\"Field not found.\"}],\"data\":null}"))
}
})
}
Then you apply the middleware to your endpoint :
func main(){
...
h := handler.New(&handler.Config{
Schema: &schema
})
http.Handle("/graphql", blockFieldSuggestion(h))
}
Graphqlyoga
Block field suggestion is supported by GraphQL Armor (opens new window) middleware.
Or, you can use the standalone envelop plugin (opens new window).
Reference:
https://blog.yeswehack.com/yeswerhackers/how-exploit-graphql-endpoint-bug-bounty/ (opens new window)
Debug mode
# Debug mode
Identifier | Severity |
---|---|
information_disclosure/debug_mode | MEDIUM |
When Debug mode is left turned on by developers, it allows attackers to gather precious information from excessive error reporting messages such as entire stack traces or tracebacks.
Remediations
Generic
Disabled Debug mode.
Reference:
GraphQL IDE
# GraphQL IDE
Identifier | Severity |
---|---|
information_disclosure/graphql_ide | LOW |
A GraphQL IDE provides an interface for users to interact with the Endpoint, but an IDE can also leave room for potential vulnerabilities.
Remediations
Generic
Disable GraphQL IDE, or restrict it. Head over to your specific engine documentation to know how to do it.
Apollo
GraphQL Playground is deprecated and disabled by default since Apollo v3. If you installed it voluntarily with the corresponding plugin, you should consider disabling it to improve security.
If you still use Apollo v2, you can disable GraphQL Playground by either:
- Setting the environment variable
NODE_ENV
toproduction
- Explicitly disabling it:
const server = new ApolloServer({ // ... playground: false, });
Source:
Introspection
# Introspection
Identifier | Severity |
---|---|
information_disclosure/introspection | INFO |
GraphQL introspection enables you to query a GraphQL server for information about the underlying schema, including data like types, fields, queries, mutations, and even the field-level descriptions. It discloses sensitive information that potentially allows an attacker to design malicious operations.
Remediations
Generic
Introspection should primarily be used as a discovery and diagnostic tool when we're in the development phase of building out GraphQL APIs. While it's still possible for bad actors to learn how to write malicious queries by reverse engineering your GraphQL API through a lot of trial and error, disabling introspection is a form of security by obscurity.
Apollo
To disable Introspection, either set NODE_ENV
to production
or enforce it :
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: false
});
Source: https://escape.tech/blog/9-graphql-security-best-practices/ (opens new window)
Ariadne
When defining the ariadne.asgi.GraphQL
app make sure to add the flag introspection=False
Awsappsync
Add ACL rule to prevent GraphQL __schema introspection queries to the API. This is achieved by blocking any HTTP body that includes the string "__schema". This would be entered into the Rule JSON editor when creating a web ACL in the AWS WAF Console.
{
"Name": "Block-Introspection",
"Priority": 5,
"Action": {
"Block": {}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "Block-Introspection"
},
"Statement": {
"ByteMatchStatement": {
"FieldToMatch": {
"Body": {}
},
"PositionalConstraint": "CONTAINS",
"SearchString": "__schema",
"TextTransformations": [
{
"Type": "NONE",
"Priority": 0
}
]
}
}
}
Don't forget to associate the previously created ACL rule with your AppSync API.
For more information refer to :
AWS AppSync - Developer Guide (opens new window)
Graphene
When using Graphene, here is how you would disable introspection for your schema.
from graphql import validate, parse
from graphene import ObjectType, Schema, String
from graphene.validation import DisableIntrospection
class MyQuery(ObjectType):
name = String(required=True)
schema = Schema(query=MyQuery)
# introspection queries will not be executed.
validation_errors = validate(
schema=schema.graphql_schema,
document_ast=parse('THE QUERY'),
rules=(
DisableIntrospection,
)
)
Graphqlgo
graphql-go/graphql
does not allow you to disable Introspection.
However, you can disable it with a custom middleware filtering the keyword __schema
:
func blockIntrospection(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := ioutil.ReadAll(r.Body)
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
body_lower := bytes.ToLower(bodyBytes)
subslice := "__schema"
if bytes.Contains(body_lower, []byte(subslice)) {
no_introspection := "{\"errors\": [{\"message\": \"Introspection is disabled.\"}],\"data\": null}"
w.Write([]byte(no_introspection))
} else {
next.ServeHTTP(w, r)
}
})
}
...
func main(){
...
h := handler.New(...) // GraphQL handler
http.Handle("/graphql", blockIntrospection(h))
}
Graphqljava
GraphQLSchema schema = GraphQLSchema.newSchema()
.query(StarWarsSchema.queryType)
.fieldVisibility( NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY )
.build();
Source: https://www.graphql-java.com/documentation/v11/execution/
Graphqlphp
use GraphQL\GraphQL;
use GraphQL\Validator\Rules\DisableIntrospection;
use GraphQL\Validator\DocumentValidator;
DocumentValidator::addRule(new DisableIntrospection());<code>
</code>
Source: https://webonyx.github.io/graphql-php/security/#disabling-introspection
Hasura
Hasura allows you to control who can run an introspection query.
To do so:
- Go to Project Console > Security Settings > Schema Introspection
- Select a role (e.g., guest)
- Check "Disabled"
See the official guide (opens new window) for more information.
Ruby
class MySchema < GraphQL::Schema
disable_introspection_entry_points if Rails.env.production?
end
Source: https://github.com/rmosolgo/graphql-ruby/blob/master/guides/schema/introspection.md#disabling-introspection
Reference:
# HTTP
SSL certificate expiration
# SSL certificate expiration
Identifier | Severity |
---|---|
http/ssl_certificate_expiration | HIGH |
The SSL certificate found on the website is no longer valid. This is most probably due to the fact that the SSL certificate is expired.
Remediations
Generic
Purchase a new SSL certificate.
Reference:
https://www.thesslstore.com/blog/what-happens-when-your-ssl-certificate-expires/ (opens new window)
HeartBleed
# HeartBleed
Identifier | Severity |
---|---|
http/heartbleed | HIGH |
The TLS implementation in OpenSSL 1.0.1 before 1.0.1g does not properly handle Heartbeat Extension packets, which allows remote attackers to obtain sensitive information from process memory via crafted packets that trigger a buffer over-read, potentially disclosing sensitive information.
Remediations
Generic
To effectively prevent HeartBleed attacks:
- Update to OpenSSL 1.0.1g or later.
- Re-issue HTTPS certificates.
- Change asymmetric private keys and shared secret keys, since these may have been compromised with no evidence of corruption in the server log files.
Reference:
https://www.cvedetails.com/cve-details.php?t=1&cve_id=CVE-2014-0160 (opens new window)
Unsecure HTTP
# Unsecure HTTP
Identifier | Severity |
---|---|
http/unsecure_http | MEDIUM |
This security best practice should be enforced by your organization, at least for your API routes.
Here are 4 risk examples when allowing GraphQL communication using unsecure HTTP
:
- Man in the Middle attacks: hackers can intercept and alter data from a legitimate request.
- Misuse of data: confidential information can be accessed by hackers.
- Downranking of websites: your website can be considered insecure by search engines and rated as not trustworthy.
- Loss of customers' trust: without a secure HTTPS padlock displayed on your website, users may rightfully consider it to be unsafe.
Using HSTS (opens new window) is not a solution to this problem, as it won't protect against MITM attacks and regular "public wifi sniffing" until connection has been upgraded to HTTPS. As recommended by Google (opens new window), make sure that no cookies are being sent through HSTS, which is likely the case if you are using a GraphQL API over HSTS.
Remediations
Generic
Enforce using HTTPS (using an SSL certificate) in order to protect your users' connections. In most cases, this must be done at ingress/(reverse-)proxy level.
- Kubernetes Ingress: Google Cloud documentation (opens new window).
- Caddy: CaddyServer documentation (opens new window).
- Nginx Reverse Proxy: Nginx documentation (opens new window).
- Express: Node, Express, SSL Certificate: run HTTPS server from scratch in 5 steps (opens new window).
- graphql-go/graphql: golang-tls (opens new window).
If you are using Let's Encrypt certificates, make sure to authorize HTTPS to HTTPS redirections on the path /.well-known/acme-challenge/
to avoid any issues.
Reference:
https://developers.google.com/search/docs/advanced/security/https (opens new window)
CRLF
# CRLF
Identifier | Severity |
---|---|
http/crlf | MEDIUM |
CRLF occurs when an attacker can abuse the carriage return character (\r) and a newline character (\n) in an HTTP request in order to inject new headers or a new body for the HTTP request. This attack is a very dangerous attack as it can give the attacker the ability to create whatever request he wants.
Remediations
Generic
The only way to prevent CRLF attacks is to carefully sanitize every message that is sent by the client.
Reference:
http://www.watchfire.com/resources/HTTPResponseSplitting.pdf (opens new window)
Security Headers
# Security Headers
Identifier | Severity |
---|---|
http/security_headers | LOW |
- Cache-Control:
- The HTTP 'Cache-Control' header is used to specify directives for caching mechanisms.
- The server did not return (or returned an invalid) 'Cache-Control' header, which means pages containing sensitive information could be stored client-side and then be exposed to unauthorized persons.
Content-Type:
- The Content-Type header was either missing or empty.
X-Content-Type-Options:
- The Anti-MIME-Sniffing header X-Content-Type-Options was not set to ‘nosniff’.
- This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.
Strict-Transport-Security:
- HTTP Strict Transport Security (HSTS) is a web security policy mechanism whereby a web server declares that complying user agents (such as a web browser) are to interact with it using only secure HTTPS connections (i.e. HTTP layered over TLS/SSL).
- HSTS is an IETF standard track protocol specified in RFC 6797.
CookiesSecure:
- A cookie has been set without the secure flag, which means that the cookie can be accessed via unencrypted connections.
CookiesHttpOnly:
- A cookie has been set without the HttpOnly flag, which means that JavaScript code can access the cookie.
- If a malicious script runs on this page, then the cookie will be accessible and can be transmitted to another hacker-controlled site. If this is a session cookie, then session hijacking may be possible.
VersionDisclosure:
- The web/application server is leaking server version information via one or more HTTP response headers.
- Access to such information may facilitate attackers identifying other frameworks/components your web application is reliant upon, and the vulnerabilities of such components may be subject to the leaked information.
Remediations
Generic
Cache-Control:
- Whenever possible, ensure the cache-control HTTP header is set with no-cache, no-store, must-revalidate, and that the pragma HTTP header is set with no-cache.
Content-Type:
- Ensure each page sets the specific and appropriate content-type value for the delivered content.
X-Content-Type-Options:
- Ensure that the application/web server sets the Content-Type header appropriately and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.
- If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all or that can be directed by the web application/web server to not perform MIME-sniffing.
Strict-Transport-Security:
- Ensure that your web server, application server, load balancer, etc., are configured to enforce Strict-Transport-Security.
CookiesSecure:
- Whenever a cookie contains sensitive information or is a session token, it should always be passed using an encrypted channel.
- Ensure that the secure flag is set for cookies containing such sensitive information
CookiesHttpOnly:
- Ensure that the HttpOnly flag is set for all cookies.
VersionDisclosure:
- Remove headers disclosing server-side softwares version.
Apollo
When using Apollo with Express.js, helmet (opens new window) can take care of the security headers.
const helmet = require("helmet");
...
app.use(helmet);
Awsappsync
- Add security headers with the API Gateway
Put your AppSync API behind an API Gateway and configure it to act as a proxy to your AppSync endpoint (e.g., using the HTTP Proxy feature).
API Gateway Documentation (opens new window)
Then you can manually add headers to each resource. (There is only one resource if your API Gateway is only used to proxy a single AppSync endpoint)
Here is an example of security headers you can add :
Cache-Control: no-store
Content-Security-Policy: default-src 'self'
Strict-Transport-Security: max-age=63072000
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
- Add security headers using only AWS AppSync
AWS AppSync currently does not allow to append custom headers to every response.
However, custom response headers can be configured individually for every resolver by using response mapping templates.
To do this, go to:
- AppSync > {Your App} > Schema
For every attached resolver :
- Go to the resolver configuration
- In the "Configure the response mapping template" section, add this :
$util.http.addResponseHeader("Cache-Control", "no-store")
$util.http.addResponseHeader("Content-Security-Policy", "default-src 'self'")
$util.http.addResponseHeader("Strict-Transport-Security", "max-age=63072000")
$util.http.addResponseHeader("X-Content-Type-Options", "nosniff")
$util.http.addResponseHeader("X-Frame-Options", "SAMEORIGIN")
$util.http.addResponseHeader("X-XSS-Protection", "1; mode=block")
You can safely ignore this warning if you did this for every single resolver.
However, it may still appear here as GraphQL requests like query { __typename }
are not associated with a resolver; therefore, you cannot add custom response headers. (Which doesn't matter as such requests cannot leak data as no actual field is queried)
AWS AppSync adds support for custom response headers (opens new window)
HTTP helpers in $util.http (opens new window)
Resolver Mapping Template Programming Guide (opens new window)
Graphene
To add Security Headers to Django, follow this guide :
How to Score A+ for Security Headers on Your Django Website (opens new window)
For Flask, use Google's flask-talisman (opens new window)
Graphqlgo
You can use a HTTP middleware to add security headers.
For instance, with srikrsna/security-headers (opens new window):
import (
secure "github.com/srikrsna/security-headers"
)
h := handler.New(&handler.Config{
Schema: &schema,
...
})
s := &secure.Secure{
STSIncludeSubdomains: true,
STSPreload: true,
STSMaxAgeSeconds: 90,
FrameOption: secure.FrameAllowFrom,
FrameOrigin: "https://example.com/",
ContentTypeNoSniff: true,
XSSFilterBlock: true,
HPKPPins: []string{
"HBkhsug765gdKHhvdj6jdb7jJh/j+soZS7sWs=",
"hjshHSHU68hbdkHhvdkgksgsg+jd/jHJ68HBH=",
},
HPKPMaxAge: 5184000,
HPKPReportURI: "https://www.example.org/hpkp-report",
HPKPIncludeSubdomains: true,
ExpectCTMaxAge: 5184000,
ExpectCTEnforce: true,
ExpectCTReportUri: "https://www.example.org/ct-report",
ReferrerPolicy: secure.ReferrerStrictOriginWhenCrossOrigin,
}
http.Handle("/graphql", s.Middleware()(h))
http.ListenAndServe(":8082", nil)
Reference:
- Security Headers: OWASP: Security Headers (opens new window)
- Cache: OWASP: Session Management Cheat Sheet (opens new window)
- Cookies: OWASP: Testing for Cookies attributes (opens new window)
- Version Disclosure: https://www.tenable.com/plugins/was/98618s (opens new window)
Open redirection
# Open redirection
Identifier | Severity |
---|---|
http/open_redirection | LOW |
Open redirects are part of the OWASP 2010 Top Ten vulnerabilities and occur when an application allows user-supplied input (e.g. http://nottrusted.com) to control an offsite redirect. For example an attacker could supply a user with the following link http://example.com/example.php?url=http://malicious.example.com.
Remediations
Generic
This check looks at user-supplied input in query string parameters and POST data to identify where open redirects might be possible. It is generally an accurate way to find where 301 or 302 redirects could be exploited by spammers or phishing attacks. To avoid the open redirect vulnerability, parameters of the application script/program must be validated before sending 302 HTTP code (redirect) to the client browser. Implement safe redirect functionality that only redirects to relative URL's, or a list of trusted domains
Reference:
CORS
# CORS
Identifier | Severity |
---|---|
http/cors | LOW |
Attackers can exploit CORS (Cross-Origin Resource Sharing) misconfigurations on the web server to perform CSRF (Cross-Site Request Forgery) attacks and send unauthorized commands from an authenticated user session.
Remediations
Generic
Configure the 'Access-Control-Allow-Origin' HTTP header to a more restrictive set of domains, or remove all CORS headers entirely, to allow the web browser to enforce the Same Origin Policy (SOP) in a more restrictive manner.
If your API is public and used in websites you don't control yourself, you want to allow any request origin and you can safely ignore this alert.
Apollo
Configure the 'Access-Control-Allow-Origin' HTTP header to a more restrictive set of domains, or remove all CORS headers entirely, to allow the web browser to enforce the Same Origin Policy (SOP) in a more restrictive manner.
For instance, with apollo-server-express
, you can restrain request origin to only a few whitelisted domains:
await server.start();
const corsOptions = {
origin: ["https://www.your-app.example", "https://studio.apollographql.com"]
};
server.applyMiddleware({
app,
cors: corsOptions,
path: "/graphql",
});
Source: https://www.apollographql.com/docs/apollo-server/security/cors/ (opens new window).
If your API is public and used in websites you don't control yourself, you want to allow any request origin and you can safely ignore this alert.
Awsappsync
Add CORS headers with the API Gateway.
Put your AppSync API behind an API Gateway and configure it to act as a proxy to your AppSync endpoint (e.g., using the HTTP Proxy feature).
To learn how to do so, see AWS's API Gateway documentation (opens new window).
You can then manually enable CORS for each resource (only for one if you created the gateway for a single AppSync endpoint):
API Gateway console > {your api gateway} > Resources > {your created resource} > Actions : Enable CORS
Reference:
https://portswigger.net/web-security/cors (opens new window)
# DOS
Unreachable server
# Unreachable server
Identifier | Severity |
---|---|
dos/unreachable_server | HIGH |
The server went offline for too long. This can be caused by a server crash, a network issue, or a misconfiguration.
Often, this is a sign of a bigger issue.
Remediations
Generic
We recommend you to investigate two possible causes:
- Unhandled exceptions in your code: Verify that your code is handling exceptions properly.
- Uncontrolled resource consumption: Verify what resolver has been used to leverage the data source.
Reference:
Security timeout
# Security timeout
Identifier | Severity |
---|---|
dos/security_timeout | HIGH |
Requests that take a long time to process can be used by attackers to create Denial-of-Service (DoS) situations.
This security test is based on an arbitrary timeout threshold that might not match your application's requirements. To learn how to change it, head over to the configuration section below.
Example: Querying getAllUsers(){ contacts { contacts }}
returns a response after 15s.
Remediations
Generic
Implement a server timeout. For example, a server configured with a 5 seconds timeout would stop the execution of any query that takes over 5 seconds.
Pros: -Simple to implement. -Most security strategies use a timeout as a final layer of protection.
Cons: -Damage can already have been done before the timeout kicks in. -Can trigger other issues. Stoping connection after a certain time may result in strange behaviors and corrupt data.
Warning : When a timeout is configured on the server, the socket may be closed while the underlying request continues. Make sure that the request is actually canceled.
Awsappsync
AWS AppSync enforces a timeout of 30s on each request by default.
If your API sits behind an API Gateway (opens new window), you can configure a different (but lower than the hard 30s limit) timeout in the AWS API Gateway console by following this path:
AWS API Gateway console > {Your App} > Resources > Integration Request > "Use default timeout".
Graphqlgo
Implement a server timeout by following this guide:
The complete guide to Go net/http timeouts - Cloudfare blog (opens new window)
Hasura
Hasura allows you to set a custom query timeout.
To do so: -Go to Project Console > Security Settings > API Limits. -Click on "Global". -Set a timeout (e.g., 10s).
Stepzen
There is no known remediation for StepZen.
Reference:
Configuration
{
"checks": {
"dos/security_timeout": {
"options":{
"threshold_low": 1,
"threshold_medium": 2,
"threshold_high": 4,
},
"skip": False,
}
}
}
Options:
threshold_low : Duration of a request (in seconds) before raising a low level alert
threshold_medium : Duration of a request (in seconds) before raising a low level alert
threshold_high : Duration of a request (in seconds) before raising a low level alert
Recursive Fragment
# Recursive Fragment
Identifier | Severity |
---|---|
dos/recursive_fragment | MEDIUM |
This is a DoS vulnerability that allows an attacker with specifically designed queries to cause stack overflow panics. Any user with access to the GraphQL handler can send these queries and cause stack overflows. This in turn could potentially compromise the ability of the server to serve data to its users.
Remediations
Generic
Implement a maximum recursion limit.
Reference:
Directive overloading
# Directive overloading
Identifier | Severity |
---|---|
dos/directive_overloading | MEDIUM |
Directive Overloading occurs when a user can send a query with many consecutive directives and overload the engine handling those directives.
Remediations
Generic
Limit the number of directives allowed in a query. This should be handled by the GraphQL engine while parsing the document, otherwise this can lead to a heap overflow.
Apollo
Upgrade to GraphQL>=16.0.0 if you are not already up to date. You can also use our GraphQL Armor (opens new window) middleware to limit the number of directives allowed in a query.
Graphqlyoga
Upgrade to GraphQL>=16.0.0 if you are not already up to date. You can also use our GraphQL Armor (opens new window) middleware to limit the number of directives allowed in a query.
Reference:
Configuration
{
"checks": {
"dos/directive_overloading": {
"options":{
"threshold": 50,
},
"skip": False,
}
}
}
Options:
threshold : Maximum number of directives allowed before raising an alert in the fast check.
Automatic Persisted Queries
# Automatic Persisted Queries
Identifier | Severity |
---|---|
dos/automatic_persisted_queries | LOW |
The absence of Automatic Persisted Queries can cause backend performance problems at scale.
GraphQL clients send queries to Apollo Servers as HTTP requests, including the GraphQL query string. Depending on your GraphQL schema, the size of a valid query string might be arbitrarily large. As query strings become larger, increased latency and network usage can noticeably degrade client performance. A persisted query is a query string cached on the server-side, along with its unique identifier (SHA-256 hash of the query). Clients can send this identifier instead of the full query string, drastically reducing request sizes.
To make a query string persist, your GraphQL server must first receive it from a requesting client. Each unique query string must therefore be sent to the server at least once. Once a client has sent a query string to persist, any other client executing that query can benefit from APQ.
Remediations
Generic
To improve network performance for large query strings, enable APQ if your GraphQL server supports it.
Apollo
Enable Automatic Persisted queries. For a complete guide on how to do so, see Apollo's Automatic Persisted Queries documentation (opens new window).
Dgraph
For a complete guide on the matter, see dgraph's Persisted Queries documentation (opens new window).
Gqlgen
Enable Automatic persisted Queries For a complete guide on how to do so, see gqlgen's Automatic Persisted Queries documentation (opens new window).
Graphene
Automatic Persisted Queries are not supported by Graphene alone.
However, if you use Graphene with Django (opens new window), the django-graphql-persist (opens new window) library allows you to implement Automatic Persisted Queries.
Graphqlapiforwp
Automatic Persisted Queries on graphqlapiforwp are different than with other engines. Learn more on graphqlapiforwp's Persisted Query guide (opens new window). You can also implement custom persisted queries using WP GraphQL Lock (opens new window).
Ruby
Add graphql-persisted_queries to your Gemfile gem 'graphql-persisted_queries'
and add the plugin to your schema class:
class GraphqlSchema < GraphQL::Schema
use GraphQL::PersistedQueries
end
Pass the :extensions
argument as part of a context to all calls of GraphqlSchema#execute
,
usually it happens in GraphqlController
, GraphqlChannel
and tests:
GraphqlSchema.execute(
params[:query],
variables: ensure_hash(params[:variables]),
context: {
extensions: ensure_hash(params[:extensions])
},
operation_name: params[:operationName]
)
Source: https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries.
Reference:
https://www.apollographql.com/docs/apollo-server/performance/apq/ (opens new window)
Circular introspection
# Circular introspection
Identifier | Severity |
---|---|
dos/circular_introspection | INFO |
A circular introspection has been performed on the system. This can be exploited to conduct a Denial of Service attack.
The GraphQL introspection is done recursively on your schema. By calling several objects that reference each other, an attacker can create a circular reference.
__schema {
types {
fields {
type {
fields {
type ... and so on infinitely
}
}
}
}
}
Creating a circular reference will increase the size of the returned document exponentially. The larger your schema is, the bigger the document will be.
This DOS attack is hard to exploit as the size of the query might be limited by a server before reaching your backend.
Remediations
Generic
To prevent circular introspection from being abused:
- Limit the maximum depth of the introspection query.
- Limit the maximum elapsed time to execute a GraphQL query.
- Disable your introspection.
# Access Control
Tenant isolation
# Tenant isolation
Identifier | Severity |
---|---|
access_control/tenant_isolation | HIGH |
Uses the rules defined by the users to detect same object instances detected by two different users whereas this is prohibited. According to the rules provided in the configuration file, the same instance or object can be detected by two different users which is prohibited.
Remediations
Generic
When accessing the application via GraphQL, we must validate whether or not the user has access to the requested elements from the schema. Especially we must implement access control policies on every path of the Graph leading considered field or object.
The authorization logic belongs to the business logic layer, and from there it is accessed by GraphQL. This way, the application can have a single source of truth for authorization, which can then be used for other access points.
Among the several access control policies we can implement in our application, the two most popular ones are Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC).
- With Role-Based Access Control, we grant permissions based on roles, and then assign the roles to the users. For instance, WordPress has an
administrator
role with access to all resources, and theeditor
,author
,contributor
, andsubscriber
roles, which each restrict permissions in varying degrees, such as being able to create and publish a blog post, just create it, or just read it. - With Attribute-Based Access Control, permissions are granted based on metadata that can be assigned to different entities, including users, assets, and environment conditions (such as the time of the day or the visitor's IP address). For instance, in WordPress, the capability
edit_others_posts
is used to validate whether the user can edit other users' posts.
In general terms, ABAC is preferable over RBAC because it allows us to configure permissions with fine-grained control, and the permission is unequivocal in its objective.
Apollo
See Apollo's Access Control Documentation (opens new window). For large scale applications, you might want to use a specific package like (opens new window) for easy Access Control Management.
Awsappsync
Appsync provides several methods for protecting critical information.
- For implementing fine-grained access control, see https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#fine-grained-access-control
Hasura
See Hasura's detailed documentation for Authorization Management (opens new window)
Reference:
https://blog.logrocket.com/authorization-access-control-graphql/ (opens new window)
Configuration
{
"checks": {
"access_control/tenant_isolation": {
"parameters":{
"objects": ['**value**'],
"scalars": {'**value**': ['**value**']},
},
"skip": False,
}
}
}
Examples:
Accessiblity of objects private instances for differents users
{
... Authentication settings ...
... Other configuration settings ...
"checks": {
... Other checks ...
"access_control/tenant_isolation": {
"parameters": {
"objects": [
"MyVeryPrivateData", # Record access to object `MyVeryPrivateData`
# if two different users access the same object
# (i.e. two different users access the same self bound private data)
# the an alert will be raised.
],
"scalars": {
"Post": [
"createdBy", # Record access to field `createdBy` of object `Post`
# if two different users can access the same scalar value
# an alert will be raised.
]
}
}
}
... Other checks ...
}
... Other configuration settings ...
}
Parameters:
objects : A list of objects that are private.
{'objects': ['**value**']}
scalars : A list of object fields that are private.
{'scalars': {'**value**': ['**value**']}}
Secrets leaks
# Secrets leaks
Identifier | Severity |
---|---|
access_control/secrets_leaks | HIGH |
The database exposes sensitive data to the public, such as secrets, private keys, tokens, passwords, etc. This security check detects this sensitive data.
Remediations
Generic
If there is a chance that unauthorized individuals have already accessed the secret, revoke it immediately and notify the concerned team.
If the leak happens through another vulnerability, such as a Server-Side Request Forgery (opens new window), fix this issue to avoid further secret leaks.
Have your developers implement these best practices to avoid leaking secrets: -Inside logs: Keeping Sensitive Data Out of Logs (opens new window). -Inside commits: Pre-commit (opens new window) or Gitleaks (opens new window). -In the configuration, using an environment variable manager like Hashicorp Vault (opens new window).
Apollo
If there is a chance that unauthorized individuals have already accessed the secret, revoke it immediately and notify the concerned team.
If the leak happens through another vulnerability, such as a Server-Side Request Forgery (opens new window), fix this issue to avoid further secret leaks.
Have your developers implement these best practices to avoid leaking secrets: -Inside logs: keeping sensitive data out of logs (opens new window). -Inside commits: Pre-commit (opens new window) or Gitleaks (opens new window). -In the configuration, using an environment variable manager like Hashicorp Vault (opens new window).
See Apollo's Access Control Documentation (opens new window). For large scale applications, you'll want to use a specific package like GraphQL Shield (opens new window) for quick and easy Access Control management.
Awsappsync
If there is a chance that unauthorized individuals have already accessed the secret, revoke it immediately and notify the concerned team.
If the leak happens through another vulnerability, such as a Server-Side Request Forgery (opens new window), fix this issue to avoid further secret leaks.
Have your developers implement these best practices to avoid leaking secrets: -Inside logs: keeping sensitive data out of logs (opens new window) -Inside commits: Pre-commit (opens new window) or Gitleaks (opens new window) -In the configuration, using an environment variable manager like Hashicorp Vault (opens new window)
Appsync provides several other methods for protecting critical information. -To learn more on implementing fine-grained access control, head over to https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#fine-grained-access-control. -To learn more on filtering critical data directly from responses, see https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#aws-appsync-filtering-information.
Hasura
If there is a chance that unauthorized individuals have already accessed the secret, revoke it immediately and notify the concerned team.
If the leak happens through another vulnerability, such as a Server-Side Request Forgery (opens new window), fix this issue to avoid further secret leaks.
Have your developers implement these best practices to avoid leaking secrets: -Inside logs: keeping sensitive data out of logs (opens new window) -Inside commits: Pre-commit (opens new window) or Gitleaks (opens new window) -In the configuration, using an environment variable manager like Hashicorp Vault (opens new window)
See Hasura's detailed documentation for authorization management (opens new window).
Reference:
https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html (opens new window)
Configuration
{
"checks": {
"access_control/secrets_leaks": {
"options":{
"blacklist": [],
},
"skip": False,
}
}
}
Options:
blacklist : List of elements to ignore.
Private fields
# Private fields
Identifier | Severity |
---|---|
access_control/private_fields | HIGH |
According to the rules provided in the configuration file, objects
and object fields
can be accessed by unauthorized users.
Remediations
Generic
When accessing the application through GraphQL, it is important to validate whether or not the user is given access to the requested elements from the schema. Access control policies must therefore be implemented on every path of the Graph leading to the given field or object.
The authorization logic belongs to the business logic layer, and from there it is accessed by GraphQL. This way, the application can have a single source of truth for authorization, which can then be used for other access points.
Among the several access control policies that can be implemented in an application, the two most popular ones are Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC).
- With Role-Based Access Control, permissions are granted based on roles which are later assigned to the users. For instance, WordPress has an
administrator
role with access to all resources, and theeditor
,author
,contributor
, andsubscriber
roles which each restrict permissions in various degrees, such as being able to create and publish a blog post, just create it, or simply read it. - With Attribute-Based Access Control, permissions are granted based on metadata that can be assigned to different entities including users, assets, or environment conditions (the time of the day or the visitor's IP address for example). In the WordPress example, the capability
edit_others_posts
is used to validate whether or not the user can edit other users' posts.
For most use cases, ABAC is preferable to RBAC as it allows for finely tuned permissions with explicit goals.
Apollo
See Apollo's Access Control documentation (opens new window). For large scale applications, you'll want to use a specific package like GraphQL Shield (opens new window) for quick and easy Access Control management.
Awsappsync
Appsync provides several methods for protecting critical information. -To learn how to implement fine-grained access control, head over to https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#fine-grained-access-control.
Hasura
See Hasura's detailed documentation for Authorization Management (opens new window).
Reference:
https://blog.logrocket.com/authorization-access-control-graphql/ (opens new window)
Configuration
{
"checks": {
"access_control/private_fields": {
"parameters":{
"**user**": ,
},
"options":{
"empty_values_are_positive": False,
},
"skip": False,
}
}
}
Examples:
Accessiblity of private objects in a GraphQL API for not authenticated users.
{
"auth": {
"noauth": { #
"tech": "noauth" # Default value on a new application
}, #
... REDACTED AUTH ...
},
"users": {
"noauth": { #
"auth": "noauth" # Default value on a new application
}, #
"exampleUser": {
... REDACTED AUTH ...
}
}
... Other configuration settings ...
"checks": {
... Other checks ...
"access_control/private_fields": {
"noauth": { <--- Triggering on a "users" object key
"Query": [ <--- Object name
"getUsersById", <--- Field name
"getAllUsers" # Here, the noauth user is not supposed to
# have access to the "getUsersById" and "getAllUsers" queries.
],
"Mutation": [
...
],
"User": [ <--- Object name
"id" # Here, the noauth user is not supposed to
# have access to the "id" field of the "User" object
]
}
}
... Other checks ...
}
... Other configuration settings ...
}
Parameters:
user : An object where the key are GraphQL objects name and the value is an array of field's name that the user is not supposed to have access to.
{'__user': {'**value**': ['**value**']}}
Options:
empty_values_are_positive : When the API returns a None value without error is the field considered to be successfully accessed ?
Private data
# Private data
Identifier | Severity |
---|---|
access_control/private_data | HIGH |
According to the rules provided in the configuration file, private data
can be accessed by unauthorized users.
Remediations
Generic
The best way to ensure that private data can only be accessed by authorized users is by implementing a proper access control system. To do so, access control must be applied to every object and every link in the graphQL schema.
Apollo
See Apollo's Access Control documentation (opens new window). For large scale applications, you'll want to use a specific package like GraphQL Shield (opens new window) for quick and easy Access Control management.
Awsappsync
Appsync provides several methods for protecting critical information.
- To learn more on implementing fine-grained access control, head over to https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#fine-grained-access-control.
Hasura
See Hasura's detailed documentation for Authorization Management (opens new window).
Reference:
https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html (opens new window)
Configuration
{
"checks": {
"access_control/private_data": {
"parameters":{
"**user**": ,
},
"skip": False,
}
}
}
Parameters:
user : A list of values the user should never be able to access.
{'__user': ['**value**']}
Public Unsafe Route
# Public Unsafe Route
Identifier | Severity |
---|---|
access_control/public_unsafe_route | MEDIUM |
A route that mutate application data should not be public.
Remediations
Generic
Restrict access to the route, using an authentication middleware for example.
Reference:
https://owasp.org/Top10/A01_2021-Broken_Access_Control/ (opens new window)
IDOR
# IDOR
Identifier | Severity |
---|---|
access_control/idor | MEDIUM |
We are able to enumerate a field without any limits.
Remediations
Generic
Change the argument to one that cannot be enumerated.
Configuration
{
"checks": {
"access_control/idor": {
"options":{
"threshold_res": 0.8,
"threshold_enum": 0.6,
},
"skip": False,
}
}
}
Options:
threshold_res : Rate of correct responses to an argument being enumerated to raise an alert.
threshold_enum : Rate of iterable values of a field to be considered iterable.
# Introspection
Typing inconsistency
# Typing inconsistency
Identifier | Severity |
---|---|
introspection/typing_inconsistency | MEDIUM |
Look for typing misconfigurations by checking if a mutation parameter with the wrong parameter type succeeds.
Remediations
Generic
Do not resolve queries with a wrong argument type.
Apollo
Apollo doesn't allow arguments of the wrong type by default.
Example:
{
"errors": [
{
"message": "String cannot represent a non string value: 123",
"extensions": {
"code": "GRAPHQL_VALIDATION_FAILED"
}
}
]
}
This error should appear if you are using Apollo.
Awsappsync
AWS AppSync doesn't allow arguments of the wrong type by default.
Example:
{
"data": null,
"errors": [
{
"path": null,
"locations": [
{
"line": 1,
"column": 18,
"sourceName": null
}
],
"message": "Validation error of type WrongType: argument 'a' with value 'StringValue{value='4'}' is not a valid 'Int' @ 'testType'"
}
]
}
Reference:
https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html (opens new window)
Configuration
{
"checks": {
"introspection/typing_inconsistency": {
"options":{
"skip_objects": [],
},
"skip": False,
}
}
}
Options:
skip_objects : List of object that are to be skipped by the security test.
Zombie objects
# Zombie objects
Identifier | Severity |
---|---|
introspection/zombie_objects | LOW |
Zombie objects are objects that are not accessible from any query, mutation,or subscription, but are still declared in your GraphQL schema. Most of the time, zombie objects reveal legacy or unused part of your codebase. Because they are not maintained nor patched, they are a privileged vector of attack and represent a severe security risk for your application.
Remediations
Generic
Remove zombie objects from your GraphQL schema and associated code if they are indeed useless in your codebase, otherwise make them accessible from at least one query, mutation or subscription.
Reference:
https://cwe.mitre.org/data/definitions/561.html (opens new window)
Unhandled operation
# Unhandled operation
Identifier | Severity |
---|---|
introspection/unhandled_operation | INFO |
Some operations can be found in the introspection but have no handler implemented.
Remediations
Generic
Either remove the operations from your schema or create a handler for them.
Undefined objects
# Undefined objects
Identifier | Severity |
---|---|
introspection/undefined_objects | INFO |
Undefined objects are objects that use the built-in GraphQL object type instead of referencing a custom one. They can be at the root of security issues due to their unstructured nature.
Remediations
Generic
Enforce strong typing in your GraphQL objects.
Typing inconsistency (interceptor)
# Typing inconsistency (interceptor)
Identifier | Severity |
---|---|
introspection/typing_inconsistency_interceptor | INFO |
This security check verifies that all the data returned in the response matches its expected type, as defined in the introspection.
Remediations
Generic
Update your resolver to make the introspection type match the actual returned type.
Reference:
https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html (opens new window)
Imputable JSON scalars
# Imputable JSON scalars
Identifier | Severity |
---|---|
introspection/imputable_json_scalars | INFO |
Imputable JSON scalars are an arbitrary GraphQL scalar type that allow users to return JSON objects from the GraphQL schema. It is a bad practice and may represent an unhandled data leak risk for your application.
Remediations
Generic
When possible, use typed input objects instead.
# Complexity
Pagination limit
# Pagination limit
Identifier | Severity |
---|---|
complexity/pagination_limit | MEDIUM |
A security check that ensures that an attacker cannot launch a DoS attack by quering all the nodes in a connection.
Remediations
Generic
To prevent such an attack, limit pagination variables.
Configuration
{
"checks": {
"complexity/pagination_limit": {
"options":{
"skip_objects": [],
},
"skip": False,
}
}
}
Options:
skip_objects : List of object that are to be skipped by the security test.
Large JSON
# Large JSON
Identifier | Severity |
---|---|
complexity/large_json | MEDIUM |
Inputting a very large sized JSON as an argument.
Remediations
Generic
Limit the maximum size of a JSON that can be inputted.
Reference:
https://gusralph.info/file-upload-checklist/ (opens new window)
Configuration
{
"checks": {
"complexity/large_json": {
"options":{
"skip_objects": [],
},
"skip": False,
}
}
}
Options:
skip_objects : List of object that are to be skipped by the security test.
Field limit
# Field limit
Identifier | Severity |
---|---|
complexity/field_limit | MEDIUM |
Attackers may craft complex queries by requesting a significant number of fields.
This could lead to potential DoS attacks or information leakage.
Remediations
Generic
Limit query complexity by using a library specific to your engine.
Apollo
Add a module to compute query complexity and set a threshold so that overly broad requests get canceled.
For a user-friendly module which requires no schema modification whatsoever, check out the graphql-validation-complexity (opens new window) module.
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const ComplexityLimitRule = createComplexityLimitRule(1000);
const apolloServer = new ApolloServer({
...
validationRules: [ComplexityLimitRule],
});
For a more customizable module that lets you manually configure the cost of each field/type of your schema, take a look at the graphql-cost-analysis (opens new window) module.
This second option is best suited for a more realistic complexity estimator as all fields may not be equal in terms of complexity.
To learn more about complexity estimation, you can read: Securing Your GraphQL API from Malicious Queries (opens new window).
Source: https://escape.tech/blog/9-graphql-security-best-practices/ (opens new window).
Graphene
With graphene-django
, it is possible to implement a custom GraphQL backend to limit query complexity, such as this one:
graphene-django query cost analysis / complexity limits (opens new window).
Graphqlphp
use GraphQL\GraphQL;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\DocumentValidator;
$rule = new QueryComplexity($maxQueryComplexity = 100);
DocumentValidator::addRule($rule);
GraphQL::executeQuery(/*...*/);
Hasura
Limit the number of fecthed fields with Response Limiting. To learn how to implement it, check out Hasura's Response Limiting guide (opens new window).
Reference:
Configuration
{
"checks": {
"complexity/field_limit": {
"options":{
"threshold": 100,
},
"skip": False,
}
}
}
Options:
threshold : Maximum fields before raising an alert (-1 = infinite).
Depth limit
# Depth limit
Identifier | Severity |
---|---|
complexity/depth_limit | MEDIUM |
GraphQL does not limit how deep a query can be.
Since GraphQL schemas are often cyclic graphs, a query like the one below could technically be crafted:
query IAmEvil {
author(id: "abc") {
posts {
author {
posts {
author {
posts {
author {
# Can go on as deep as the client wants
}
}
}
}
}
}
}
}
This can lead to potential DoS attacks or information leakage.
Remediations
Generic
Secure your application by preventing clients from abusing query depth. To do so, add a Maximum Query Depth limit based on your knowledge of the schema and how deep you believe a legitimate query could go. By analyzing the query document's abstract syntax tree (AST), a GraphQL server is able to reject or accept a request based on its depth.
For instance, using graphql-ruby with the max query depth setting set to 3
gives the following result:
{
"errors": [
{
"message": "Query has depth of 6, which exceeds max depth of 3"
}
]
}
Since the document's AST is analyzed statically, the query does not even execute, which adds no load on your GraphQL server.
Depth alone is often not enough to cover all abusive queries. For example, a query requesting an enormous amount of nodes on the root will be very expensive but unlikely to be blocked by a query depth analyzer.
Apollo
This remediation is supported by our GraphQL Armor (opens new window) middleware.
You can also limit the depth of queries with the very light graphql-depth-limit (opens new window) library.
Check how deep legitimate queries should be and set a maximum depth accordingly.
import depthLimit from 'graphql-depth-limit'
const server = new ApolloServer({
// ...
validationRules: [depthLimit(5)]
});
Source: https://escape.tech/blog/9-graphql-security-best-practices/ (opens new window).
Awsappsync
For now, AppSync does not allow out-of-the-box query depth limit configuration.
This can however be bypassed by implementing depth limit using Velocity Template Language VTL in Escape's Resolver. Below is an example of using the Matches regex to determine the length of selectionSetList. This example enforces a depth limit of 3 and can be added inside of an AppSync resolver function.
#set($selectionSetList = $ctx.info.selectionSetList)
#foreach ($item in $selectionSetList)
#if($item.matches(".*/.*/./."))
$util.error("Error: Queries with more than 3 levels found. At level - $item")
#end
#end
#return($ctx.prev.result)
Source: https://robertbulmer.medium.com/aws-appsync-rate-and-max-depth-limiting-c536e5ba43d6.
Graphene
With graphene-django
, it is possible to implement a custom GraphQL backend to limit query complexity, such as this one:
graphene-django query cost analysis / complexity limits (opens new window).
Graphqlphp
use GraphQL\GraphQL;
use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\DocumentValidator;
$rule = new QueryDepth($maxDepth = 10);
DocumentValidator::addRule($rule);
GraphQL::executeQuery(/*...*/);
Source: https://github.com/webonyx/graphql-php/blob/master/docs/security.md#limiting-query-depth.
Graphqlyoga
This remediation is supported by our GraphQL Armor (opens new window) middleware.
You can also use the standalone envelop plugin (opens new window).
Hasura
Hasura allows for manual query depth limit configuration directly in the security settings:
- Go to Project Console > Security Settings > API Limits.
- Click on "Global".
- Set a depth limit (e.g., 3).
Reference:
https://www.howtographql.com/advanced/4-security/ (opens new window)
Configuration
{
"checks": {
"complexity/depth_limit": {
"options":{
"threshold": 3,
},
"skip": False,
}
}
}
Options:
threshold : Maximum depth before raising an alert (-1 = infinite).
Width limit
# Width limit
Identifier | Severity |
---|---|
complexity/width_limit | LOW |
GraphQL defines the maximum width of a query as the maximum number of subfields queried from one field.
If no limit is set on query width, clients may therefore craft a complex query that could lead to potential DoS attacks or information leakage.
Remediations
Generic
Set a threshold on the maximum number of subfields that can be queried simultaneously.
Apollo
Add a module to compute query complexity and set a threshold on this complexity so that overly broad requests get canceled.
For a user-friendly module which requires no schema modification whatsoever, check out the graphql-validation-complexity (opens new window) module.
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const ComplexityLimitRule = createComplexityLimitRule(1000);
const apolloServer = new ApolloServer({
...
validationRules: [ComplexityLimitRule],
});
For a more customizable module that lets you manually configure the cost of each field/type of your schema, take a look at the graphql-cost-analysis (opens new window) module.
This second option is best suited for a more realistic complexity estimator as all fields may not be equal in terms of complexity.
To learn more on complexity estimation, you can read: Securing Your GraphQL API from Malicious Queries (opens new window).
Graphene
With graphene-django
, it is possible to implement a custom GraphQL backend to limit query complexity, such as this one:
graphene-django query cost analysis / complexity limits (opens new window).
Hasura
Hasura allows you to set a width (=node) limit.
To do so: -Go to Project Console > Security Settings > API Limits. -Click on "Global". -Set a node limit (e.g., 15).
Reference:
Configuration
{
"checks": {
"complexity/width_limit": {
"options":{
"threshold": 20,
},
"skip": False,
}
}
}
Options:
threshold : Maximum width before raising an alert (-1 = infinite).
Cyclic query
# Cyclic query
Identifier | Severity |
---|---|
complexity/cyclic_query | LOW |
GraphQL allows developers to nest queries and objects. Attackers can abuse this feature by calling a deeply nested query similar to a recursive function and causing a Denial of Service by exhausting CPU, memory, or other resources.
Remediations
Generic
Although the ability to fetch a cyclic query is necessary for some GraphQL application, it is best to always implement security measures to control these cyclic queries: -Set query timeouts: restrict the time a query is allowed to run. -Set a maximum query depth: limit the tolerated depth of queries in order to prevent overly deep queries from abusing resources. -Set a maximum query complexity: limit the complexity of queries to mitigate the abuse of GraphQL resources. -Use server-time-based throttling: limit the amount of server time a user can consume. -Use query-complexity-based throttling: limit the total complexity of queries a user can consume.
Apollo
Although the ability to fetch a cyclic query is necessary for some GraphQL application, it is best to always implement security measures to control these cyclic queries: -Set a maximum query depth: limit the depth of allowed queries in order to prevent overly deep queries from abusing GraphQL resources.
You can easily limit query depth with the very light graphql-depth-limit (opens new window) library.
Add a maximum query depth limit based on your knowledge of the schema and how deep you believe a legitimate query could go.
import depthLimit from 'graphql-depth-limit'
const server = new ApolloServer({
...
validationRules: [depthLimit(5)]
});
Source: https://escape.tech/blog/9-graphql-security-best-practices/ (opens new window).
-Set maximum query complexity: limit the complexity of allowed queries to prevent overly complex queries from abusing GraphQL resources.
To do so, add a module to compute the complexity of each query and set a threshold on this complexity so that overly broad requests get canceled.
For a user-friendly module which requires no schema modification whatsoever, check out the [graphql-validation-complexity](https://github.com/4Catalyzer/graphql-validation-complexity) module.
```javascript
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const ComplexityLimitRule = createComplexityLimitRule(1000);
const apolloServer = new ApolloServer({
...
validationRules: [ComplexityLimitRule],
});
```
For a more customizable module that lets you manually configure the cost of each field/type of your schema, take a look at the [graphql-cost-analysis](https://github.com/pa-bru/graphql-cost-analysis) module.
This second option is best suited for a more realistic complexity estimator as all fields may not be equal in terms of complexity.
To learn more about complexity estimation, you can read: [Securing Your GraphQL API from Malicious Queries](https://www.apollographql.com/blog/graphql/security/securing-your-graphql-api-from-malicious-queries/).
Source: <https://escape.tech/blog/9-graphql-security-best-practices/>.
Graphene
With graphene-django
, it is possible to implement a custom GraphQL backend to limit query complexity, such as this one:
graphene-django query cost analysis / complexity limits (opens new window).
Reference:
Character limit (interceptor)
# Character limit (interceptor)
Identifier | Severity |
---|---|
complexity/character_limit_interceptor | LOW |
Clients using GraphQL may craft a query with a huge amount of characters. This could lead to potential DoS attacks or information leakage.
Remediations
Generic
Reject requests containing more than a certain number of characters.
For instance, 3,000
is a coherent threshold for characters.
This naïve approach will not prevent clever hackers from crafting costly requests if short field names are available. One should prefer the better but more difficult to implement "query complexity" method and set a complexity threshold instead.
Apollo
Reject requests containing more than a certain number of characters.
For instance, 3,000
is a coherent threshold for characters.
This remediation is supported by GraphQL Armor (opens new window) middleware.
Otherwise, this is an example code for Apollo with Express.js:
import bodyParser from "body-parser";
...
app.use(bodyParser.json({ limit : 3000, type : '*/*' }));
Note: If your application is designed to send big graphql queries, you might want to put a higher character limit.
This naïve approach will not prevent clever hackers from crafting costly requests if short field names are available. One should additionally use the better but more difficult to implement "query complexity" method and set a complexity threshold.
Awsappsync
Add ACL rule to prevent requests bigger than a threshold. (e.g., 3000 characters) This would be entered into the Rule JSON editor when creating a web ACL in the AWS WAF Console :
{
"Name": "BodySizeRule",
"Priority": 1,
"RuleAction": {
"Block": {}
},
"Statement": {
"SizeConstraintStatement": {
"ComparisonOperator": "GE",
"FieldToMatch": {
"Body": {}
},
"Size": 3000,
"TextTransformations": [
{
"Priority": 0,
"Type": "NONE"
}
]
}
},
"VisibilityConfig": {
"CloudWatchMetricsEnabled": true,
"MetricName": "BodySizeRule",
"SampledRequestsEnabled": true
}
}
Don't forget to associate the previously created ACL rule with your AppSync API.
For more information refer to :
AWS AppSync - Developer Guide (opens new window)
Graphqlgo
You can limit query size with a net/http middlware.
func limitBodySize(next http.Handler, limit int64) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
limitedBody := http.MaxBytesReader(w, r.Body, limit)
bodyBytes, err := ioutil.ReadAll(limitedBody)
limitedBody.Close()
if err != nil {
message := "{\"errors\": [{\"message\": \"Request too large.\"}],\"data\": null}"
w.Write([]byte(message))
} else {
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
next.ServeHTTP(w, r)
}
})
}
func main(){
...
h := handler.New(&handler.Config{
Schema: &schema
})
http.Handle("/graphql", limitBodySize(h, 3000))
}
Graphqlyoga
Reject requests containing more than a certain number of characters.
This remediation is supported by GraphQL Armor (opens new window) middleware.
Otherwise, you can use the standalone envelop plugin (opens new window) directly.
Reference:
Configuration
{
"checks": {
"complexity/character_limit_interceptor": {
"options":{
"threshold": 15500,
},
"skip": False,
}
}
}
Options:
threshold : Maximum characters before raising an alert (-1 = infinite)
Batch Limit
# Batch Limit
Identifier | Severity |
---|---|
complexity/batch_limit | LOW |
Some GraphQL engines support batching of multiple queries into a single request. This allows users to request multiple objects or multiple instances of objects efficiently. However, an attacker can leverage this feature to evade many security measures, including rate limiting.
Remediations
Generic
Disable or limit queries batching in your GraphQL engine.
Apollo
Disable query batching in ApolloServer
constructor options.
This remediation is supported by GraphQL Armor (opens new window) middleware.
Otherwise, you can switch off batching in the ApolloServer
constructor options.
const server = new ApolloServer({
...
allowBatchedHttpRequests: false,
)}
Source: https://www.apollographql.com/docs/apollo-server/requests/#batching (opens new window)
Reference:
https://lab.wallarm.com/graphql-batching-attack/ (opens new window)
Configuration
{
"checks": {
"complexity/batch_limit": {
"options":{
"threshold": 15,
},
"skip": False,
}
}
}
Options:
threshold : Maximum number of batched documents allowed to be sent
Alias limit
# Alias limit
Identifier | Severity |
---|---|
complexity/alias_limit | LOW |
GraphQL supports the aliasing of multiple sub-queries into a single query. It allows requesting multiple instances of objects efficiently and without conflicts. However, attackers can leverage this feature to bypass many security measures, including rate limiting.
Query example:
query {
a: myself{username}
b: myself{username}
...
}
Response example:
{
"data": {
"a": {"username":"EscapeAdmin"},
"b": {"username":"EscapeAdmin"},
...
}
}
Remediations
Generic
Limit query aliasing in your GraphQL Engine to prevent aliasing-based attacks.
Apollo
Adding a limit on request complexity is a much better alternative to disabling aliasing, which can trigger other issues.
This remediation is supported by our GraphQL Armor (opens new window) middleware.
When using Apollo, you can also add a module to compute the query complexity and set a threshold so that overly broad requests get canceled.
For a user-friendly module which requires no schema modification whatsoever, check out the graphql-validation-complexity (opens new window) module.
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const ComplexityLimitRule = createComplexityLimitRule(1000);
const apolloServer = new ApolloServer({
...
validationRules: [createComplexityLimitRule(1000)],
});
For a more customizable module that lets you manually configure the cost of each field/type of your schema, take a look at the graphql-cost-analysis (opens new window) module.
This second option is best suited for a more realistic complexity estimator as all fields may not be equal in terms of complexity.
To learn more about complexity estimation, you can read: Securing Your GraphQL API from Malicious Queries (opens new window)
Reference: