GraphQL API 安全攻防学习---基于PortSwigger靶场
GraphQL API 安全攻防学习---基于PortSwigger靶场
参考文章: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) - 每个类型的字段、参数、返回值类型
- 查询、变更、订阅的入口(
queryType、mutationType、subscriptionType) - 自定义指令(
directives)
例如,最基础的探测查询:
query {
__typename
}若端点正常,将返回 {"data": {"__typename": "Query"}}。
完整的 Schema 获取可通过标准的内省查询完成(此查询较长,官网和工具中均有现成版本),它能导出整个服务的数据结构。
安全风险:一旦生产环境未禁用内省,攻击者无需猜测即可获得完整的“攻击地图”,包括隐藏的管理字段(如 password、role、isAdmin)、内部变更操作(如 deleteUser)和所有查询参数。这属于严重的信息泄露,是 GraphQL 安全测试的首要检查项。
即便内省被禁止,攻击者也可能通过错误消息字段提示、前端源码中的残留片段、字典猜测等方式部分还原 Schema(工具如 Clairvoyance 可自动化此过程)。
5、GraphQL 常见攻击面
访问控制缺陷与 IDOR 由于 GraphQL 解析器依赖参数直接获取对象,若未对当前用户的数据访问权进行逐行、逐字段验证,攻击者只需修改查询中的资源 ID(如
user(id: 1)改为user(id: 2))便可能越权读取或修改数据。接口层有全局认证,但数据层无授权是这类漏洞的典型成因。注入攻击 查询参数可能被不安全地拼接到 SQL、NoSQL、OS 命令或模板中。与 REST 类似,攻击者可在字符串参数中注入恶意代码。但因 GraphQL 强类型,数字型字段会拒绝字符串输入,注入点主要集中在字符串类型的参数上。
拒绝服务(DoS) GraphQL 的灵活性使其极易受到资源耗尽攻击:
- 深度嵌套查询:利用对象之间的关联进行递归查询,例如
user -> posts -> author -> posts -> ...,即使单字段轻量,深度叠加会指数级放大数据库负载。 - 别名资源放大:同一查询中使用别名多次请求同一个资源,例如
a1: user(id:1) { name } a2: user(id:1) { name } ... a1000: ...,单次 HTTP 请求即可造成巨大开销。 - 循环片段:通过 GraphQL 片段相互引用形成死循环,可能导致解析器崩溃。
- 数组炸弹:对返回列表的字段传入超大的分页参数(如
first: 99999),拉取全表数据。
- 深度嵌套查询:利用对象之间的关联进行递归查询,例如
速率限制绕过 许多 API 是基于“每个 IP 每分钟请求数”来限制频率。攻击者可将数百个操作打包进一次 GraphQL 请求中——无论是通过别名(同一操作多次执行)还是批处理(多个独立 query 并列发送),都能让一个 HTTP 请求替代数百次调用,轻松突破速率限制。登录暴力破解正是由此得手。
跨站请求伪造(CSRF) 默认的 GraphQL 端点要求
Content-Type: application/json,跨域请求该类型会触发浏览器的 CORS 预检,因此常规 JSON POST 不易受 CSRF 攻击。然而,若服务端额外接受application/x-www-form-urlencoded或multipart/form-data(或 GET 请求),攻击者就可以利用表单自动提交,以受害者的身份执行 mutation 操作,例如修改邮箱或删除资源。信息泄露与错误消息 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 或自定义脚本,对每个候选路径同时尝试
POST和GET方法,并留意不同Content-Type头(如application/json、application/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参数和variables或operationName,即使不使用也应携带空值,例如:{ "query": "query{__typename}", "variables": {}, "operationName": null } - 端点可能被配置为仅接受 GET 并通过查询参数传递查询串,如
GET /graphql?query=query{__typename}。
三、利用不同请求方法与内容类型绕过限制
一些 GraphQL 实现会在非标准条件下露出端点,或对请求方法进行选择性处理。
方法变化:
- 若
POST请求被阻止,尝试发送GET,并将查询放入 URL 查询参数?query=...。部分服务器出于 Debug 模式或错误配置而接受此方式。 - 尝试非常规方法如
PUT或OPTIONS,但主要精力放在 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"、__schema、endpoint。 - 关注 Apollo Client、Relay、urql 等 GraphQL 客户端库的初始化代码,常直接暴露端点 URL。
- 搜索查询字符串的注释或模板字面量,可能包含字段名称,这有助于后续猜解 Schema。
检查网络请求:
- 在浏览器中使用开发者工具,观察 XHR/Fetch 请求,筛选以
/graphql开头的请求。 - 查看 Application 标签页的 Local Storage 或 Session Storage,可能存在端点配置项(如
graphqlEndpoint)。
六、搜索引擎与公共缓存利用
可通过搜索引擎 Dork 语法被动发现目标域的 GraphQL 端点。
搜索语法示例(注意必须遵守授权范围):
site:target.com intitle:"GraphQL" inurl:graphqlsite: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 参数发现工具,可针对单个端点发现支持的查询参数(如
query、variables),当端点隐藏较深(如/api)时帮助确认其是否为 GraphQL 服务,但需结合人工判断。 - Clairvoyance:主要用于内省禁用时猜解 Schema,但也可通过反馈的字段存在性间接确认 GraphQL 身份(需要事先已知端点)。
八、通过路由与应用程序行为推测
部分应用将 GraphQL 隐藏在复杂路由背后,可通过功能逻辑推测。
- 单页面应用中,若页面数据加载呈现出“一个请求返回多种关联数据”的特征(例如个人主页一次性返回用户信息、文章列表、关注者数),极可能背后是 GraphQL。
- 网络请求 Payload 中反复出现
query、variables等 JSON 键,且请求路径始终为同一端点。 - 响应 JSON 中带有
data和errors顶层字段,是 GraphQL 服务的强烈特征。
小结
发现 GraphQL 端点的本质是:路径枚举 + 通用查询确认 + 错误特征识别 + 前端与流量分析。单一端点配合 __typename 载荷是最可靠的判定手段。在实际测试中,将自动扫描与被动信息收集相结合,可最大限度降低遗漏风险,并为后续的 Schema 获取和漏洞利用铺垫基础。
三、靶场测试
先安装Burp中的这个插件InQL-GraphQL Scanner

1. Accessing private GraphQL posts
把场地址:https://portswigger.net/web-security/graphql/lab-graphql-reading-private-posts
通关要求:找到隐藏的博客文章并输入密码。
访问靶场,抓取数据包,发现数据包

这是InQL插件标记到的数据包,发送到重放模块,发送数据包,观察响应,发现没有id为3的博客,题目说密码隐藏在博客中,那id=3的文章中应该就包含需要的数据
把这个数据包发送到InQL ,插件自动向 /graphql/v1 端点发送标准的 GraphQL 内省查询,尝试获取完整的 Schema。


分析

获取 Schema 后,InQL 对其进行分析,并将结果显示在扫描器界面中。截图左侧看到的,就是解析后的结果:
Queries: 所有可用的查询。Mutations: 所有可用的变更(本实验没有)。getAllBlogPosts/getBlogPost: 具体的查询名称,这就是可以调用的“函数”。Points of Interest: 这是 InQL 安全分析的核心,它自动标记出了值得重点测试的“兴趣点”。
最关键的是,InQL 揭示了一个在正常前端请求中从未出现过的隐藏查询 getBlogPost 以及其下的敏感字段 postPassword。 这就给了一份标明了所有入口和暗门的建筑蓝图。
把这个发送到重放模块

修改id的值为3

就得到了通过的密码,浏览器提交,成功通关
流程总结
整个执行逻辑可以归结为:发现隐藏查询 -> 生成攻击模板 -> 操控参数越权访问。
- 前端只展示了
getAllBlogSummaries,但你通过 InQL 的工具发现了隐藏的getBlogPost。 - InQL 的
Points of Interest功能帮你高亮出了postPassword这个关键数据。 - 你利用
getBlogPost查询的 IDOR 漏洞,通过修改id参数,访问了本不属于你权限范围的 3 号文章。 - 最终在响应中获取到了通关所需的密码。
2. Accidental exposure of private GraphQL fields
靶场地址:https://portswigger.net/web-security/graphql/lab-graphql-accidental-field-exposure
通关要求:以管理员身份登录并删除用户名 carlos
进入靶场,随便输入账户名和密码进行登录,抓包登录的过程

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

分析
mutation login($input: LoginInput!) {
login(input: $input) {
token
success
}
}逐句解释:
mutation login:声明这是一个名为login的变更操作(通常用于写操作,比如登录)。($input: LoginInput!):告诉服务器“我会给你一个名叫$input的参数,它的类型必须符合LoginInput定义,且不能为空(!表示非空)”。login(input: $input):调用服务器上名为login的字段(字段背后是一个解析函数),并把前面的$input传给它。{ token success }:我期望这个操作成功后,返回数据里要包含token和success这两个字段。
本质上,这段文字描述了“我要做什么操作”和“我要得到什么数据”。
{
"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"
}
}
}
鼠标右键发送数据包到InQL插件,点击分析

成功通过内省拿到了 GraphQL API 的“菜单”。扫描器列出的 getUser、getBlogPost、getAllBlogPosts 就是可供调用的查询。
最终目的是登录管理员删除用户,和用户相关的查询时getUser,鼠标右键把这个发送到重放模块,再次发送数据包查看

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

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

在浏览器登录这个账号,删除用户carlos就可以过关
实验的完成,印证了这个漏洞并非源于攻击手法的精妙,而在于两个“不该”的致命组合:
- 内省暴露数据结构(不该公开的菜单) GraphQL 的内省功能默认开启,像一份详细的菜单,直接暴露了
getUser查询及其返回的password字段。这让攻击者的目标异常明确。- 访问控制过于粗糙(不该有的缺位) 服务端只做了一个粗粒度检查——验证调用者是否登录,但缺少字段级别的细粒度授权。它没有判断一个已登录的普通用户,是否有权限读取另一个用户的
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用户。主要障碍:
- 端点隐蔽:该端点不常见,无法通过浏览网站直接发现。
- 内省防御:端点的内省(Introspection)功能被限制,无法直接查询全部Schema。由于GraphQL Schema就像是API的一份“使用说明书”,能告诉我们有哪些可用的数据和操作,所以绕过内省防御也就成为实验的关键。
突破思路:
利用请求方法差异:许多GraphQL端点只接受
POST请求,但防御措施有时仅针对POST,尝试用GET请求探测内省,可能绕过防御。绕过内省防御:内省功能可能只是被部分限制,而非完全禁用。通过在查询中插入特定字符(如换行符、空格),常常可以使某些简单的匹配逻辑失效,从而绕过防御。
完成任务:得到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端点,

/api端点返回400,其他的返回404,大概这个就是隐藏端点了
或者也可以用bp的扫描来发现隐藏端点


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

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


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

尝试修改请求为POST,回显不支持该方法
,尝试弱正则匹配

成功绕过
接下俩获取完整的 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}}}}}发送后,收到包含所有 Query、Mutation 及自定义类型(如 User、DeleteOrganizationUserInput)的详细说明。

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

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

通过图中schema分析
构造mutation
GET /api?query=mutation{deleteOrganizationUser(input:{id:1}){user{id}}} ,执行

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

成功删除用户carlos,过关
实战类型靶场
靶场项目地址:https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application