mcpField
H
hunt-fintech-graphql

hunt-fintech-graphql

github.com/elementalsouls安全5.3k159 次安装今天更新
代码仓库

关于 hunt-fintech-graphql

面向金融科技 GraphQL 渗透测试人员的专项安全技能,聚焦资金流转、精度处理和 KYC/个人信息授权风险,并提供分步测试方法与验证标准。

主要功能

  1. 01梳理资金变动突变与账本写入
  2. 02测试幂等键重放和金额精度
  3. 03检查源账户授权与跨账户 IDOR
  4. 04探测嵌套 KYC/个人信息字段
  5. 05验证批处理双花与质量赋值

适用场景

  1. 01审计转账、提现和奖励兑换接口
  2. 02检查金融 GraphQL 的账户访问控制
  3. 03测试金额解析、舍入及汇率一致性
  4. 04复现账本并发写入风险

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/elementalsouls/claude-bughunter --skill hunt-fintech-graphql

安装后在 Claude Code 中输入/hunt-fintech-graphql确认已加载。

详细介绍

为什么金融科技 GraphQL 属于不同的风险等级

通用 GraphQL 漏洞(IDOR、批量赋值、内省、批处理滥用——参见 hunt-graphql) 同样适用,但其影响范围会彻底改变:SaaS 应用中的解析器漏洞会泄露数据,而账本变更中的同类 漏洞则会转移资金。金融科技 GraphQL 后端具有三项特性,使其成为独特的狩猎目标:

  • 资金转移变更几乎总是基于复式记账账本的解析器。 单个 GraphQL 变更(transferFunds、redeemRewards、withdrawToBank)可能触发多笔 账本写入(借记 + 贷记 + 手续费),这些写入必须具备原子性。GraphQL 灵活的输入结构和 别名批处理很容易导致这些写入不同步。
  • 小数是攻击者可控的输入,而非显示格式。 金额、汇率、利息和奖励积分通常以 GraphQL 标量 (Float、String、自定义的 Decimal/Money 标量)传入。解析器如何解析和舍入这些值,本身 就是可利用的攻击面——非金融类 GraphQL API 几乎不存在这一问题。
  • KYC/PII 字段与常规账户字段位于同一类型中。 User 或 Account 类型通常会公开 ssnLast4、routingNumber、kycStatus、governmentIdUrl 或 linkedBankAccount,并与 displayName 和 email 并列——在架构中被广泛使用的类型上,只要缺少一次 字段级授权检查,就会波及所有访问该类型的查询。

攻击面信号

URL / 架构命名模式(除 hunt-graphql 的通用 /graphql 列表外):

/graphql/ledger
/graphql/payments
/api/wallet/graphql
/internal/ledger-graphql
/banking/graphql

值得在架构内省或 JS 包中搜索的字段 / 类型名称:

balance, availableBalance, pendingBalance, ledgerEntry, ledgerEntries
transferFunds, withdraw, redeem, topUp, reverseTransaction, adjustBalance
kycStatus, ssnLast4, routingNumber, accountNumber, governmentIdUrl
quoteExchangeRate, interestAccrued, rewardsPoints, portfolioValue
idempotencyKey, clientMutationId

该领域特有的技术栈线索:

  • Plaid/Stripe/Dwolla/Marqeta 封装在内部 GraphQL 网关后面(bankLink、plaidLinkToken 变更)
  • Apollo Federation 配有专用的 ledger 或 payments 子图——检查子图能否直接访问其自身的内省接口,从而绕过网关拼接后的架构
  • 架构中自定义的 Money/Decimal/BigDecimal GraphQL 标量(scalar Money)——值得直接对该标量的解析器进行模糊测试

先运行 hunt-graphql 的发现与内省方法获取架构;以下内容均假设你已拥有(或已部分枚举出)包含资金转移类型的架构。


逐步狩猎方法

  1. 梳理所有会触及余额的变更,无论是直接影响还是副作用。 不要只看 transfer*/withdraw*,还要检查 redeemRewards、applyCoupon、upgradeTier、 closeAccount(通常会退还余额)、disputeTransaction(通常会临时入账)。

  2. 针对每个资金流动变更,确认其账本写入形式。 一次变更调用会产生一条账本记录,还是多条(借记付款方、贷记收款方、费用记录)?多条记录的写入值得尝试竞态——参见阶段 4。

  3. 测试幂等键处理。 连续发送两次完全相同的变更(idempotencyKey / clientMutationId 相同),然后再延迟一段时间发送一次。如果第二次调用也写入账本,说明服务端没有强制执行幂等性——重放 = 重复执行。

  4. 对每个接受金额的参数测试小数和精度边界情况——参见 Payload 部分。 确认服务端舍入结果与客户端显示的舍入结果一致;两者不一致可直接变现。

  5. 针对账户/投资组合节点 ID 探测跨账户 IDOR,方法与 hunt-idor/hunt-graphql 相同,但要特别测试 transferFunds 这类变更是否会验证源账户属于已认证的调用者,而不只是验证存在具有该 ID 的某个账户。这是金融科技领域特有的 IDOR:对贷记目标账户正确实施授权后,开发者很容易忘记对借记来源账户进行授权(开发者觉得“向任意账户贷记看起来很安全”,但借记显然不安全,因此会检查——不过有时只检查一个方向)。

  6. 检查 KYC/PII 字段的字段级授权:从所有会返回共享 User/Account 类型的上下文查询该类型,而不只是检查个人资料页面。嵌入 counterparty { ssnLast4 } 的 transaction 类型就是常见的漏检位置,因为开发者对顶层 transaction 查询进行了授权,却没有重新检查嵌套 counterparty 的字段访问权限。

  7. 检查是否能通过批量赋值触达管理员权限变更,而不只是检查授权是否缺失。例如,输入对象包含客户端可设置的 status 或 override 字段,普通用户的变更本不应开放这些字段,但解析器却仍然接受(updateTransaction(input: {id, status: "COMPLETED", amount: "..."}))。

  8. 测试货币参数的一致性。 发送 UI 不会生成的、sourceCurrency/targetCurrency 不匹配的转账/报价变更(例如,使用货币兑换进行自转账),并检查解析器的外汇汇率查询与账本写入是否使用相同汇率——这里的 TOCTOU 窗口会造成可直接套利的漏洞。

  9. 将别名批处理与资金流动变更结合起来测试双重支出——参见 hunt-race-condition,了解在单靠别名批处理确认解析器没有按账户串行化写入后,如何升级为并行 HTTP 测试。


载荷与检测模式

幂等键重放测试:

mutation {
  transferFunds(input: {
    idempotencyKey: "test-key-001"
    sourceAccountId: "acc_1"
    destAccountId: "acc_2"
    amount: "10.00"
  }) { transactionId status }
}

使用完全相同的 idempotencyKey 发送两次。若得到两个成功且不同的 transactionId 值, 则说明未强制执行幂等性。

小数精度/舍入探测:

mutation { transferFunds(input: {sourceAccountId:"acc_1", destAccountId:"acc_2", amount: "0.001"}) { transactionId } }
mutation { transferFunds(input: {sourceAccountId:"acc_1", destAccountId:"acc_2", amount: "9999999999999999.99"}) { transactionId } }
mutation { transferFunds(input: {sourceAccountId:"acc_1", destAccountId:"acc_2", amount: "1e2"}) { transactionId } }
mutation { transferFunds(input: {sourceAccountId:"acc_1", destAccountId:"acc_2", amount: "-50.00"}) { transactionId } }

小于一分的金额可测试截断与舍入处理(重复 N 次以累积舍入误差导致的余额偏差);科学计数法和超大数值可测试 Money/Decimal 标量解析器是否会回退到原生浮点数/整数,并产生溢出或精度损失;负数金额可测试解析器是否假定符号由服务端处理,还是信任客户端。

别名批处理双重消费探测(升级到并行 HTTP 前先确认):

mutation {
  r1: redeemRewards(input: {rewardId: "rwd_1", accountId: "acc_1"}) { success }
  r2: redeemRewards(input: {rewardId: "rwd_1", accountId: "acc_1"}) { success }
  r3: redeemRewards(input: {rewardId: "rwd_1", accountId: "acc_1"}) { success }
}

如果针对仅限单次使用的奖励/优惠券,多个别名都能成功,则说明解析器不会在批处理请求中按账户/资源串行执行写入操作——请参阅 hunt-race-condition,了解如何结合并行 HTTP POST 来确认实际的双重消费影响。

源账户授权探测(非对称 IDOR 检查):

mutation {
  transferFunds(input: {
    sourceAccountId: "VICTIM_ACCOUNT_ID"
    destAccountId: "ATTACKER_CONTROLLED_ACCOUNT_ID"
    amount: "1.00"
  }) { transactionId status }
}

使用攻击者自己的会话/令牌运行。若成功,说明解析器验证了目标账户由攻击者控制(这是显而易见的必要条件),但从未验证源账户是否属于调用者。

嵌套字段级 PII 探测:

query {
  transaction(id: "txn_123") {
    amount
    counterparty { displayName ssnLast4 routingNumber kycStatus }
  }
}

使用与交易对手除共享一笔交易外毫无关联的用户进行查询;即使顶层 transaction 查询已正确限定交易范围,嵌套 PII 字段查询成功仍属于发现的问题。

针对管理员类输入字段的批量赋值探测:

mutation {
  updateTransaction(input: {id: "txn_123", status: "COMPLETED", amount: "0.01"}) { id status }
}

以非管理员用户身份,对客户端 UI 从不开放这些字段的变更操作发送请求;如果架构仍接受这些字段,则属于对账本状态的批量赋值。


常见根本原因

  1. 仅在客户端验证金额/费用。 UI 会计算并显示正确金额;解析器却信任 GraphQL 客户端实际发送的任何值,因为“应用总会发送正确的值”。
  2. 非原子性的多条账本写入。 借记、贷记和费用分成多个顺序语句写入,而非置于单个事务/锁中——由此产生的竞态窗口,正是别名批处理加并行 HTTP 所利用的。
  3. Money/Decimal 标量在边界情况输入下回退到原生浮点数解析(科学计数法、超大字符串),使浮点舍入误差重新进入原本应保证定点精度的系统。
  4. 幂等键虽被存储,但在执行写入前从未检查——该键仅用于支持/调试日志,并未用作去重门控。
  5. 字段级授权按顶层查询实现,而非按类型实现。 User/Account 类型的敏感字段在直接查询时(me { ssnLast4 })受到保护,但在无关查询中嵌套返回同一类型时(transaction { counterparty {...} })却不受保护。
  6. 缺少源账户所有权检查,但存在目标账户是否存在的检查——请参阅方法步骤 5。借记看起来风险较高,因此会经过审查;而“此账户是否属于调用者”的检查,却悄悄地只应用在入账一侧。
  7. 管理员/内部变更操作复用与公共变更操作相同的输入类型,只是增加了额外的可选字段——解析器层没有任何逻辑会为非管理员调用者移除这些字段。

Gate 0 验证

资金转移类发现的验证门槛应高于常见的 GraphQL IDOR——“查询返回了他人的余额”确实构成影响;“我发送了格式错误的金额,结果收到 400”则不算。

  1. 是否实际写入了账本,并且你能证明这一点? 查询账户变更前后的余额——状态发生变化(而不只是响应正文返回 200/成功)才是证据。
  2. 结果是否可确定复现,而非时机巧合? 对于竞态/双重消费类发现,应从干净状态复现两次。如果只在特定负载条件下有效,就如实记录触发窗口,不要声称漏洞一定可利用。
  3. 该发现是否转移了攻击者原本不拥有的资金,或泄露了攻击者不应看到的数据——而不只是“变更接受了意外的输入类型,API 返回了一条错误消息”。格式错误的 Money 标量导致详细的 GraphQL 错误泄露堆栈跟踪,这属于 hunt-source-leak 类发现,而非金融科技逻辑类发现——报告中不要混为一谈。

相关技能与攻击链

  • hunt-graphql — 通用 GraphQL 发现、内省绕过、节点 ID IDOR 和别名批处理机制的上级技能。请先加载此技能;hunt-fintech-graphql 假定你已掌握该方法,只补充资金转移相关的特定内容。
  • hunt-business-logic — 优惠券/奖励重复兑换及其他逻辑缺陷模式,可直接推广到此处的 redeemRewards/applyCoupon 类变更。
  • hunt-race-condition — 一旦别名批处理单独证实资金转移变更不会串行写入,后续升级路径就是结合并行 HTTP / 单数据包攻击,制作可确定复现的双重消费 PoC。
  • hunt-api-misconfig — 批量赋值和 JWT 声明篡改模式,直接适用于普通用户可访问的管理员形 GraphQL 输入对象。
  • hunt-idor — 源账户与目标账户之间的不对称授权模式(步骤 5),是通用“变更参数 IDOR”类别在金融科技中的具体实例。
  • evidence-hygiene — 余额截图和账本条目 PoC 需要遵循与其他任何捕获内容相同的 Cookie/PII 编辑规范,此外还要注意,切勿原样包含真实金融账户的实际账号/余额。
  • triage-validation — 起草报告前应用上述 Gate 0;没有证明账本状态发生变化的内容,会被金融科技项目的分流团队立即驳回。

评论

支持 Markdown