All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.com.netflix.graphql.dgs.mvc.GraphQLCSRFRequestHeaderValidationRule.kt Maven / Gradle / Ivy

There is a newer version: 9.1.3
Show newest version
/*
 * Copyright 2022 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.netflix.graphql.dgs.mvc

import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.util.StringUtils

/**
 * Implementation of a [GraphQLRequestHeaderValidationRule] that will make sure that the HTTP Request
 * Has either a content-type that enforces a _pre-flight_ check or has a preflight header, defined below.
 * A content-type that enforces a _pre-flight_ check shouldn't be any of the content-types defined in [NON_PREFLIGHTED_CONTENT_TYPES].
 * Which are the _pre-flight_ headers we support? See [GRAPHQL_PREFLIGHT_REQUESTS_HEADERS]
 *
 * What is a _pre-flight_ check?
 * It is a check that a common browser will do to enforce a [CORS policy](https://github.com/apollographql/apollo-server/security/advisories/GHSA-2p3c-p3qw-69r4).
 *
 * **Note**, is the responsibility of the applications to define a sensible CORS policy that will prevent a CSRF attack.
 */
class GraphQLCSRFRequestHeaderValidationRule : GraphQLRequestHeaderValidationRule {
    companion object {
        // CSRF Prevention Request Headers
        @Suppress("MemberVisibilityCanBePrivate")
        const val HEADER_X_APOLLO_OPERATION_NAME = "x-apollo-operation-name"

        @Suppress("MemberVisibilityCanBePrivate")
        const val HEADER_APOLLO_REQUIRE_PREFLIGHT = "apollo-require-preflight"

        @Suppress("MemberVisibilityCanBePrivate")
        const val HEADER_GRAPHQL_REQUIRE_PREFLIGHT = "graphql-require-preflight"

        /**
         * Headers, defined as `content-type`, that will not enforce a _preflight_ check by browsers.
         * In other words, if the `content-type` of the request matches any of these the browser will not enforce a CORS
         * check.
         *
         * See [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).
         */
        val NON_PREFLIGHTED_CONTENT_TYPES = setOf(
            MediaType.APPLICATION_FORM_URLENCODED,
            MediaType.MULTIPART_FORM_DATA,
            MediaType.TEXT_PLAIN
        )

        /**
         * Headers that should be available in case the request has either no `content-type` or one
         * that matches any of the [NON_PREFLIGHTED_CONTENT_TYPES].
         * Clients, which is the case with Apollo Client for example, *should always*  define a `content-type` even
         * if they are doing a `GET` request.
         *
         * Apollo Client Web, Apollo iOS, and Apollo Kotlin always send `x-apollo-operation-name` for example.
         *
         * See [](https://github.com/apollographql/apollo-server/blob/version-4/packages/server/src/preventCsrf.ts)
         */
        val GRAPHQL_PREFLIGHT_REQUESTS_HEADERS = listOf(
            HEADER_APOLLO_REQUIRE_PREFLIGHT,
            HEADER_X_APOLLO_OPERATION_NAME,
            HEADER_GRAPHQL_REQUIRE_PREFLIGHT
        ).map { it.lowercase() }.toSet()

        /**
         * > We don't want random websites to be able to execute actual GraphQL operations
         * > from a user's browser unless our CORS policy supports it. It's not good
         * > enough just to ensure that the browser can't read the response from the
         * > operation; we also want to prevent CSRF, where the attacker can cause side
         * > effects with an operation or can measure the timing of a read operation. Our
         * > goal is to ensure that we don't run the context function or execute the
         * > GraphQL operation until the browser has evaluated the CORS policy, which
         * > means we want all operations to be pre-flighted. We can do that by only
         * > processing operations that have at least one header set that appears to be
         * > manually set by the JS code rather than by the browser automatically.
         *
         * > POST requests generally have a content-type `application/json`, which is
         * > sufficient to trigger preflighting. So we take extra care with requests that
         * > specify no content-type or that specify one of the three non-preflighted
         * > content types. For those operations, we require (if this feature is enabled)
         * > one of a set of specific headers to be set. By ensuring that every operation
         * > either has a custom content-type or sets one of these headers, we know we
         * > won't execute operations at the request of origins who our CORS policy will
         * > block.
         *
         * From [Apollo Server](https://github.com/apollographql/apollo-server/blob/version-4/packages/server/src/preventCsrf.ts)
         */
        fun assertGraphQLCsrf(headers: HttpHeaders) {
            val contentType: MediaType? = headers.contentType
            if (contentType != null && isPreflightedContentType(contentType)) {
                // We managed to parse a MIME type that was not one of the
                // CORS-safe-listed ones. (Probably application/json!) That means that if
                // the client is a browser, the browser must have applied CORS
                // preflighting, and we don't have to worry about CSRF.
                return
            }
            // Either there was no content-type, or the content-type parsed properly as
            // one of the three CORS-safelisted values. Let's look for another header that
            // (if this was a browser) must have been set by the user's code and would
            // have caused a preflight.
            if (containsCSRFinFlightHeader(headers)) {
                return
            }
            throw DgsGraphQLRequestHeaderValidator.GraphQLRequestHeaderRuleException(
                "Expecting a CSRF Prevention Header but none was found, " +
                    "supported headers are $GRAPHQL_PREFLIGHT_REQUESTS_HEADERS."
            )
        }

        private fun isPreflightedContentType(mediaType: MediaType): Boolean {
            return NON_PREFLIGHTED_CONTENT_TYPES.find { it.isCompatibleWith(mediaType) } == null
        }

        private fun containsCSRFinFlightHeader(headers: HttpHeaders): Boolean {
            val csrfInFlightHeader: String? =
                headers.keys.find { GRAPHQL_PREFLIGHT_REQUESTS_HEADERS.contains(it.lowercase()) }
            return StringUtils.hasText(csrfInFlightHeader)
        }
    }

    override fun assert(headers: HttpHeaders) {
        assertGraphQLCsrf(headers)
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy