GraphQL API 安全攻防学习---基于PortSwigger靶场

GraphQL API 安全攻防学习---基于PortSwigger靶场

官网:https://graphql.cn/

参考文章:https://www.freebuf.com/articles/web/418803.html

一、GraphQL API 详细介绍

1、定义与核心思想

GraphQL 是一种为 API 设计的查询语言和运行时环境,由 Facebook 于 2012 年开发,2015 年开源。它并不是要取代 REST,而是提供一种更高效、更灵活的数据交互方式。其核心思想可概括为:

  • 由客户端声明数据需求:不再由服务端规定返回什么字段,客户端通过结构化查询精确指定需要的字段和关联关系。
  • 单一端点:与 REST 的多端点不同,GraphQL 通常只暴露一个端点(如 /graphql),所有数据请求都通过该端点完成。
  • 强类型系统:服务端通过 Schema 严格定义所有可查询的对象、字段、参数及类型,客户端和服务端均以此契约通信。
  • 一次性获取关联数据:可以在一次请求中将多个嵌套资源组合查询,避免 REST 常见的多次往返和过度获取。

2、核心构成:Schema、类型与操作

GraphQL 的一切基于 Schema。Schema 使用类型定义语言(SDL)描述 API 的完整能力。例如:

type User {
  id: ID!
  name: String
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String
  author: User!
}

type Query {
  user(id: ID!): User
  allPosts: [Post!]!
}

type Mutation {
  createPost(title: String!, authorId: ID!): Post
  deleteUser(id: ID!): Boolean
}

Schema 中定义了三种操作类型:

  • Query:用于读取数据,相当于 REST 的 GET。
  • Mutation:用于写入或修改数据,对应 POST/PUT/DELETE。
  • Subscription:基于 WebSocket 等协议实现的实时数据推送,当某个事件发生时服务端主动向客户端推送数据。

每个字段背后都有一个解析器(Resolver) 函数,负责从数据库或其他数据源获取实际数据,并完成关联关系的拼接。

3、与 REST 的关键差异

维度 REST GraphQL
端点数量 多个(按资源划分) 单一端点
数据获取方式 服务端决定返回哪些字段(可能过载或不足) 客户端精确声明所需字段
关联数据获取 需要多次请求多个端点 一次请求完成嵌套关联查询
版本管理 常见于 URL 中添加版本号 通过扩展 Schema 实现无版本演化
缓存策略 可依赖 HTTP 缓存机制 通常需客户端实现专门缓存
文件上传 原生支持 multipart/form-data 需通过特定规范(如graphql-multipart)或独立接口

GraphQL 把“请求什么数据”的控制权交给了客户端,极大地减少了网络请求次数和数据传输量,对移动端和复杂页面尤其有利。但同时也把安全职责从路由层转移到了数据解析层。

4、内省机制及其安全影响

内省(Introspection) 是 GraphQL 的一项内置功能,允许客户端通过保留字段 __schema__type 查询整个 Schema 的信息,包括:

  • 所有已定义的类型(types
  • 每个类型的字段、参数、返回值类型
  • 查询、变更、订阅的入口(queryTypemutationTypesubscriptionType
  • 自定义指令(directives

例如,最基础的探测查询:

query {
  __typename
}

若端点正常,将返回 {"data": {"__typename": "Query"}}

完整的 Schema 获取可通过标准的内省查询完成(此查询较长,官网和工具中均有现成版本),它能导出整个服务的数据结构。

安全风险:一旦生产环境未禁用内省,攻击者无需猜测即可获得完整的“攻击地图”,包括隐藏的管理字段(如 passwordroleisAdmin)、内部变更操作(如 deleteUser)和所有查询参数。这属于严重的信息泄露,是 GraphQL 安全测试的首要检查项。

即便内省被禁止,攻击者也可能通过错误消息字段提示、前端源码中的残留片段、字典猜测等方式部分还原 Schema(工具如 Clairvoyance 可自动化此过程)。

5、GraphQL 常见攻击面

  1. 访问控制缺陷与 IDOR 由于 GraphQL 解析器依赖参数直接获取对象,若未对当前用户的数据访问权进行逐行、逐字段验证,攻击者只需修改查询中的资源 ID(如 user(id: 1) 改为 user(id: 2))便可能越权读取或修改数据。接口层有全局认证,但数据层无授权是这类漏洞的典型成因。

  2. 注入攻击 查询参数可能被不安全地拼接到 SQL、NoSQL、OS 命令或模板中。与 REST 类似,攻击者可在字符串参数中注入恶意代码。但因 GraphQL 强类型,数字型字段会拒绝字符串输入,注入点主要集中在字符串类型的参数上。

  3. 拒绝服务(DoS) GraphQL 的灵活性使其极易受到资源耗尽攻击:

    • 深度嵌套查询:利用对象之间的关联进行递归查询,例如 user -> posts -> author -> posts -> ...,即使单字段轻量,深度叠加会指数级放大数据库负载。
    • 别名资源放大:同一查询中使用别名多次请求同一个资源,例如 a1: user(id:1) { name } a2: user(id:1) { name } ... a1000: ...,单次 HTTP 请求即可造成巨大开销。
    • 循环片段:通过 GraphQL 片段相互引用形成死循环,可能导致解析器崩溃。
    • 数组炸弹:对返回列表的字段传入超大的分页参数(如 first: 99999),拉取全表数据。
  4. 速率限制绕过 许多 API 是基于“每个 IP 每分钟请求数”来限制频率。攻击者可将数百个操作打包进一次 GraphQL 请求中——无论是通过别名(同一操作多次执行)还是批处理(多个独立 query 并列发送),都能让一个 HTTP 请求替代数百次调用,轻松突破速率限制。登录暴力破解正是由此得手。

  5. 跨站请求伪造(CSRF) 默认的 GraphQL 端点要求 Content-Type: application/json,跨域请求该类型会触发浏览器的 CORS 预检,因此常规 JSON POST 不易受 CSRF 攻击。然而,若服务端额外接受 application/x-www-form-urlencodedmultipart/form-data(或 GET 请求),攻击者就可以利用表单自动提交,以受害者的身份执行 mutation 操作,例如修改邮箱或删除资源。

  6. 信息泄露与错误消息 GraphQL 的详细错误响应可能泄露字段名、类型信息甚至数据库错误堆栈。例如,错误的查询会返回“Cannot query field 'xxx' on type 'User'. Did you mean 'password'?”这样的提示,攻击者可借此推断出真实的字段名。

6、总结

GraphQL 是一种革命性的 API 设计方式,它让数据获取变得极其高效和灵活。但它也重新定义了攻击面:安全性不再局限于端点防护,而必须深入到 Schema 的每一个字段、每一个解析器的权限验证和资源控制。理解内省、查询复杂度、别名机制和内容类型差异,是掌握 GraphQL 安全测试的基础。接下来,我们将使用 PortSwigger 的实验环境,逐一实践如何发现和利用这些漏洞。

在渗透测试中,发现 GraphQL API 是测试的起点。以下为详细的方法论,涵盖从主动扫描到被动分析的各类技术。

二、渗透测试中 GraphQL 端点的发现方法

一、基于路径的枚举与字典扫描

GraphQL 没有强制的端点命名规范,但实践中存在大量惯用路径。可使用字典对这些路径发起请求,通过响应特征进行判断。

常见端点路径:

  • /graphql
  • /api/graphql
  • /graphql/api
  • /graphql/v1
  • /v1/graphql
  • /graphql/console
  • /gql
  • /query
  • /graphiql (交互式调试界面,若出现 HTML 界面则直接确认)
  • /_graphql
  • /api

扫描策略:

  • 使用 Burp Intruder 或自定义脚本,对每个候选路径同时尝试 POSTGET 方法,并留意不同 Content-Type 头(如 application/jsonapplication/x-www-form-urlencoded)。
  • 注意路径大小写和结尾斜杠(如 /graphql/),部分服务端对路径敏感。

二、通过通用查询载荷确认端点

GraphQL 规范规定,任何有效的查询中都会包含保留字段 __typename。向疑似端点发送一个极简查询:

query{__typename}

判断依据:

  • 若端点正常,返回的 JSON 中必定包含 data 字段,且其内部有 __typename 字段,值为 "Query"(如果是 mutation 端点也可能是 "Mutation")。例如:
    { "data": { "__typename": "Query" } }
  • 即使禁止内省,该查询依然有效,因为它只取元数据无关 Schema 细节。
  • 也可尝试发送 {__schema{queryType{name}}} 测试内省是否开启,但若内省关闭则会返回错误。故优先使用 __typename 探测。

特殊情形:

  • 部分 GraphQL 端点要求请求中同时包含 query 参数和 variablesoperationName,即使不使用也应携带空值,例如:
    { "query": "query{__typename}", "variables": {}, "operationName": null }
  • 端点可能被配置为仅接受 GET 并通过查询参数传递查询串,如 GET /graphql?query=query{__typename}

三、利用不同请求方法与内容类型绕过限制

一些 GraphQL 实现会在非标准条件下露出端点,或对请求方法进行选择性处理。

方法变化:

  • POST 请求被阻止,尝试发送 GET,并将查询放入 URL 查询参数 ?query=...。部分服务器出于 Debug 模式或错误配置而接受此方式。
  • 尝试非常规方法如 PUTOPTIONS,但主要精力放在 POST/GET。

Content-Type 变化:

  • 标准的 GraphQL 使用 Content-Type: application/json
  • 也可尝试 application/x-www-form-urlencoded,将查询本体置于 query 表单字段中。某些框架自动兼容表单提交,这有助于绕过仅拦截 JSON 的 WAF 规则。
  • 若端点支持文件上传,可能接受 multipart/form-data,同样可测试。

四、通过错误消息推断和确认

即便请求无效,GraphQL 服务常返回特定结构的错误提示,可据此识别端点。

常见错误特征:

  • 返回 JSON 中包含 "errors" 数组,例如:
    { "errors": [{"message": "Must provide query string."}] }
  • 错误消息中出现 “query not present”、“syntax error”、“Expected Name” 等关键词。
  • 若发送非 JSON 请求,GraphQL 服务可能返回:“POST body missing, invalid Content-Type, or JSON object has no keys.” 这类消息直接泄露了它是一个 GraphQL 端点。

主动触发:

  • 向疑似端点发送空 POST 请求或随机字符串,观察是否有与 GraphQL 相关的错误信息。
  • 若前端存在调试界面(如 /graphiql),访问时页面源码会包含 “GraphQL” 特征字符串。

五、被动信息收集:前端源码与网络流量

现代 SPA 应用常在前端代码中泄露 GraphQL 端点及查询片段。

分析 JavaScript 文件:

  • 搜索静态资源中(.js、.map)的字符串:graphql"query""mutation"__schemaendpoint
  • 关注 Apollo Client、Relay、urql 等 GraphQL 客户端库的初始化代码,常直接暴露端点 URL。
  • 搜索查询字符串的注释或模板字面量,可能包含字段名称,这有助于后续猜解 Schema。

检查网络请求:

  • 在浏览器中使用开发者工具,观察 XHR/Fetch 请求,筛选以 /graphql 开头的请求。
  • 查看 Application 标签页的 Local Storage 或 Session Storage,可能存在端点配置项(如 graphqlEndpoint)。

六、搜索引擎与公共缓存利用

可通过搜索引擎 Dork 语法被动发现目标域的 GraphQL 端点。

搜索语法示例(注意必须遵守授权范围):

  • site:target.com intitle:"GraphQL" inurl:graphql
  • site:target.com intitle:"GraphiQL"
  • site:target.com "execute in the GraphQL playground"
  • 利用 Google 缓存、Wayback Machine 等查看历史版本源码。

七、工具自动化探测

Burp Suite:

  • Burp Scanner 的默认探针中已包含对 GraphQL 端点的检测,会尝试发送 __typename 查询并分析响应。在 Dashboard 中关注 “GraphQL endpoint found” 的提示。
  • 使用 Burp 扩展 InQL,可在发现端点后快速进行内省分析,但其本身也可辅助识别。

专用工具:

  • graphw00f:专门用于识别 GraphQL 引擎并探测端点,可发送多种探测载荷,检测引擎类型并输出防御措施建议。
  • Arjun:HTTP 参数发现工具,可针对单个端点发现支持的查询参数(如 queryvariables),当端点隐藏较深(如 /api)时帮助确认其是否为 GraphQL 服务,但需结合人工判断。
  • Clairvoyance:主要用于内省禁用时猜解 Schema,但也可通过反馈的字段存在性间接确认 GraphQL 身份(需要事先已知端点)。

八、通过路由与应用程序行为推测

部分应用将 GraphQL 隐藏在复杂路由背后,可通过功能逻辑推测。

  • 单页面应用中,若页面数据加载呈现出“一个请求返回多种关联数据”的特征(例如个人主页一次性返回用户信息、文章列表、关注者数),极可能背后是 GraphQL。
  • 网络请求 Payload 中反复出现 queryvariables 等 JSON 键,且请求路径始终为同一端点。
  • 响应 JSON 中带有 dataerrors 顶层字段,是 GraphQL 服务的强烈特征。

小结

发现 GraphQL 端点的本质是:路径枚举 + 通用查询确认 + 错误特征识别 + 前端与流量分析。单一端点配合 __typename 载荷是最可靠的判定手段。在实际测试中,将自动扫描与被动信息收集相结合,可最大限度降低遗漏风险,并为后续的 Schema 获取和漏洞利用铺垫基础。

三、靶场测试

先安装Burp中的这个插件InQL-GraphQL Scanner

image-20260428002613660

1. Accessing private GraphQL posts

把场地址:https://portswigger.net/web-security/graphql/lab-graphql-reading-private-posts

通关要求:找到隐藏的博客文章并输入密码。

访问靶场,抓取数据包,发现数据包

image-20260428002908145

这是InQL插件标记到的数据包,发送到重放模块,发送数据包,观察响应,发现没有id为3的博客,题目说密码隐藏在博客中,那id=3的文章中应该就包含需要的数据

把这个数据包发送到InQL ,插件自动向 /graphql/v1 端点发送标准的 GraphQL 内省查询,尝试获取完整的 Schema。

image-20260428005008995

image-20260428005028569

分析

image-20260428005044288

获取 Schema 后,InQL 对其进行分析,并将结果显示在扫描器界面中。截图左侧看到的,就是解析后的结果:

  • Queries: 所有可用的查询。
  • Mutations: 所有可用的变更(本实验没有)。
  • getAllBlogPosts / getBlogPost: 具体的查询名称,这就是可以调用的“函数”。
  • Points of Interest: 这是 InQL 安全分析的核心,它自动标记出了值得重点测试的“兴趣点”。

最关键的是,InQL 揭示了一个在正常前端请求中从未出现过的隐藏查询 getBlogPost 以及其下的敏感字段 postPassword 这就给了一份标明了所有入口和暗门的建筑蓝图。

把这个发送到重放模块

image-20260428005205121

修改id的值为3

image-20260428005256243

就得到了通过的密码,浏览器提交,成功通关

流程总结

整个执行逻辑可以归结为:发现隐藏查询 -> 生成攻击模板 -> 操控参数越权访问

  1. 前端只展示了 getAllBlogSummaries,但你通过 InQL 的工具发现了隐藏的 getBlogPost
  2. InQL 的 Points of Interest 功能帮你高亮出了 postPassword 这个关键数据。
  3. 你利用 getBlogPost 查询的 IDOR 漏洞,通过修改 id 参数,访问了本不属于你权限范围的 3 号文章。
  4. 最终在响应中获取到了通关所需的密码。

2. Accidental exposure of private GraphQL fields

靶场地址:https://portswigger.net/web-security/graphql/lab-graphql-accidental-field-exposure

通关要求:以管理员身份登录并删除用户名 carlos

进入靶场,随便输入账户名和密码进行登录,抓包登录的过程

image-20260428221745086

插件识别出了GraphQL API的数据包,发送到重放模块,在发送一次数据包,点开InQL插件

image-20260428223309563

分析

mutation login($input: LoginInput!) {
    login(input: $input) {
        token
        success
    }
}

逐句解释:

  • mutation login:声明这是一个名为 login 的变更操作(通常用于写操作,比如登录)。
  • ($input: LoginInput!):告诉服务器“我会给你一个名叫 $input 的参数,它的类型必须符合 LoginInput 定义,且不能为空(! 表示非空)”。
  • login(input: $input):调用服务器上名为 login 的字段(字段背后是一个解析函数),并把前面的 $input 传给它。
  • { token success }:我期望这个操作成功后,返回数据里要包含 tokensuccess 这两个字段。

本质上,这段文字描述了“我要做什么操作”和“我要得到什么数据”。

{
  "input": {
    "username": "admin",
    "password": "123456"
  }
}

这里:

  • 最外层的 "input" 对应上面查询里定义的 $input 变量。
  • 里面的 "username""password" 就是 LoginInput 类型里要求的字段,代表你要尝试登录的账号和密码。

所以说,变量就是“行动用到的实际数据”。

两者加在一起,就是一次完整的 GraphQL 请求

最终发给服务器的是类似这样的一个 JSON 包:

{
  "query": "mutation login($input: LoginInput!) { login(input: $input) { token success } }",
  "variables": {
    "input": {
      "username": "admin",
      "password": "123456"
    }
  }
}

image-20260428223550520

鼠标右键发送数据包到InQL插件,点击分析

image-20260428223631293

成功通过内省拿到了 GraphQL API 的“菜单”。扫描器列出的 getUsergetBlogPostgetAllBlogPosts 就是可供调用的查询。

最终目的是登录管理员删除用户,和用户相关的查询时getUser,鼠标右键把这个发送到重放模块,再次发送数据包查看

image-20260428224131119

没有有效数据返回,猜测id: 42 可能指向一个不存在的用户

遍历id的值

image-20260428224324922

可以看到修改不同的id就返回不同的用户名和密码,当di为1的时候返回了管理员的用户名和密码

image-20260428224423383

在浏览器登录这个账号,删除用户carlos就可以过关

实验的完成,印证了这个漏洞并非源于攻击手法的精妙,而在于两个“不该”的致命组合:

  1. 内省暴露数据结构(不该公开的菜单) GraphQL 的内省功能默认开启,像一份详细的菜单,直接暴露了 getUser 查询及其返回的 password 字段。这让攻击者的目标异常明确。
  2. 访问控制过于粗糙(不该有的缺位) 服务端只做了一个粗粒度检查——验证调用者是否登录,但缺少字段级别的细粒度授权。它没有判断一个已登录的普通用户,是否有权限读取另一个用户的 password 敏感字段。

一句话总结:设计上将“能否读到密码”的控制,错误地交给了客户端去决定。 攻击者只需通过简单的参数遍历,就能越过这道形同虚设的门禁,越权获取管理员凭据。这再次印证了,在安全设计上不能依赖“隐匿性”,必须实施服务端强制执行的字段级权限控制。

3. Finding a hidden GraphQL endpoint

靶场地址:https://portswigger.net/web-security/graphql/lab-graphql-find-the-endpoint

通关poc:GET /api?query=mutation{deleteOrganizationUser(input:{id:3}){user{id}}}

核心思路与原理

  • 核心目标:找到一个未公开的GraphQL端点,并利用其API删除carlos用户。

  • 主要障碍

    1. 端点隐蔽:该端点不常见,无法通过浏览网站直接发现。
    2. 内省防御:端点的内省(Introspection)功能被限制,无法直接查询全部Schema。由于GraphQL Schema就像是API的一份“使用说明书”,能告诉我们有哪些可用的数据和操作,所以绕过内省防御也就成为实验的关键。
  • 突破思路

    1. 利用请求方法差异:许多GraphQL端点只接受POST请求,但防御措施有时仅针对POST,尝试用GET请求探测内省,可能绕过防御。

    2. 绕过内省防御:内省功能可能只是被部分限制,而非完全禁用。通过在查询中插入特定字符(如换行符、空格),常常可以使某些简单的匹配逻辑失效,从而绕过防御。

    3. 完成任务:得到Schema后,即可构造删除用户的操作。

      绕过方法速查表

      序号 绕过方法 核心原理 适用场景举例
      1 请求方法差异 防御措施仅拦截过滤 POST 请求,未对 GET 请求做安全校验 通用配置错误场景
      2 字符编码绕过(CRLF / 空格) 插入%0a、特殊空格等字符,规避弱正则表达式匹配规则 PortSwigger 靶场、CVE-2024-37155
      3 注释符插入(#) 利用%23注释符截断语句,切断 WAF / 规则的检测逻辑 Web 通用常规绕过技巧
      4 内联片段(Inline Fragment) 嵌套__type内联查询,规避 GraphQL 字段访问限制 CVE-2026-30854(Parse Server)
      5 别名机制(__schema 别名) 借助忽略内省特性,自定义字段伪装__schema绕过检测 多款 GraphQL Armor 相关漏洞
      6 错误内容类型(Content-Type) 伪造非标准请求头(text/plain 等),使 WAF / 网关放弃报文检测 绕过 API 网关、WAF 内省防御
      7 SDL 端点泄露 访问未授权内部 SDL 接口,直接获取完整预编译 Schema 文件 CVE-2026-35413(Directus)
      8 WebSocket 端点绕过 切换 WebSocket 协议访问 GraphQL 服务,规避 HTTP 层防护 部分 Parse Server 暴露实例
      9 工具化盲内省(模糊测试) 爆破枚举字段建议接口,暴力猜解并重构完整 Schema 通用后手渗透、Clairvoyance 工具
      10 源码与文档信息泄露 前端 JS、Swagger、开发文档泄露接口与结构信息 通用开发配置遗留漏洞

进入靶场,导出点击,查看bp的流量,和题目描述的一样,确实没有看到GraphQL的端点的数据包,尝试常见的GraphQL端点,image-20260428233006188

image-20260428233027481

/api端点返回400,其他的返回404,大概这个就是隐藏端点了

或者也可以用bp的扫描来发现隐藏端点

image-20260428233203516

image-20260428233220507

把这个包发送到重发模块,探测内省状态:使用通用查询/?query=query{__typename},若响应中包含{"data": {"__typename": "query"}},则基本确认为GraphQL端点

image-20260428233425541

返回:

{
  "data": {
    "__typename": "query"
  }
}

查询之所以有效,是因为每个GraphQL端点都有一个名为__typename的保留字段,该字段以字符串形式返回查询对象的类型。引自:https://www.freebuf.com/articles/web/376645.html

尝试标准的__schema内省查询

image-20260428233714294

image-20260428234234748

查询被拦截或返回空,证明存在_scheme和_type的过滤,尝试使用空格、换行符和逗号等字符,因为它们会被GraphQL忽略,

image-20260428234415102

尝试修改请求为POST,回显不支持该方法

,尝试弱正则匹配

image-20260428234716879

成功绕过

接下俩获取完整的 Schema(Repeater 中操作)

修改 query 参数,将 _schema 的选择集展开,一次性拿到核心信息:在 Burp Repeater 中,将 GET 请求的 query 参数设置为:

{__schema%0a{types{name fields{name type{name kind}}}}}

URL 编码后(完整 GET 请求示例):

GET /api?query={__schema%0a{types{name%20fields{name%20type{name%20kind}}}}}

发送后,收到包含所有 QueryMutation 及自定义类型(如 UserDeleteOrganizationUserInput)的详细说明。

image-20260428235846095

拿到 Schema 后:根据 Schema 中 Query 类型的字段,找到获取用户的查询(通常叫 getUser),然后构造带 %0a 的简单查询(这里 %0a 是为了保持绕过防御的习惯,但其实不带也可能成功,因为此时你可能已经用有权限的会话了):

image-20260429000207312

payload: GET /api?query={getUser(id:1){id%20username}},id值可以遍历,为1的时候查询到用户administrator

3的时候用户为carlos

Schema中有删除 mutation

image-20260429000650083

通过图中schema分析

构造mutation

GET /api?query=mutation{deleteOrganizationUser(input:{id:1}){user{id}}} ,执行

image-20260429001353356

不能删除管理员,但是通关要求是删除carlos,修改id为3

image-20260429001449556

成功删除用户carlos,过关

实战类型靶场

靶场项目地址:https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application

先搭建环境,根据官方文档

← AntSword虚拟终端XSS到RCE漏洞复现 非对称加密与RSA算法 →