Skip to content

GraphQL threats detection and protection#3769

Merged
vpellan merged 36 commits into
masterfrom
vpellan/graphql-threat-detection
Jul 24, 2024
Merged

GraphQL threats detection and protection#3769
vpellan merged 36 commits into
masterfrom
vpellan/graphql-threat-detection

Conversation

@vpellan

@vpellan vpellan commented Jul 9, 2024

Copy link
Copy Markdown
Contributor

What does this PR do?

This PR adds threats detection and protection for GraphQL using libddwaf, the reactive engine and a GraphQL-Ruby (fake) tracer acting as a middleware for the method we want to instrument.
Instead of instrumenting execute_query and verifying variables and arguments there, we go through every query's AST in execute_multiplex, which enable us to block all queries if a threat is detected, and not just the one where the threat is actually located.

Motivation:

There has been some customer demand about that feature. The goal is to dogfood this on Datadog's GitLab.

Additional Notes:

There are complementary changes on this PR :

  • Added support for GraphQL 2.3, and added Rails in GraphQL appraisals to do integration tests on a full rails app.

    • This means that a lot of changed files are just gemfiles and gemfile locks.
  • Refactored waf result specs that were in:

    • appsec/contrib/rack/reactive/*
    • appsec/contrib/rails/reactive/*
    • appsec/contrib/sinatra/reactive/*
    • appsec/monitor/reactive/*

    in appsec/reactive/shared_examples.rb

  • Fixed a typo in fetch_configuration in:

    • appsec/processor/actions.rb and associated sig & spec
    • appsec/response.rb
  • Fixed indentation in docs/GettingStarted.md

  • The files that are directly connected to GraphQL Threats detection are:

    • Rakefile
    • Matrixfile
    • lib/datadog/appsec.rb
    • lib/datadog/appsec/response.rb
    • lib/datadog/appsec/contrib/graphql/*
    • sig/datadog/appsec/contrib/graphql/*
    • spec/datadog/appsec/contrib/graphql/*
    • spec/datadog/tracing/contrib/graphql/support/*
    • spec/datadog/tracing/contrib/rails/support/*

How to test the change?

bundle exec appraisal ruby-X.X-graphql-X.X rake spec:appsec:graphql

@vpellan
vpellan requested review from a team as code owners July 9, 2024 13:56
@github-actions github-actions Bot added appsec Application Security monitoring product integrations Involves tracing integrations labels Jul 9, 2024
@vpellan vpellan self-assigned this Jul 9, 2024
# skip CSRF token check for non-GET requests
begin
if respond_to?(:skip_before_action)
skip_before_action :verify_authenticity_token

Check failure

Code scanning / CodeQL

CSRF protection weakened or disabled

Potential CSRF vulnerability due to forgery protection being disabled or weakened.

@maycmlee maycmlee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for docs

@vpellan
vpellan requested a review from lloeki July 10, 2024 11:38

@lloeki lloeki left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall it looks good.

I see a factorisation opportunity, plus a few small notes+questions for clarification.

Comment on lines +28 to +34
multiplex_return = []
gateway_multiplex.queries.each do |query|
query_result = ::GraphQL::Query::Result.new(
query: query,
values: JSON.parse(AppSec::Response.content_json)
)
multiplex_return << query_result

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be extracted into AppSec::Response.negotiate?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could extract it but I believe it would become un-necessarily more complex. AppSec::Response.negotiate was made for HTTP-level frameworks, selecting the type of response the client configured (JSON, HTML or Plain text). But GraphQL only return JSON, so we'd have to make a special case to always enforce JSON when it is GraphQL, and also duplicate the resulting JSON times the number of queries in the multiplex.

@lloeki lloeki Jul 15, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this then?

multiplex_return = appsec_response(some, args)

and

private

def appsec_response(some, args)
  gateway_multiplex.queries.map do |query|
    query_result = ::GraphQL::Query::Result.new(
      query: query,
      values: JSON.parse(AppSec::Response.content_json)
    )
  end
end

To split concerns

module GraphQL
# GraphQL integration constants
# @public_api Changing resource names, tag names, or environment variables creates breaking changes.
module Ext

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this Ext empty, you might as well remove the file.

private

def create_arguments_hash
require 'graphql/language/nodes'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why this require is dynamic instead of top-level?

Also, if it needs to be dynamic for some reason, it might be worth doing the require out of a hot code path.

bits = schema.execute('query test{ user(id: 1) { name } }')
expect(bits.to_h).to eq({ 'data' => { 'user' => { 'name' => 'Bits' } } })

caniche = schema.execute('query test{ user(id: 10) { name } }')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆

Suggested change
caniche = schema.execute('query test{ user(id: 10) { name } }')
poodle = schema.execute('query test{ user(id: 10) { name } }')

(j/k)

Comment thread docs/GettingStarted.md
| `schemas` | | `Array` | Array of `GraphQL::Schema` objects (that support class-based schema only) to trace. If you do not provide any, then tracing will applied to all the schemas. | `[]` |
| `with_unified_tracer` | | `Bool` | Enable to instrument with `UnifiedTrace` tracer, enabling support for API Catalog. `with_deprecated_tracer` has priority over this. Default is `false`, using `GraphQL::Tracing::DataDogTrace` (Added in v2.2) | `false` |
| `with_deprecated_tracer` | | `Bool` | Enable to instrument with deprecated `GraphQL::Tracing::DataDogTracing`. This has priority over `with_unified_tracer`. Default is `false`, using `GraphQL::Tracing::DataDogTrace` | `false` |
| `service_name` | | `String` | Service name used for graphql instrumentation | `'ruby-graphql'` |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'ruby-graphql'

The ruby- prefix seems odd. Shouldn't it simply be graphql? (Really I don't know)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree but this has been added 6 years ago, maybe we should create a new PR about it ?

@codecov-commenter

codecov-commenter commented Jul 11, 2024

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 96.81021% with 20 lines in your changes missing coverage. Please review.

Project coverage is 97.89%. Comparing base (ca006e9) to head (9295cad).
Report is 3002 commits behind head on master.

Files with missing lines Patch % Lines
...dog/tracing/contrib/graphql/support/application.rb 90.90% 7 Missing ⚠️
lib/datadog/appsec/contrib/graphql/appsec_trace.rb 83.33% 4 Missing ⚠️
...cing/contrib/graphql/support/application_schema.rb 90.90% 4 Missing ⚠️
...atadog/appsec/contrib/graphql/gateway/multiplex.rb 97.14% 1 Missing ⚠️
.../datadog/appsec/contrib/graphql/gateway/watcher.rb 97.29% 1 Missing ⚠️
lib/datadog/appsec/contrib/graphql/integration.rb 95.00% 1 Missing ⚠️
lib/datadog/appsec/contrib/graphql/patcher.rb 94.44% 1 Missing ⚠️
...ec/datadog/tracing/contrib/rails/support/models.rb 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3769      +/-   ##
==========================================
- Coverage   97.91%   97.89%   -0.02%     
==========================================
  Files        1243     1256      +13     
  Lines       74763    74983     +220     
  Branches     3608     3667      +59     
==========================================
+ Hits        73205    73408     +203     
- Misses       1558     1575      +17     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vpellan
vpellan merged commit 26a5abf into master Jul 24, 2024
@vpellan
vpellan deleted the vpellan/graphql-threat-detection branch July 24, 2024 08:34
@github-actions github-actions Bot added this to the 2.3.0 milestone Jul 24, 2024
@TonyCTHsu TonyCTHsu mentioned this pull request Aug 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

appsec Application Security monitoring product integrations Involves tracing integrations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants