iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 20
5
Modern Web

Think in GraphQL系列 第 20

GraphQL Design: 關於 Security 的二三事

  • 分享至 

  • xImage
  •  

header

今天會來講解一系列使用 GraphQL 會遇到的安全問題!其實大部分問題在各類型 API 系統都會遇到,只是 GraphQL 的一些特性使得攻擊起來更容易,而且 GraphQL 實作方式與其他的 API 不盡相同,容易在一些安全性防護疏忽。

今天大概會講解以下問題:

  1. 敏感資訊洩露
  2. 權限管理
  3. Query 複雜度限制
  4. GraphQL Injection 防護
  5. 其他常見攻擊

1. GraphQL Introspection 導致敏感資訊洩露

相比 RESTful ,在 GraphQL 我們可以使用 GraphiQL 、 GraphQL Playground 或 graphqldoc 等工具來輕鬆取得對方的 api ,而這些工具雖然降低了開發了成本,卻也讓有心人士可以透過熟悉 Schema 來增加他攻擊的手段。

如果要防止被外部人士得到 GraphQL Schema ,我們得要了解這些工具背後所運用的 Introspection 功能,這項功能讓我們可以去解析 GraphQL Schema 。比如我在 GitHub API Explorer 輸入以下的 Query :

{
  __schema {
    types {
      name
    }
  }
}

結果如下

img

此時若想取得 User Type 的結構就輸入:

{
  __type(name: "User") {
    name
    fields {
      name
      type {
        name
      }
    }
  }
}

就會得到下圖的結果 :

img

這些沒有出現在 documentation 中的 type 及 fields 就是 GraphQL 為了 Introspection 所設計的,通常我們會在 production (正式) 環境下限制外部對於 API 的 Introspection 。

在實作上很簡單,只要在 GraphQL 加入 validationRules 的設定,將 schema 與 type 的要求擋掉就行了,詳細可參考 helfer/graphql-disable-introspection

如果是使用 Apollo Server 2 ,那麼會在 NODE_ENV 為 production 時自動關閉 introspection ,直接省下設定的麻煩。另外如果要關閉 Apollo Server 的 GraphQL Playground 的話,需要額外的設定如下:

const { ApolloServer } = require('apollo-server');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  // 若 NODE_ENV=production 則自動為 fasle
  introspection: false,
  playground: false
});

雖然可以使用以上的方式來阻擋外人得知我們的 GraphQL Schema ,但對方只是不能看文件,仍不能保證機密資料不會被竊取,因此設計上就應該盡量避免在 GraphQL 開放高機密的資料如 Secret 或是 password 等,以防有心人士繞過 query 或 mutation 權限去竊取或偽造。

除此之外,我們也需要良好的權限管理來保護我們的資料。

2. 不管是 Query 、 Mutation 還是 field 都得做好權限管理

其實權限管理在不管是哪一種 API 技術都需要注意,只是 GraphQL 的 Type System 讓權限管理變得稍微複雜一些,有些內容可以給尚未登入的人觀看,或是有些內容僅會員本人能觀看不能分享等,加上很多權限管理要求做到 field 層級,不像 RESTFul API 要就 ban 掉整個 endpoint 。

前面有提到,在 GraphQL 做 Authentication 與 Authorization 第一步就是將發出 query 的使用者記錄在 context.me 中如以下程式碼:

const server = new ApolloServer({
 typeDefs,
 resolvers,
 context: ({ req }) => {
   // 取出 token
   const token = req.headers.['x-token'] || '';
  
   // 從 token 取出資料
   const me = getUser(token);
  
   // 將 me 加入 context
   return { me };
 },
});

然後在 Resolver 中做驗證,如以下程式碼:

const resolvers = {
  Query: {
    users: (root, args, { me }) => {
      // 檢查有無 me 或是檢查 me 的權限
      if (!me) return [];
      return { /* your data */ };
  }
}

以上的方式雖然簡潔,但當 Schema 越來越複雜,同樣的權限管理檢查就會一直被重複寫進去,導致權限管理難以管理與更新,因此 Facebook 官方建議將權限管理抽出 Resolver ,放進 business logic layer (商業邏輯層) 如下

img
(圖片來源: https://graphql.org/learn/thinking-in-graphs/#business-logic-layer)

這樣可以解除 Resolver 直接檢查權限的工作,將工作委託出去。這樣的好處在於可以做到 Single Source Of Truth 也就是保證特定邏輯的行為在整個程式中只維持在一處,避免 Duplicate Coding。如同 Facebook 也在官方教學說過

Your business logic layer should act as the single source of truth for enforcing business domain rules
你的商業邏輯層應該作為整個程式唯一應用商業領域規則的地方。

另外 Apollo Server 一樣建議將權限管理從 Resolver 中抽出,不過他們建議將權限檢查抽出後放進 Model Layer (資料取得層) 會是更好的選擇,可見以下程式碼:

const { generateUserModel } = require('./models/user');

const server = new ApolloServer({
  ...,
  context: ({ req }) => {
    const token = req.headers.['x-token'] || '';

    const me = getUser(token);

    return {
      user,
      // 讓 Model 使用 me 作為初始化參數
      models: {
        User: generateUserModel({ user: me }),
        ...
      }
    };
  },
})

// models/user.js
export const generateUserModel = ({ user }) => ({
 // 在裡面可以做認證檢查、權限檢查等等
 getFriends: () => { ... },
 getById: (id) => { ... },
 updateInfo: (data) => { ... }
});

(程式碼參考: https://www.apollographql.com/docs/guides/access-control.html)

另外前面也提過可以用 Directive 的方式做權限管理, Schema 如下:

directive @auth(requires: Role = ADMIN) on OBJECT | FIELD_DEFINITION

enum Role {
  ADMIN
  REVIEWER
  USER
}

type User @auth(requires: USER) {
  name: String
  banned: Boolean @auth(requires: ADMIN)
  canPost: Boolean @auth(requires: REVIEWER)
}

至於如何實作或是想知道更多權限管理可以參考 Apollo Server - Access Control 或是這篇 Handling Authentication and Authorization in GraphQL

個人想法是, Directive 雖然簡單易用又好讀,但是目前此功能尚未非常穩定,還有許多改進空間,並且對於一些複雜的權限驗證邏輯還是依靠 business logic layer 來處理比較好,比如使用 Medium 就會有「文章 A 只能給 VIP 會員看但路人及一般會員可以看預覽」的場景或是 Facebook 也有「會員資料有些完全公開、有些只開放好友或摯友以及有些僅供本人觀看」等等的複雜邏輯,就不建議在 Directive 實作。

在一些案例中可發現 (案例 1, 案例 2) 要取得一些不該被公開的欄位並非難事 (雖然這些案例中暴露出的資料並不嚴重),因此絕不能輕忽權限管理!

3. 利用 Query 的彈性所引發的 DoS 攻擊

之前有提過,由於 GraphQL 資料領域 (data domain) 可以互相串連的特性,可以讓一個 query 無限增長下去,光是之前說的 User/Post 系統就能製造出以下的 Query:

query {
  users (first: 999999999) {
    posts {
      author {
        posts {
          author {
            # ...這可以永無止盡的延續下去
          }
        }
      }
    }
  }
}

這樣就能讓有心人士透過這樣的 query 來增加我們 Server 的負擔進而影響甚至癱瘓我們的系統,或甚至這是因為自己本身程式設計的疏忽也有可能導致這樣的情形。

解決方法有很多,讓我們來看:

3-1. 限制 query 大小

缺點是限制了 GraphQL 的彈性而且仍難以避免有心人一次索取大量資料 (提高資料量 limit ) 或透過字數較少的 field 做出 nested

3-2. 將合法的 Query 一一加入白名單,並且只允許前端傳送白名單內的 Query

缺點是需要付出額外的成本去維護白名單,更別提要做到向後相容。此外也有點違反了當初 GraphQL 追求彈性的目標也限制了未來公開 API 給外界使用的可能性。

但如果 API 僅供內部使用,比如只有後端服務互相溝通,因為會使用到 query 相對固定,所以白名單的做法在這裡就非常適合!

3-3. 實作 Maximum Query Depth (最大 Query 深度) 來避免高度 nested 的 query

對於深度的計算可以參考這張圖:

img

在實作方面可以透過解析 Query 的 AST 結構或是直接使用這個套件 stems/graphql-depth-limit

const depthLimit = require('graphql-depth-limit');

const server = new ApolloServer({
  ...,
  validationRules: [depthLimit(5)]
});

可以得到以下的效果:

img

3-4. 實作 Amount Limiting。

除了深度以外, GraphQL 也會面臨一次請求過多資料的問題如下:

query {
  users(first: 999999999999999999) { ... }
}

最簡單的解決方式就是限制 input number,比如僅能索取 1 - 100 之間的數量,這方面可以在 Resolver 做參數檢查或是利用現有套件 joonhocho/graphql-input-number 來協助,比如我們可以建立一個新的 Scalar Type 來限制參數:

const FirstAmount = GraphQLInputInt({
  name: 'FirstAmount',
  min: 1,
  max: 100,
});

const typeDefs = gql`
  type Query {
    users (first: FirstAmount): [User]
  }
`

3-5. 使用 Query Cost Analysis 來做到 Complexity Limiting

透過前面的方式雖然能夠有效解決大部分的問題,但仍無法避免這樣的攻擊:

query {
  user(name: "Fong") {
    posts (first: 100) { ... },
    followingUsers (first: 100) {
      posts (first: 100) { ... }
      clubs (first: 100) { ... }
      ...
    }
  }
}

可以發現以上 quey 深度不深且數量都控制在 100 ,可以輕鬆繞過你的檢查限制,因此需要用 complexity 來預防。 Complexity 的計算如下:

query {
  user(name: "Fong") {    # complexity: 1
    posts(first: 5) {    # complexity: 5
      title              # complexity: 1
    }
  }
}

解決方式就是實際去計算該 query 造成的 Complexity ,如果超過規定的數值即拋棄該次 query ,推薦可以使用 graphql-validation-complexity 或是 graphql-cost-analysis 來協助計算複雜度!

用起來如下:

type Query {
  user: User @cost(complexity: 1)
  users(first: FirstAmount,...): [User] @cost(complexity: 2, multipliers: ["first"])
}

(可參考 GitHub 的做法)

但介紹了這麼多外部套件仍必須提醒大家,投資理財有風險基金有賺有賠這些套件在使用上都會有一定風險,之前 npm 被植入惡意後們的案例!所以可以考慮在 CI 中加入 npm audit 來減少這類風險

4. GraphQL Injection 攻擊

假如原本只是一個簡單的修改個人資料的 mutation 如下 :

mutation {
  editProfile(name: "Fong2", age: 24) {
    id
    name
    age
    password
  }
}

但如果今天使用者被吸引去點擊一些惡意的 URL ,而這些網址取得以上參數後修改 age 參數成惡意的 Injection 參數如下:

mutation {
+  editProfile(name: "Fong2", age: 24) {
+    id
+    password
+  }
+  changePassword(password: "123456"){
    id
    name
    age
    password
  }
}

(程式碼參考來源: https://xzfile.aliyuncs.com/upload/zcon/2018/7_%E6%94%BB%E5%87%BBGraphQL_phithon.pdf)

就會發生很可怕的事情!因此就如同使用 SQL 時要養成的好習慣一樣, 使用參數化 Query來避免這類型的攻擊,比如前端將 query 改成以下的形式:

mutation ($name: String, age: Int) {
  editProfile(name: $name, age: $age) {
    id
    name
    age
    password
  }
}
---
VARIABLES
{
  "name": "Fong2",
  "age: 24
}

就可以避免掉 GraphQL Injection 的攻擊!

5. 其他攻擊

其他攻擊如 CSRF 或是 clickjacking 等等也有可能在 GraphQL 發生,所以平常在 RESTful 會做的防護措施在 GraphQL 中也不能馬虎!

因為這段比較通泛,因此不特別贅述,有興趣的可以參考這篇優質投影片攻击GraphQL,裡面有許多相關的範例與介紹可以參考!


總結來說, GraphQL 的安全問題其實並不新鮮,大多數的 API 技術都會遇到,只是 GraphQL 彈性化的 query 結構以及本質上只是一層 API Interface Layer 並無安全管理的責任,因此讓人容易忽略背後的風險!


Reference


上一篇
GraphQL Design: Pagination 輕鬆處理大資料!
下一篇
GraphQL 設計: Autentication 與 Authorization 大全
系列文
Think in GraphQL30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言