使用Semgrep保护Apollo GraphQL服务器安全

本文介绍了如何使用Semgrep静态分析工具检测Apollo GraphQL服务器v3和v4版本中的CSRF和CORS错误配置,包含具体的规则示例和漏洞分析,帮助开发者提升服务器安全性。

使用Semgrep保护Apollo GraphQL服务器安全

tl;dr: 我们公开可用的Semgrep规则集新增了9条规则,用于检测Apollo GraphQL服务器v3和v4版本的错误配置。使用semgrep --config p/trailofbits来尝试它们!

在审计多个客户的Apollo GraphQL服务器时,我反复发现相同的问题:跨站请求伪造(CSRF)允许攻击者代表用户执行操作、缺乏速率限制允许攻击者暴力破解密码或MFA令牌,以及跨域资源共享(CORS)错误配置允许攻击者获取不应访问的机密信息。开发者忽略这些问题有多种原因:Apollo GraphQL服务器v3版本的不良默认设置(例如,csrfProtection选项默认不为true)、对某些GraphQL功能(如批处理查询)缺乏理解或知识,以及对某些Web概念(如同源策略[SOP]和CORS的工作原理)缺乏理解。

反复发现相同问题促使我利用内部研发(IRAD)时间,在未来审计中一致检测这些问题,从而有更多时间寻找更深层次、更复杂的漏洞。Semgrep——一种用于检测单个文件中简单模式的静态分析工具——是这项工作的明显选择,因为这些问题很容易用类似grep的结构检测,且不需要过程间或其他类型的更复杂分析。

我们开源了Semgrep规则,用于查找Apollo GraphQL服务器v3和v4的错误配置。我们的规则利用Semgrep的污点模式,使其更易于编写并提高准确性。快去测试你的GraphQL服务器!

我们之前公开发布了Semgrep规则,用于查找Go并发错误和机器学习库的误用。

常见GraphQL问题

GraphQL有几个设计选择,使得某些漏洞(如CSRF)比典型REST服务器更普遍。当然,GraphQL服务器也遭受所有常见问题:访问控制问题(例如,GitLab中的访问控制缺陷泄露了私有用户信息,或HackerOne中的漏洞允许攻击者泄露用户机密数据)、SQL注入(例如,HackerOne的GraphQL服务器中的SQL注入)、服务器端请求伪造(SSRF)、命令注入等。

本博客文章将介绍我们创建的用于检测CSRF和CORS错误配置的规则。我们还将展示使用Semgrep的污点模式如何通过减少定义值流入接收器的所有方式所需的模式数量来节省时间并提高规则的准确性。

CSRF

CSRF是一种攻击,允许恶意行为者诱骗用户在他们已认证的网站中执行不需要的操作(例如,编辑用户个人资料)。如果你不熟悉细节,请阅读PortSwigger的Web安全学院CSRF解释中的更多内容。

Apollo服务器中的CSRF攻击

CSRF一直困扰着Apollo GraphQL服务器,直到引入csrfPrevention选项。CSRF漏洞在Apollo服务器中普遍存在,原因有两个:开发者将突变错误标记为查询,以及Apollo服务器允许用户使用GET请求发出查询操作(但不允许突变操作)。查询不应更改状态(如RESTful API中的GET请求),而突变预期会更改状态(如POST、PATCH、PUT或DELETE)。如果开发者遵循此约定,一切都会好。然而,我尚未找到一个代码库没有将突变错误标记为查询,这使得这些错误标记的操作立即容易受到CSRF攻击。

幸运的是,Apollo团队非常清楚这一点,并在v3版本中添加了csrfPrevention选项以完全消除此问题。它通过确保任何请求必须具有不同于text/plainapplication/x-www-form-urlencodedmultipart/form-data的Content-Type头、非空的X-Apollo-Operation-Name头或非空的Apollo-Require-Preflight头来防止CSRF攻击。这确保了请求将始终被预检,从而防止CSRF攻击。

csrfPrevention选项在v3中默认为false,在v4中默认为true,因此仍在使用v3的用户需要有意识地在服务器初始化中添加此选项,根据我们的经验,这几乎从未发生。

使用Semgrep查找CSRF错误配置

我们创建了两个Semgrep规则来查找v3和v4版本中的错误配置。对于v3,我们查找所有csrfPrevention选项未设置为true的ApolloServer初始化。

1
2
3
4
patterns:
  - pattern: new ApolloServer({...})
  - pattern-not: |
      new ApolloServer({..., csrfPrevention: true, ...})

图1.1:检测Apollo服务器v3版本中错误配置的csrfProtection选项的Semgrep规则

对于v4,我们查找所有csrfPrevention选项设置为false的服务器初始化。

1
2
3
patterns:
  - pattern: |
      new ApolloServer({..., csrfPrevention: false, ...})

图1.2:检测Apollo服务器v4版本中错误配置的csrfProtection选项的Semgrep规则

CORS

CORS允许服务器放宽浏览器的SOP。正如预期,开发者有时放宽SOP过多,这可能允许攻击者获取他们不应访问的机密信息。如果你不熟悉细节,请阅读PortSwigger的Web安全学院CORS解释中的更多内容。

在Apollo服务器中设置CORS策略

在Apollo服务器v3版本中,开发者可以通过两种方式设置其服务器的CORS策略。首先,他们可以将cors参数传递给他们的ApolloServer类实例。

1
2
3
4
5
import { ApolloServer } from 'apollo-server';

const apolloServerInstance = new ApolloServer({
    cors: CORS_ORIGIN
});

图1.3:在Apollo GraphQL服务器v3版本中配置CORS

或者,他们可以在使用的后端框架上设置CORS策略。例如,使用Express.js后端服务器时,CORS属性作为参数传递给applyMiddleware函数。

1
2
3
4
5
6
7
8
import { ApolloServer } from 'apollo-server-express';

const apolloServerInstance = new ApolloServer({});

apolloServerInstance.applyMiddleware({
    app,
    cors: CORS_ORIGIN,
});

图1.4:使用后端Express服务器在Apollo GraphQL服务器v3版本中配置CORS

在Apollo服务器v4版本中,开发者必须直接在后端设置CORS。因此,为v4编写规则超出了我们特定于Apollo的Semgrep查询范围——其他Semgrep规则已经覆盖了大多数这些情况。

我们的v3版本规则涵盖了Express.js和内置Apollo服务器后端的用法,因为这些是我们看到使用最多的。如果你为Apollo服务器使用不同的后端框架,我们的规则可能不起作用,但我们在trailofbits/semgrep-rules接受PR!根据现有查询调整它们应该很容易。;)

查找缺失的CORS策略

每个后端的规则非常相似,因此让我们看其中一个——检测内置Apollo服务器中CORS错误配置的规则。我们在同一文件中有两个规则:一个用于检测未定义CORS策略的情况,一个用于检测配置不良的CORS策略。

为了检测缺失的CORS策略,我们查找cors参数未定义的ApolloServer实例化。我们还需要确保ApolloServer来自apollo-server包(ApolloServer类也可能来自apollo-server-express包,但我们不想捕获这些情况)。查询如图1.5所示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
patterns:
  - pattern-either:
      - pattern-inside: |
          $X = require('apollo-server');
          ...
      - pattern-inside: |
          import 'apollo-server';
          ...
  - pattern: |
      new ApolloServer({...})
  - pattern-not: |
      new ApolloServer({..., cors: ..., ...})

图1.5:检测Apollo GraphQL服务器(v3)中缺失CORS策略的Semgrep规则

查找不良CORS策略

检测不良CORS策略并不那么简单。我们必须检测几种情况:

  • 源设置为true的情况——true源告诉服务器接受所有源。
  • 源设置为null的情况——攻击者可以诱骗用户从沙盒iframe等位置发出来自null源的请求。
  • 源是带有未转义点字符的正则表达式的情况——在正则表达式中,点匹配任何字符,因此如果我们使用/api.example.com$/正则表达式,它将匹配apiXexample.com域,这可能被攻击者控制。
  • 源不以$字符结尾的情况——在正则表达式中,$字符匹配字符串的结尾,因此如果我们使用/api.example.com/正则表达式,它也将匹配api.example.com.attacker.com域,一个攻击者控制的域。

这些不会覆盖所有可能的不良CORS策略(例如,不良CORS策略可能简单地包含攻击者域或允许攻击者上传HTML代码的域)。我们使用下图中的规则测试上述所有情况。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pattern-either:
  # 'true' mean that every origin is reflected
  - pattern: |
      true
  # the '.' character is not escaped
  - pattern-regex: ^/.*[^\\]\..*/$
  # the regex does not end with '$'
  - pattern-regex: ^/.*[^$]/$
  # An attacker can make requests from ‘null’ origins
  - pattern: |
      'null'

图1.6:检测不良CORS源的Semgrep模式

这些不良源可以单独使用或在数组中使用。为了测试这两种情况,我们首先检查孤立或在数组中的$CORS_SINGLE_ORIGIN元变量出现,然后使用元变量模式用我们在图1.6中创建的模式定义什么是不良源。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
pattern-either:
  - patterns:
      # pattern alone or inside an array
      - pattern-either:
          - pattern: |
              $CORS_SINGLE_ORIGIN
          - pattern: |
              [..., $CORS_SINGLE_ORIGIN, ...]
      - metavariable-pattern:
          metavariable: $CORS_SINGLE_ORIGIN
          pattern-either:
             # <The bad origin checks from the previous figure>

图1.7:检测单个条目或数组中不良CORS源的Semgrep模式

最后,我们需要在ApolloServer初始化中找到此源的用法。我们使用以下模式实现:

1
new ApolloServer({..., cors: $CORS_ORIGIN, ...})

这个$CORS_ORIGIN可以内联使用(例如,cors: true),或者可以来自变量(例如,cors: corsOriginVariableDefineElsewhere)。定义源可能来自的所有可能位置是费力的。幸运的是,我们不需要用Semgrep的污点模式这样做!

我们只需要定义以下内容:

  • pattern-sources: 不良CORS策略——我们将其定义为{origin: $BAD_CORS_ORIGIN},其中$BAD_CORS_ORIGIN元变量是我们上面定义的不良源模式。
  • pattern-sinks: 不良CORS策略不应流入的位置——我们将其定义为模式new ApolloServer({..., cors: $CORS_ORIGIN, ...})中的$CORS_ORIGIN元变量。

使用污点模式,我们可以捕获设置CORS策略的多种方式:直接(图1.8中的案例1)、通过配置整个CORS策略的变量(案例2)、通过仅设置源的变量(案例3),以及许多其他我们不想手动设置的配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Case 1: Has a very permissive 'cors' (true)
const apollo_server_bad_1 = new ApolloServer({
    //ruleid: apollo-graphql-v3-bad-cors
    cors: { origin: true }
});

// Case 2: Has a very permissive 'cors' from a variable
const bad_CORS_policy = { origin: true }
const apollo_server_bad_2 = new ApolloServer({
    //ruleid: apollo-graphql-v3-bad-cors
    cors: bad_CORS_policy
});

// Case 3: Has a very permissive 'cors' from a variable (just the origin)
const bad_origin = true;
const apollo_server_bad_3 = new ApolloServer({
    //ruleid: apollo-graphql-v3-bad-cors
    cors: { origin: bad_origin }
});

图1.8:Semgrep污点模式免费帮助捕获的几个测试案例

整个注释规则如图1.9所示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
mode: taint
pattern-sources:
  - patterns:
      - pattern-inside: |
          { origin: $BAD_CORS_ORIGIN }
      - metavariable-pattern:
          metavariable: $BAD_CORS_ORIGIN
          pattern-either:
            # 'true' means that every origin is reflected
            - pattern: |
                true
            - patterns:
                # pattern alone or inside an array
                - pattern-either:
                    - pattern: |
                        $CORS_SINGLE_ORIGIN
                    - pattern: |
                        [..., $CORS_SINGLE_ORIGIN, ...]
                - metavariable-pattern:
                    metavariable: $CORS_SINGLE_ORIGIN
                    pattern-either:
                      # the '.' character is not escaped
                      - pattern-regex: ^/.*[^\\]\..*/$
                      # the regex does not end with '$'
                      - pattern-regex: ^/.*[^$]/$
                      # An attacker can make requests from ‘null’ origins
                      - pattern: |
                          'null'
pattern-sinks:
  - patterns:
      # The ApolloServer comes from the 'apollo-server' package
      - pattern-either:
          - pattern-inside: |
              $X = require('apollo-server');
              ...
          - pattern-inside: |
              import 'apollo-server';
              ...
      # The sink is the ApolloServer's cors argument
      - pattern: |
          new ApolloServer({..., cors: $CORS_ORIGIN, ...})
      # This tells Semgrep that the sink is only the $CORS_ORIGIN variable
      - focus-metavariable: $CORS_ORIGIN

图1.9:检测Apollo GraphQL服务器(v3)中不良CORS策略的Semgrep规则

我们还为审计员和安全工程师创建了一个Semgrep规则,即使策略可能安全,他们也希望详细审查其Apollo服务器的CORS策略。此规则报告任何不是false或空数组的CORS策略——明显良好的CORS策略。当你想手动检查所有硬编码源时,它很有用,但你不希望将其集成到CI管道中,因为它会报告误报(审计规则)。你可以在trailofbits.javascript.apollo-graphql.v3-cors-audit.v3-potentially-bad-cors找到此规则。

结束思考

Semgrep擅长查找单个文件中发生的简单模式,如我们在本文中描述的模式。对于更复杂的分析,你可能想使用诸如CodeQL之类的工具,它也有其缺点:涉及更陡峭的学习曲线、为不同语言使用不同的API、需要编译代码,并且不支持Semgrep支持的一些语言(例如Rust)。

Semgrep的最大限制之一是缺乏文件间和过程间分析。例如,上述规则不会捕获CORS策略在一个文件中设置而Apollo Server初始化在另一个文件中发生的情况。这可能现在通过Semgrep Pro Engine(以前称为DeepSemgrep)成为可能,它通过文件间分析功能增强了Semgrep引擎。然而,此功能目前仅限于付费客户和有限数量的语言。

在Trail of Bits,我们广泛使用静态分析工具,通常最终会编写特定于客户代码库的自定义规则和查询。这些可以提供巨大价值,因为它们可以找到特定于你代码库的模式,甚至强制执行你组织的工程最佳实践。当我们编写的规则对社区有用时,我们喜欢开源它们。在https://github.com/trailofbits/semgrep-rules查看它们。

使用semgrep --config p/trailofbits使用我们的新Apollo GraphQL规则,并尝试编写你自己的自定义规则!

如果你喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计