Vấn đề lỗ hổng schema và type trong graphQL

Vấn đề lỗ hổng schema và type trong graphQL

December 22, 2020 0 By Nam Vu

Đầu tiên phải nói đến thế mạnh nổi bật trong graphQL đó là vấn đề dynamic câu lệnh request. Tuy nhiên để làm được vấn đề này thì graphQL đưa ra một khái niệm đó là Introspection. Khái niệm này thường được sử dụng để hỗ trợ câu lệnh truy vấn có thể merge các request lại với nhau, nó cho chúng ta biết cấu trúc schema hay kiểu dữ liệu của từng thuộc tính trong Dto (một tầng trao đổi giữa frontend và backend).

blog.ntechdevelopers.com

Ví dụ bạn muốn lấy toàn bộ schema và type của chúng thì bạn co thể sử dụng câu graph như sau:

{
  __schema {
    types {
      name
    }
  }
}

Kết quả sẽ là như vậy

{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Query"
        },
        {
          "name": "String"
        },
        {
          "name": "ID"
        },
        {
          "name": "Mutation"
        },
        {
          "name": "Episode"
        },
        {
          "name": "Character"
        },
        {
          "name": "Int"
        },
        {
          "name": "LengthUnit"
        },
        {
          "name": "Human"
        },
        {
          "name": "Float"
        },
        {
          "name": "Droid"
        },
        {
          "name": "FriendsConnection"
        },
        {
          "name": "FriendsEdge"
        },
        {
          "name": "PageInfo"
        },
        {
          "name": "Boolean"
        },
        {
          "name": "Review"
        },
        {
          "name": "ReviewInput"
        },
        {
          "name": "Starship"
        },
        {
          "name": "SearchResult"
        },
        {
          "name": "__Schema"
        },
        {
          "name": "__Type"
        },
        {
          "name": "__TypeKind"
        },
        {
          "name": "__Field"
        },
        {
          "name": "__InputValue"
        },
        {
          "name": "__EnumValue"
        },
        {
          "name": "__Directive"
        },
        {
          "name": "__DirectiveLocation"
        }
      ]
    }
  }
}

Hoặc bạn có thể sử dụng truy vấn qua link url kiểu như này thì schema cũng hiển thị tương tự

https://<domain>/gateway/graphql?query={__schema{types{name,fields{name}}}}

Ngoài schema và type ra ta còn lấy được các cấu trúc dữ liệu khác như:

__Schema, __Type, __TypeKind, __Field, __InputValue, __EnumValue, __Directive

Điều này vô tình lại để lộ ra một lỗ hổng trong bảo mật dữ liệu, và người ngoài có thể tấn công dựa trên lỗ hỗng này từ những thông tin cấu trúc, mỗi quan hệ và kiểu dữ liệu trong hệ thống mà họ thu thập được.

Vậy làm sao để phòng tránh với lỗ hổng này!

Cách resolve vấn đề này thì việc đầu tiên bạn nên nghĩ tới đó là disable introspection. Thực tế nó là một tầng middleware chặn ngay đầu vào của mỗi request, nó chỉ cho phép bạn call get được schema internal mà thôi, còn nếu gọi từ bên ngoài thì nó sẽ chặn lại.Bạn có thể tham khảo cách implement tại đây:
# Disable Introspection

Tuy nhiên mình gặp 2 vấn đề khi giải quyết issue này.

Thứ nhất, disable introspection không có thư viện hỗ trợ đối với ngôn ngữ DotNet (CSharp). Thật đáng buồn! Vậy là lại phải tự handle dựa trên ý tưởng middleware bên trên.Bạn có thể tham khảo sample tại đây:
# Github

Trước khi gắn Disable Introspection

Sau khi gắn Disable Introspection

Thứ 2, khi làm việc với microservice, cụ thể hơn là sử dụng schema stitching. Bạn hình dung, bạn có nhiều service đảm nhận nhiệm vụ khác nhau, mỗi service chứa một database riêng của nó, vấn đề khi graph gateway cần tổng hợp tất cả các schema của tất cả các service lại để có thể query. Vậy là stitching ra đời để giúp bạn merge các schema của nhiều service lại với nhau thành một schema tổng để cung cấp cho graphQL.Bạn có thể đọc thêm về Schema Stitching tại đây
Schema Stitching

Vậy là vấn đề khác lại phát sinh khi mình apply code disable introspection vào trong schema stitching này. Hiển nhiên nó không thể schema các service con khi bạn đã chặn nó, khi này stitching sẽ fail do bạn không lấy được

Application startup exception
System.InvalidOperationException: The HeaderPropagationValues.Headers property has not been initialized. Register the header propagation middleware by adding 'app.UseHeaderPropagation()' in the 'Configure(...)' method. Header propagation can only be used within the context of an HTTP request.
   at Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.DelegatingHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
   at HotChocolate.Stitching.Utilities.HttpQueryClient.FetchStringInternalAsync(HttpQueryRequest request, HttpClient httpClient)
   at HotChocolate.Stitching.Introspection.IntrospectionClient.GetSchemaFeaturesAsync(HttpClient httpClient, HttpQueryClient queryClient)
   at HotChocolate.Stitching.Introspection.IntrospectionClient.LoadSchemaInternalAsync(HttpClient httpClient)
   at HotChocolate.Stitching.Introspection.IntrospectionClient.LoadSchema(HttpClient httpClient)
   at HotChocolate.Stitching.StitchingBuilderExtensions.<>c__DisplayClass3_0.<AddSchemaFromHttp>b__0(IServiceProvider s)
   at HotChocolate.Stitching.StitchingBuilder.StitchingFactory.LoadSchemas(IDictionary`2 schemaLoaders, IServiceProvider services)
   at HotChocolate.Stitching.StitchingBuilder.StitchingFactory.Create(StitchingBuilder builder, IServiceProvider services)
   at HotChocolate.Stitching.StitchingBuilder.<Populate>b__27_0(IServiceProvider services)

Đến đây thì chẳng biết phải giải quyết sao nữa.

Rồi nảy ra một ý đó là sử dụng route endpoint để phân loại được đầu vào của schema nó đến từ request như thế nào. Mình nhận request từ route map when sau đó check nó là internal hay external. Rồi chặn schema nếu là external, còn internal thì mình redirect về grpc nội bộ service. Nói chung dựa vào router gateway còn chặn schema như nào thì vẫn dùng disable introspection thôi. Chỉ là kết hợp với router để phân loại internal và external. 

Nhiều khi giải pháp đến từ những thứ đơn giản nhất.
Mình thì đánh giá giải pháp này chưa thực sự hay nhưng với tình thế trước mắt thì mình nghĩ nó đáp ứng được nhu cầu giải quyết vấn đề của bài toán.

Theo bạn, bạn có ý tưởng nào khác cho vấn đề này. Bạn có thể góp ý dưới phần bình luận nhé!