GraphQL specification

GraphQL is an open source standard for query language and execution engine for application services to describe their data model, what kind of interactions can be performed with that data, and what requirements those interactions have in a flexible way.

On the surface, GraphQL looks very much like the OAS. A GraphQL API is a documented interface that clients calling a service can consume to make requests to the service and for the service to respond to those requests. The API document describes data objects as schema definitions as well as operations that can be performed on those data objects. However, there are some fundamental differences the GraphQL specification and the OAS, such as:

  • The OAS provides a granular standard way of describing APIs. In GraphQL the type systems described in schema definition files are application-specific. This allows for the flexibility in tailoring the GraphQL API to the exact needs of the application, but the ensuing variation and tailor-made details reduce the footprint of the standard, leaving it on a much more generic level compared to the OAS.
  • REST APIs following the OAS can have multiple endpoints. GraphQL APIs have only one endpoint.
  • The OAS uses eight standard HTTP protocol methods (GET, POST, PUT, PATCH, HEAD, TRACE, OPTIONS, DELETE) as API operations. GraphQL has only three types of operations (query, mutation, subscription), all specific to GraphQL.
  • The OAS supports describing the required authentication methods as part of the documented API contract. GraphQL completely externalizes security measures outside the documented API contract, meaning that they are not described and cannot be deduced from the API definition itself.
  • REST APIs following the OAS specify the full contract they offer, including both capabilities and accepted input as well as the outgoing response in full. The APIs of GraphQL services do define the capabilities, but it is the GraphQL clients that specify which of those capabilities they consume and to what extent: the response to a client contains only what it asked for, instead of a predetermined set of data, only part of which the client might use.

Types of GraphQL files

A GraphQL API is the documentation of a type system of a GraphQL service. Unlike with the OAS, GraphQL API documentation can span and include several different types of files:

  • GraphQL schema definition files
  • GraphQL data definition files
  • GraphQL federation files (supergraphs)

All these files can use any of the file formats .graphql, .graphqls, .gql, .gqls, .sdl.

In addition, GraphQL clients can query the capabilities of a GraphQL service through introspection, which produces introspection files. However, GraphQL introspection files are not supported on 42Crunch Platform, as their contents depend on the client's query and are not a complete and reliable server-side representation of the whole service.

GraphQL schema definition files are full APIs that can be deployed live to serve API consumers as stand-alone APIs. However, GraphQL data definition files are best considered as API "fragments" and they only really make sense as a part of GraphQL federation.

When you import a GraphQL file, you must state what type of GraphQL file it is. This is then compared against the contents of your file and if it looks like the selected type does not match, the import fails. This ensures that files are not imported with a mismatched stated type, which could cause problems further on when using the platform features.

GraphQL federation

GraphQL federation, such as Apollo Federation, allows you to combine multiple files, each documenting one or more aspects or capabilities that your GraphQL services offer. This is especially suited for microservice architecture and in effect works like microservices for GraphQL.

GraphQL federation has two key parts:

  • Subgraphs: Subgraphs describe the capabilities (such as operations and resources) of the service.
    • They define the input and output of requests and responses, as well as the data objects in the backend services.
    • A GraphQL federation can have any number of subgraphs.
    • Subgraphs can be a mix of GraphQL schema definitions, GraphQL data definitions, and other resource files. In 42Crunch Platform, only GraphQL schema definitions and data definitions are currently supported.
  • Supergraph: A supergraph is a programmatically produced file that orchestrates the API traffic within the federation: where should each request be routed, where to fetch the requested resources, and how is the response returned to the API consumer.
    • If subgraphs describe the operations and resources of a federation, the supergraph describes how they are federated.
    • While a federation can have any number of subgraphs, it can have only one supergraph.

The federation is based on the supergraph containing a reference URL for each subgraph. This federation URL is the endpoint that should be called to reach the resources defined in the subgraph. Federation URLs are not exposed directly to API consumers. Instead, the supergraph exposes a single endpoint for the whole federation behind it that API consumers call. This means that API consumers do not have to care about the underlying complexity and where exactly the data they are requesting is located, they can simply send their requests to the supergraph which takes care of routing it correctly.

GraphQL federation is currently supported in Security Audit and API Scan in 42Crunch Platform. GraphQL federation is not yet supported in API Protection or in IDEs.

GraphQL validation frameworks

The constraints that GraphQL specification provides by default may not always be adequate to business needs. For example, the five built-in scalar types — String, Int, Float, Boolean, and ID — carry no semantic or boundary constraints. This means an attacker can supply wildly out-of-range or malformed values as input, and a poorly written resolver may return uncontrolled content in output. To counter this, companies may decide to take advantage of additional validation frameworks to provide more constrained validation than pure GraphQL validation would.

Security Audit recognizes GraphQL directives from two frameworks: the 42Crunch framework, based on advising the using of custom scalars, and the GraphQL Java Extended Validation framework.

  • By default, Security Audit uses the 42Crunch framework that recommends using custom scalars to enhance the quality of the data definition. The audit flags any use of a built-in scalar type that could have been replaced by a more specific custom scalar and verifies that every custom scalar carries the minimum required constraints.
  • Alternatively, you can specify that your APIs are following GraphQL Java Extended Validation, in which case Security Audit will follow the guidelines of that framework. The main difference is that directives are placed on the argument or input field, not on the scalar definition and the framework does not require the use of custom scalars, because the constraints are being applied at the point of use.

Currently, APIs can only use a single framework at a time. For more details see, x-42c-graphql-validation-framework.

For GraphQL APIs, Security Audit uses cycle detection instead of a fixed depth limit, because all types in a GraphQL API must be unique. If the audit encounters a type it has already checked, it raises cycle warning and stops.

GraphQL specification provides for both built-in and custom scalar types, and both are supported in Security Audit. However, nested lists ([[String]], [[[Int]]]) require explicit @list directives on each nesting level. For complex nested list scenarios, we recommend using custom scalars.

Custom scalar libraries

Custom scalars are one of the most powerful features of GraphQL and allow companies to create bespoke scalars to address their business. However, different developers might implement a scalar for the same purpose differently, which could lead to inconsistencies or even security risks if constraints needed for security reasons are omitted from the implementation. Defining the same custom scalars over and over again also means a lot of duplicate work. Reusable custom scalar libraries allow defining a scalar once and using it many times, with only one place to update when updates are needed.

42Crunch Platform supports defining reusable custom scalars and managing them in a centralized way. The custom scalars are defined using the standard GraphQL schema definition language in a separate file that is uploaded to the platform. Security Audit and API Scan automatically take into account any custom scalar files that they find, with no additional changes required to the GraphQL API definitions themselves: simply using such a custom scalar in the API definition is enough.

The screenshots shows the custom scalar library view, with an example custom scalarfile opened for viewing.

Collection and global custom scalars

Custom scalar libraries can be defined on two levels, depending on the scope of their use:

  • API collection level: The custom scalar file is added locally to the same API collection as the GraphQL APIs that use those scalars
    • Security Audit and API Scan automatically use these scalars when processing GraphQL APIs from this API collection, but ignore the scalars when processing APIs from any other API collection
    • Collection custom scalars can be managed by anyone with at least read-write access to the API collection in question, see Sharing APIs and access level
    • Collection custom scalars are best suited where teams are working on a set of APIs that work together to form a distinct whole

      The screenshot shows an API collection where the GraphQL APIs have one collection-level custom scalar library and one global organization-level file applied to them.

  • Organization level: The custom scalar file is added as a top-level resource global to the whole organization
    • Security Audit and API Scan automatically use these scalars for any GraphQL APIs in any API collection in your organization (exemptions can be declared)
    • Global custom scalars can only be managed by organization administrators but can be viewed by anyone in the organization, just like with data dictionaries
    • Global custom scalars are best suited for custom scalars used across all or most of your GraphQL APIs, or when you want a particular implementation to be used

A custom scalar defined in a custom scalar file but absent from the GraphQL definition being audited is ignored, not reported as not being used. This keeps a custom scalar library file usable across teams who each use a different subset of it.

Custom scalar libraries define a baseline, not a ceiling: a field of any of the defined types can be constrained further when used in a schema definition or data definition file, and often should be.

Metadata directives in custom scalar files

42Crunch provides three optional directives that you can use in your custom scalar files. These directives help API Security Audit and API Scan to correctly interpret and apply your custom scalars, reducing potential unnecessary noise in audit and scan reports.

@baseType directive

On its own, a custom scalar might be too opaque: tools cannot tell simply from the scalar name what kind of constraint is appropriate on that field, which family of constraint to ask for when none is present, or what shape of value to generate. For example, Security Audit might report that a value called LargeDecimalValue — that a human would deduce is always going to be numeric — is missing string constraints, and API Scan might try to generate a string value for it.

The @baseType directive expresses what a custom scalar really is, stating which built-in GraphQL type a custom scalar behaves like.

scalar LargeDecimalValue
  @baseType(is: [Float])

Some scalars genuinely accept more than one representation: for example, a scalar for an account number might internally be represented as a string, but an integer value might also be accepted for caller convenience. In this case, the scalar has two base types, both types needing constraints:

"Service account number. Held as a string; an integer is accepted for caller convenience."

scalar ServiceAccountNumber
  @baseType(is: [String, Int])
  @stringOrNumberValue(pattern: "^[0-9]{10}$", min: 0, max: 9999999999)
  @sample(value: "0123456789")
  @sample(value: 123456789)

The @baseType directive uses enum values named after the built-in scalars in GraphQL, the values are not type references. The enum values are always written as a list, even when there is a single value: @baseType(is: [String]).

The @baseType directive provides the context for interpreting the scalar, not the full constraint. The custom scalar file states the widest bound the type permits, and individual fields can and should narrow it as far as its own business rules require. The more local constraint takes precedence: if a GraphQL API defines stricter limit than the custom scalar library, it is the value defined in the API that Security Audit and API Scan will use.

The @baseType directive also enables the right treatment of aliased scalars: an alias declares its target as its base type and inherits that target's constraints instead of restating them.

@sample directive

As already stated, a custom scalar might not tell tools what kind of values should be generated for accepted input. From a tool's perspective, constraints might not be enough either: for example, constraints tell API scan what should be rejected, but not that much what kind of value it should send for a request to be accepted.

The @sample directive helps tools, such as API Scan to send values that are valid for the custom scalar. This is particularly useful for scalars that do not carry a clear pattern, such as URL: while a URL does have a standard format, generating a URL that actually exists is unlikely without a sample. In such a case, the request to a URL invented by API Scan is liable to be rejected by the gateway before it ever reaches the API, resulting in a conformance failure that says nothing about the security of the API. With the @sample directive providing an example URL that is reachable, API Scan can provide a known-good baseline to mutate away from when generating the actual test requests.

scalar Url
  @baseType(is: [String])
  @sample(value: "https://api.example.com/v1/test_resources")

Without a sample, scan has to invent a value for a Url field, and the request is liable to be rejected by the gateway before it ever reaches the API — a conformance failure that says nothing about the API's security. With one, the happy-path request is valid by construction, and negative tests have a known-good baseline to mutate away from. The @sample directive is repeatable, so a scalar accepting more than one shape can offer a sample for each:

"Service account number. Held as a string; an integer is accepted for caller convenience."

scalar ServiceAccountNumber
  @baseType(is: [String, Int])
  @stringOrNumberValue(pattern: "^[0-9]{10}$", min: 0, max: 9999999999)
  @sample(value: "0123456789")
  @sample(value: 123456789)

Defining a sample for a custom scalar in a definition file applies to every field of that type across its scope, so one line can cover a scalar used in dozens of subgraphs. Just like with the @baseType directive, defining the example value in the custom scalar files sets the default for the type, it does not displace any sample values already defined in your schemas. Where a particular field needs a more specific value than defined in the custom scalar file, you can add an example directly for that field and API Scan will use the more local value when scanning the API.

@externalValidation directive

Some scalars have no safe pattern. For example, a regular expression for date-time as per RFC 3339 rejects values that a Java date library coerces without complaint, which can be a problem with GraphQL federation where same GraphQL files can be used across wildly different implementations. The above example could mean that a date-time pattern could be deemed good in Security Audit and result in false rejections in API Scan when used with one implementation, but could still pose security risk to an implementation using a Java date library. Such false confidence is worse than not having a pattern at all.

The @externalValidation directive lets you define exemptions to normal validation in Security Audit and outsource the validation of a scalar to named code residing outside your GraphQL schemas. The directive indicates that to Security Audit that it should not validate this scalar as the validation is done elsewhere, and the constraint (handledBy: ) references the source of the validation:

scalar Url
  @baseType(is: [String])
  @externalValidation(handledBy: "graphql-java-extended-scalars@24.0")
  @sample(value: "https://api.example.com/v1/test_resources")

The @externalValidation directive cannot be used to circumvent governance:

  • This directive can only be used in the global custom scalar library, which are managed by organization administrators. Trying to upload a custom scalar file that includes @externalValidation is rejected with a clear error, so while a team can define and constrain their own scalars freely, they cannot grant themselves an exemption from being constrained at all.
  • The directive names what performs the validation. The constraint handledBy: "graphql-java-extended-scalars@24.0" is a claim that a governance team can check and verify that the validation is adequate.

An exemption with the @externalValidation directive is the last resort, not the first. If a deliberately loose pattern would catch the overwhelming majority of malformed input without rejecting anything valid, that is preferable. A scalar that a custom scalar library simply leaves unbounded is not a candidate for exemption at all: declare its base type and let the fields in each GraphQL API definition carry the bounds that its own business rules imply.

The @externalValidation directive should always be paired with a @sample directive. Without a pattern, API Scan has nothing to generate from; with a known-good value provided as an example, it can still build a valid happy-path request. The combination of no pattern, but a working example value is better than either an invented regular expression or nothing at all.