Skip to content

CSRF on GraphQL API `IntrospectionQuery` operation can run mutation to create gitlab-ci.yml to exfiltrate victim's CI/CD job token

Please read the process on how to fix security issues before starting to work on the issue. Vulnerabilities must be fixed in a security mirror.

HackerOne report #2473644 by ahacker1 on 2024-04-22, assigned to @ottilia_westerlund:

Report | How To Reproduce

Report

Summary

There's improper logic used to block mutations on GraphQL mutations on GET requests.

See the code:
https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/controllers/graphql_controller.rb?ref_type=heads

When the query is a GraphQL introspection query (checked with operationName = IntrospectionQuery), it executes execute_introspection_query:

  def execute  
    result = if introspection_query?  
               execute_introspection_query  
             else  
               multiplex? ? execute_multiplex : execute_query  
             end

    render json: result  
  end  

The execute_introspection_query ultimately uses the execute_query function to execute, if not an actual introspection query. Importantly, this executes based on the regular parameters e.g. params[:query]

  def execute_query  
    variables = build_variables(params[:variables])  
    operation_name = params[:operationName]

    GitlabSchema.execute(query, variables: variables, context: context, operation_name: operation_name)  
  end  

However, the code used to check mutation on GET requests. checks the multiplex (_json parameter) first for mutations.

  def any_mutating_query?  
    if multiplex?  
      multiplex_queries.any? { |q| mutation?(q[:query], q[:operation_name]) }  
    else  
      mutation?(query)  
    end  
  end  

By making the request with both an psuedo introspection query (params[:operationName]= IntrospectionQuery) and a multiplex query, (params[:json] = Array), the controller checks the multiplex parameter for mutations, but actually executes according to the normal query parameter (which remains unchecked for mutations).

Steps to reproduce
  1. As victim, visit: https://gitlab.com/api/graphql?query=mutation%20IntrospectionQuery{createSnippet(input:{title:%22test%22%20description:%22test%22%20visibilityLevel:public%20blobActions:[{action:create%20previousPath:%22test%22%20filePath:%22test%22%20content:%22test%20new%20file%22}]}){errors%20clientMutationId%20snippet{webUrl}}}&operationName=IntrospectionQuery&_json[][query]=query%20{__typename}

Observing that a snippet has been created on your account. (There are other more sensitive actions that can be performed here).

Alternatively, for an AC:Low scenario, an attacker can place this inside a image:

![test](https://gitlab.com/api/graphql?query=mutation%20IntrospectionQuery{createSnippet(input:{title:%22test%22%20description:%22test%22%20visibilityLevel:public%20blobActions:[{action:create%20previousPath:%22test%22%20filePath:%22test%22%20content:%22test%20new%20file%22}]}){errors%20clientMutationId%20snippet{webUrl}}}&operationName=IntrospectionQuery&_json[][query]=query%20{__typename})  
What is the current bug behavior?

Improper logic used in the graphql controller to allow mutations to be run on GET request with specially crafted payload.

What is the expected correct behavior?

Mutations should be blocked on GET requests correctly.

Relevant logs and/or screenshots
Output of checks

This bug happens on GitLab.com

Results of GitLab environment info

Impact

CSRF, can make victim run arbitary mutations (including those that steal CI/CD variables) with one click.

Note, specifically, the commitCreate mutation can be used to create arbitrary commits on victim Project (including updating the workflow file). Thus, granting access to CI/CD secrets

CVSS justification:
AV: N, this occurs through HTTP requests

AC: Low An attacker can place an image payload of this in Gitlab's markdown, thus infecting notables, issues ... Additionally, since GitLab's cookies are samesite:None, this would also work for any other site that supports images URL (without proxying). So this could be considered the same attack complexity as a stored XSS, Low.

PR: None, An attacker can place the image in other sites, which the victim can visit, without the attacker having an account on GitLab the victim's GitLab instance.

UI: Required, but close to None, as visiting a GitLab issue, or any other popular site/email, can be considered part of their normal workflow.

Scope: Unchanged

Confidentiality: High Through commitCreate, an attacker can update ci/cd workflow file, and steal creds.
Integrity: High commitCreate mutation
Availability: None

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N

How To Reproduce

Please add reproducibility information to this section: