关于 aegis-api-security
aegis-api-security 是用于审查代码仓库的技能,可发现 API 设计和配置缺陷,防止客户端越权。它侧重静态证据、复现步骤、严重性依据,以及适配框架的修复建议,覆盖面向 HTTP 的 API。
主要功能
- 01审查请求绑定与批量赋值风险
- 02检查速率限制和分页上限
- 03审查 GraphQL 与 gRPC 暴露面
- 04检查 REST、API 密钥与错误泄露
- 05评估 WebSocket 和 SSE 认证
适用场景
- 01在 API 发布前审查代码仓库
- 02定位缺少限流或范围上限的端点
- 03检查 Schema 与文档的暴露情况
- 04以证据和修复建议记录发现
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ as-foss/ aegis-skills --skill aegis-api-security安装后在 Claude Code 中输入/aegis-api-security确认已加载。
附带文件references/background.mdreferences/further-reading.md
详细介绍
范围与目标
目标
审计目标仓库面向 HTTP 的接口,查找 API 特有的设计与配置缺陷,这些缺陷可能让客户端超出预期权限:写入服务器本不打算开放的字段、消耗不受限制的资源、通过架构和文档接口枚举内部信息、绕过传输层或通道身份验证,或跨源滥用特权通道。针对每项发现,提供文件:行号证据、复现步骤(优先静态分析)、严重性及其理由,以及符合框架规范的修复方案。
范围内
| 类别 | 常见发现 | 主要 CWE |
|---|---|---|
| 批量赋值 / 过度提交 | 客户端通过请求体绑定到 ORM 实体,设置 role、is_admin、price、user_id | CWE-915 |
| 速率限制与反自动化 | 身份验证、搜索、导出接口不受限,没有分页上限,没有按密钥设置配额 | CWE-770、CWE-799 |
| GraphQL 配置 | 生产环境启用内省、暴露 playground/GraphiQL、没有深度/复杂度限制、批处理滥用、字段建议泄露 | CWE-200、CWE-400 |
| gRPC / protobuf | 公开反射服务、可从外部访问明文 h2c 监听器、缺少每个 RPC 的身份验证拦截器 | CWE-306、CWE-200 |
| REST 规范 | _method 覆盖造成的影子接口、具有状态更改效果的 GET、已弃用版本仍可用、未经身份验证的 OpenAPI 文档、调试标头、允许凭据的 CORS 通配符、内容类型混淆入口、信息泄露的错误响应 | CWE-200、CWE-942 |
| API 密钥管理 | 接受 URL 中的密钥、在移动端/SDK 构建中嵌入共享密钥、无范围限制的全权限密钥、没有撤销流程 | CWE-798 |
| WebSocket / SSE 通道 | 升级时缺少 Origin 验证、仅在握手时进行身份验证、主题缺少按用户设置的 ACL | CWE-346、CWE-862 |
范围外(交叉引用)
- 凭据填充的阈值和账户锁定策略细节 -> AUTHN 模块。本模块仅覆盖平台限流层。
- 业务流程中的对象级 IDOR 逻辑 -> AUTHZ 模块。此处仅涵盖 API 机制使其成为可能的情形(GraphQL 解析器、订阅主题)。
- 通过 XML 内容类型混淆构造 XXE 载荷 -> DESER 模块。仅标记接受此类输入的接口。
- CSRF 令牌设计 -> WEB 模块。说明与
_method覆盖的相互作用;令牌分析交由该模块处理。 - 完整的 CORS Origin 反射矩阵 -> CONFIG 模块。将 API 侧的通配符+凭据观察结果作为输入记录。
- 响应或日志中回显的机密信息 -> SECRETS 模块。此处标记密钥出现在 URL 中导致的日志泄露。
- 嵌套查询炸弹的定量 DoS 计算 -> DOS 模块。确认缺少限制即可;不要计算影响范围。
操作假设
只读访问仓库;不保证存在可运行的实例。动态复现为可选项,且仅允许针对明确授权的环境。基于已记录的框架语义进行静态确认,足以作为报告证据。
前置知识与术语
零基础入门:本模块使用的术语,每项一行。仓库 GUIDE.md 术语表中包含每种类别的详细通俗解释。
- 批量赋值:框架将整个请求体复制到对象中,使客户端能够设置
is_admin等字段 - BOLA:更改请求中的标识符以访问其他用户的对象(详见 AUTHZ 模块)
- 速率限制:限制每个客户端在每个时间窗口内可发起的请求数
- 分页上限:单次调用允许的最大页面大小,防止一次性导出百万行数据
- 内省 / playground:GraphQL 自我文档功能,会向攻击者提供 API 地图
- 解析器:负责响应每个 GraphQL 查询的字段级函数;每个解析器都需要单独进行权限检查
- 影子 API:未列入文档但仍在运行的接口,因此也逃过了审核
- 污点流 / source→sink:不可信数据从入口点流向危险函数的路径
思维模型
一个端点,四种隐式契约
API 处理程序组合了四种客户端只能部分感知的契约:
- 传输:HTTP/1.1、HTTP/2、h2c、WebSocket 升级、SSE 流。
- 路由/中间件:处理程序主体运行前执行了哪些守卫。
- 绑定:原始字节如何在服务端转换为类型化对象。
- 序列化:哪些服务端对象会再次转换为字节。
大多数 API 安全问题都源于开发者建模的内容与框架在某一层自动生成的内容不一致。批量赋值问题出现在绑定和序列化环节。限流、来源和通道身份验证问题出现在传输和中间件环节。信息泄露问题则出现在架构暴露面:OpenAPI 文件、GraphQL 内省、protobuf 反射。
框架魔法会改变默认安全态势
现代技术栈优先考虑开发者体验:将整个请求正文绑定到模型、从 JSON 填充实体、向工具公开描述符。每一项便利功能都意味着更宽松的默认设置:
- 宽松的绑定会导致批量赋值(CWE-915)。
- 宽松的架构暴露会泄露内部信息(内省、
/openapi.json、gRPC 反射)。 - 宽松的请求处理没有默认上限来限制列表大小、查询深度或请求速率。
审计时,应查找明确选择加入安全措施的配置(字段允许列表、限流器、禁用内省、身份验证拦截器),而不只是确认没有明确选择退出。除非已证实并非如此,否则应假定默认设置是宽松的。
成本不对称决定限流类别
每个端点都有服务端成本,也有客户端代价。带联接的搜索、导出、密码重置邮件发送和短信发送都会放大成本;分页上限和限流器就是计量表。缺少计量表,就会让一次请求变成一万次自动化请求:枚举、暴力破解、抓取、资金损失。评估每个高成本端点时,只需问一个问题:是什么阻止脚本调用它 10,000 次?
GraphQL 将边界收缩到解析器
一个 URL、数千个可访问的解析器,以及由客户端塑造的查询。如果解析器跳过对象级检查,理由是“架构是内部的”,那么在 /graphql 上进行边界身份验证毫无作用。架构本身就是对象图的公开地图;查询深度则是由攻击者选择的循环计数器。除非中间件证明情况并非如此,否则应将每个解析器都视为未经身份验证的处理程序;并将内省、playground 和字段建议视为文档泄露。
gRPC 将相同的问题下移一层
Protobuf 取代 JSON 绑定;反射取代 Swagger;拦截器取代中间件。要提出相同的问题:谁可以调用哪些 RPC(由拦截器强制执行的逐方法身份验证元数据,还是根本没有?);超大消息或填满未知字段的消息会对内存和存储造成什么影响;集群外部是否可以访问明文 h2c 端口?
通道的存续时间超过握手
WebSocket 和 SSE 连接会长期保持,并通过消息复用操作。只在握手时进行身份验证、却不对每条消息授权,意味着任何已连接的套接字在整个连接期间都会继承会话权限。如果升级时也缺少 Origin 检查,受害者浏览器中的恶意网页就能跨源打开该套接字,并利用受害者的 Cookie。
检查要点
批量赋值 / 过度提交
- 找出所有请求正文接收点(控制器操作、GraphQL mutation 解析器、gRPC 处理程序)。明确记录绑定的具体类型,以及它是 ORM/领域实体还是专用请求 DTO。
- 标记直接绑定到 ORM 实体的情况:DRF
ModelSerializer使用宽松字段,Mongoose 模型直接传给create()/findByIdAndUpdate(),Sequelize/Prismacreate(req.body),EloquentModel::create($request->all()),RailsModel.new(params)未使用permit,Spring@ModelAttribute绑定到 JPA 实体,ASP.NET Core[FromBody] User user中的User是DbSet<>实体,Gojson.NewDecoder(r.Body).Decode(&domainStruct)将数据解码到共享模型。 - 在序列化器/架构定义中搜索允许列表缺失的情况:DRF
fields = "__all__",NestJS DTO 使用的ValidationPipe()未设置whitelist: true,Laravel 模型缺少$fillable(或设置了$guarded = []),Pydantic 模型配置了extra="allow"。 - 枚举每个绑定模型上可设置的敏感字段候选项:
role、roles、is_admin、admin、permissions、price、amount、balance、discount、status、verified、email_verified、user_id、owner_id、account_id、tenant_id、created_at、password_hash、credits。对每个候选项,追踪是否有任何面向客户端的路径会绑定它。 - 专门检查 PATCH/PUT 合并路径:
Object.assign(entity, req.body)、将req.body展开到更新对象中、Python 遍历载荷键的setattr循环、Rubymerge!(params)、对实体调用 EF CoreCurrentValues.SetValues(dto)、dict.update(payload)。 - 检查会将同一缺陷级联到更深层的嵌套/内嵌写入:Rails
accepts_nested_attributes_for、可写的嵌套 DRF 序列化器、从请求正文填充的 Mongoose 子文档数组。 - 检查响应是否对称,以作佐证:如果 GET 会返回某个字段,但没有记录在案的写入路径,在报告前确认是否存在未记录的路径会绑定该字段。
速率限制与反自动化
- 按路由组还原中间件链;记录哪些路由启用了速率限制,哪些没有。优先检查缺少任何速率限制的身份验证(登录、注册、密码重置、OTP)、搜索、导出、报告生成、触发邮件/SMS 的端点,以及支付报价端点。
- 确认速率限制的键设置方式:仅使用 IP 的键在代理后可能失效,因为客户端可控制
X-Forwarded-For(app.set('trust proxy', true))。优先使用用户 ID/API 密钥,并以 IP 作为回退。 - 列出所有集合端点;验证服务器端是否设置了硬性上限:分页大小上限、受限的偏移量/窗口、游标范围、
SELECT ... LIMIT,不会通过.all()/.find({})/FindAll返回无界集合,且导出端点不会在单个请求中流式传输整张表。 - 对计量型 API,验证是否存在并在服务器端强制执行每个 API 密钥的配额(配额中间件、计划层级表),而非仅在文档中宣称提供配额。
- 检查易遭滥用的流程是否设置验证码/工作量证明或同等的操作阻力:注册、投票、批量邀请、奖励/礼品卡兑换。
- 记录会增加单次请求成本的因素:预先加载所有内容(
include: [...all]、populate()、select_related链)、跨大型表的搜索联接、PDF/报告生成。
GraphQL 层
- 查找服务器引导代码(apollo-server、
@nestjs/graphql、graphql-yoga、strawberry-graphql、graphene、gqlgen、graphql-go)。确定生产环境中 introspection 和 landing-page/playground 标志的值,而不只是确认它们是否存在于代码中。 - 验证查询校验控制:深度限制、复杂度/成本分析、别名数量限制(如受支持),以及错误中是否禁用了字段建议。
- 抽查解析器是否存在对象级授权缺口:接受
id/uuid参数并获取、返回对象,却不检查所有权或租户权限的查询/变更,尤其是靠近“internal schema”等注释的地方。 - 静态检查批处理配置:端点是否接受数组请求体(
[{"query":"..."},...]),从而允许每个请求执行多个操作?是否使用带有已注册操作允许列表的 AutomaticPersistedQuery,还是接受任意哈希? - 检查错误格式化路径:未知字段是否会出现“Did you mean”建议,
errors[].extensions中是否包含堆栈跟踪或内部路径。 - 订阅:在订阅时和每次事件投递时都进行身份验证;检查主题/频道 ACL。
gRPC / Protobuf
- 查找反射注册,并确定是否按环境启用(仅开发环境还是始终启用)。
- 确认监听器凭据:明文
NewServer()/h2c 还是 TLS;将监听端口与 DockerfileEXPOSE、Kubernetes 清单和负载均衡器配置进行比对,以判断其是否可从外部访问。 - 验证身份验证是否强制执行:验证元数据(令牌、API 密钥)的单向/流式拦截器是否应用于所有已注册服务,还是有些服务注册时未使用该拦截器?确认有意豁免的服务(健康检查)范围是否严格受限。
- 评估消息健壮性:
MaxRecvMsgSize是否设置合理(不是math.MaxInt64);是否使用google.protobuf.Any根据类型 URL 进行分派;持久化路径上如何处理未知字段;存储前是否验证枚举值。 - 检查网关暴露情况(grpc-gateway、Envoy JSON 转码):HTTP 路径是否强制执行与原生 RPC 相同的身份验证?
REST 规范
- 动词/路由遮蔽:检查
_method表单/查询参数和X-HTTP-Method-Override中间件;确认被覆盖的动词受允许列表限制,并在使用会话身份验证时受到 CSRF 保护。 - GET 请求中的状态变更:扫描路由表,查找包含变更类动词(
delete、update、create、reset、send、confirm、approve)的 GET 路径。 - 版本泛滥:同时挂载多个
/vN前缀;已弃用的版本仍在接收流量;安全中间件(授权防护、限流器)只接入最新路由器,而/v1则没有任何防护。 - 文档暴露:检查
/swagger、/swagger-ui.html、/api-docs、/openapi.json、带有 HTML Accept 头的/graphql、redoc 页面、drf-yasg、springdoc actuator 映射;确认生产环境配置中每个入口都需要身份验证。还要检查规范内容,查看其中描述的仅供内部使用的主机/端点是否被公开。 - 调试/内部标头:检查
X-Powered-By、X-AspNet-Version、SymfonyX-Debug-Token及可访问的/_profiler、Server横幅,以及列出内部信息的Access-Control-Expose-Headers。 - 各路由组的中间件一致性:比较
app.use(...)的顺序;查找跳过现代路由组所应用身份验证或限流中间件的路由组(/internal、/webhooks、旧版/v1)。 - Content-Type 处理:识别支持 XML 的端点(仅关注入口点;载荷构造属于 DESER),以及接受
text/plain并绕过基于 Content-Type 检查的 CSRF 防护的路由,还有会解析请求体的 GET 处理器。 - 内容协商:检查由 Accept 标头驱动的序列化器(DRF
renderer_classes、ASP.NET 输出格式化器、Rails responders)是否会暴露比默认 JSON 视图更丰富的序列化内容(例如,?format=xml泄露关联数据)。 - 错误封装:是否有统一错误处理器?检查错误响应正文是否泄露堆栈跟踪、类名、内部主机名;是否因未关闭环境标志而保持高详细度。
- 隐藏 API 发现:将已实现的路由表与 OpenAPI 规范逐一比对——代码中存在但未出现在已发布规范中的端点属于未记录的攻击面;记录每处差异两侧的文件:行号证据。
- 网关层访问日志分析标记:确认 API 访问日志会用于某种分析/异常审查(按密钥统计流量基线、错误率突增);如果只收集日志却从不分析,滥用模式就会被忽视。
API 密钥管理
- 定位密钥接受入口:查询字符串参数(
api_key、access_token、token)与标头(Authorization、X-API-Key)。标记通过 URL 传输的情况:URL 会留在代理日志、浏览器历史记录和引用来源标头中。 - 搜索代码库及任何移动端/SDK 目录,查找 SDK 初始化位置嵌入的静态密钥;根据提供商前缀模式分类,并判断这些密钥是否会随客户端一同分发。
- 评估授权范围:一个密钥是否可以访问所有端点(万能密钥)?检查是否根据密钥记录进行范围/权限检查,以及是否执行按密钥配额限制。
- 评估生命周期:密钥状态列、过期时间戳、轮换端点、撤销流程、签发审计记录。完全没有撤销路径本身就是一项发现。
- 公开且无需身份验证的 API:宽松的匿名默认限额且无需注册,会增加滥用风险;记录默认值。
- 生命周期跟踪深度:密钥记录中的创建时间戳和最后使用时间跟踪有助于发现休眠密钥;如果密钥没有使用情况遥测,就无法清理过期凭据。
WebSocket / SSE
- 升级路径:定位服务创建位置(ws、socket.io、SignalR、gorilla/websocket、ActionCable、Phoenix Channels、Django Channels)。确认升级期间强制执行 Origin/Host 校验(ws 中的
verifyClient、socket.io 的cors选项、SignalR 的AllowedOrigins、CheckOrigin、OriginValidator)。 - 握手后认证:身份是否只在连接时建立?每条消息是否都会对消息类型/操作进行授权?查找使用仅从握手获取的身份以及客户端提供的 ID 来修改数据的
socket.on('action', ...)处理程序。 - 订阅主题/房间:检查加入房间/频道时是否会验证用户 ACL,还是接受客户端提供的任意主题名称(
socket.join(req.body.room))。 - SSE:定位
text/event-stream端点;确认流请求本身经过认证,并检查Last-Event-ID的重放语义(重放不得泄露其他租户的事件)。 - 跨服务信任:套接字通常会完全绕过 HTTP 中间件链;确认套接字处理程序会重新执行授权,而不是假定“连接是可信的”。
查找位置
框架位置图
| 技术栈 | 绑定/序列化文件 | 路由/中间件文件 | 备注 |
|---|---|---|---|
| Express/Fastify | 与路由放在一起的处理程序 | app.js、server.ts、routes/*.js、Fastify 的 schema: 键 | Fastify JSON-schema 的 additionalProperties:false 会改变绑定方式;检查每个路由的 schema |
| NestJS | *.dto.ts、entities/*.entity.ts | main.ts(全局管道/守卫)、*.controller.ts、app.module.ts | ValidationPipe 的全局配置与控制器级配置;守卫顺序 |
| Django REST Framework | serializers.py、models.py | urls.py、views.py、settings.py 中的 REST_FRAMEWORK 配置块 | 默认限流类位于 settings 中 |
| FastAPI/Flask-RESTful | Pydantic schema:schemas.py | main.py、api/*.py、Flask app = Flask(...) 装饰器链 | Pydantic 的 extra 配置决定如何处理未知字段 |
| Spring MVC/WebFlux | 实体类、投影 DTO、@ControllerAdvice | @SpringBootApplication 主类、WebSecurityConfigurerAdapter/SecurityFilterChain bean、控制器 | springdoc.* 属性控制文档暴露 |
| ASP.NET Core Web API | Models/、DTOs/ | Startup.cs/Program.cs、Controllers/*.cs | 管道中的中间件顺序;Swashbuckle 的 UseSwaggerUI 环境控制 |
| Laravel/Lumen | app/Http/Requests/*、app/Models/*($fillable) | routes/api.php、app/Http/Kernel.php($middleware、$middlewareGroups) | 检查每个路由组是否存在 throttle: 中间件别名 |
| Rails API 模式 | app/models/*、app/serializers/* | config/routes.rb、config/application.rb、Rack::Attack 初始化器 | API 模式下 ActionController::Base.allow_forgery_protection 不相关;控制器中使用强参数 |
| Go chi/gin/echo | 绑定结构体的处理程序文件 | main.go/cmd/*/main.go 中的路由器设置、中间件注册 | 没有自动绑定机制;风险在于将数据解码到共享领域结构体中 |
| GraphQL(apollo/yoga/strawberry/graphene) | typeDefs、resolvers.*、schema 文件 | 服务启动文件(apollo-server 构造函数、yoga 配置、strawberry extensions=[...]) | 仅在此处配置验证规则 |
| gRPC | *.proto,忽略生成的存根 | 服务端设置(grpc.NewServer(...)、Python grpc.server(...)、Java ServerBuilder) | 在此处注册拦截器和反射 |
影响 API 安全态势的配置文件
settings.py:REST_FRAMEWORK字典(限流、渲染器、解析器)、DEBUG、中间件列表。.env、config/*.yaml、application.properties/yml、appsettings.json:内省标志、swagger 启用状态、限流数值、信任代理设置。Dockerfile、docker-compose.yml、k8s/:对外发布的端口(包括明文 gRPC 端口)。- 反向代理(
nginx.conf、Envoy 配置):可能在标记代码问题前,已由平台层限流满足限流要求。
搜索顺序
从路由表入手,枚举完整的端点清单;然后检查数据写入入口,确认绑定策略;接着检查引导程序/配置文件中的全局标志(管道、限流器、内省);最后逐个检查端点,查找缺失的控制措施。按此顺序检查,可避免将某个受保护的路由组误认为整个服务都受到保护。
模式与特征
所有正则表达式均由 ripgrep(rg)和 Rust 正则表达式库执行:不支持后行断言和反向引用。请从仓库根目录运行;报告前手动排查命中结果。
批量赋值特征
fields\s*=\s*["']__all__["']
DRF 序列化器暴露模型的所有列,包括 is_admin、password_hash。
\$request->all\(\)
Laravel/Lumen 原始请求数组;检查每个命中结果是否流入 create()/update()/fill()。
->(create|update|fill|firstOrCreate|updateOrCreate)\(\$request->all\(\)\)
直接的 Eloquent 过度赋值接收点。
\.new\(params\[|\.create\(params\[|\.update\(params\[
Rails 模型由未经筛选的 params 构建;确认同一操作上游存在 .permit(...)。
(Object\.assign|merge|extend)\(\s*[A-Za-z_$][\w$.]*\s*,\s*req\.body
Express/Fastify 将任意键合并到持久化对象上。
\.\.\.req\.body
将客户端请求体展开到更新负载或构造函数选项中。
\.(create|findByIdAndUpdate|findOneAndUpdate|updateOne|bulkWrite)\(\s*req\.body\b
Mongoose 写入接收点使用整个请求体。
ValidationPipe\(\)
NestJS 管道使用默认选项:未知属性会原样通过。
@ModelAttribute|setAllowedFields|ignoreUnknownFields
Spring 数据绑定入口及其(少见的)安全阀;标记绑定到实体类的 @ModelAttribute。
FromBody\]\s+[A-Za-z_][A-Za-z0-9_]*\s+\w+
ASP.NET Core 请求体绑定参数;将绑定类型名称与 DbSet<...> 声明进行比对,以查找实体绑定。
extra\s*[=:]\s*["'](allow|ignore)["']?
Pydantic 策略:allow 会接受并存储额外字段;普通的 ignore 会丢弃这些字段(对写入操作而言安全)。
setattr\s*\(\s*[a-z_]+\s*,\s*(k|key|field|attr|name)\s*,|merge!\(|SetValues\(|\.update\(payload\)|\.update\(data\)
跨 Python/Ruby/.NET/JS 的动态键 PATCH 合并接收点。
速率限制与分页特征
先盘点是否存在限流器,再与端点清单进行比对:
(?i)(rate.?limit|throttle|rack.?attack|slowapi|flask_limiter|django_ratelimit|addratelimiter|ratelimiting|express-rate-limit|nestjs/throttler)
TooManyRequests|StatusTooManyRequests|HttpStatusCode\.TooManyRequests
无界集合接收点:
\.all\(\)|\.find\(\s*\{\s*\}\s*\)|findAll\(\)|FindAll\(\)|SELECT\s+\*\s+FROM
列表路由应包含的分页参数(集合路由缺少这些参数即为发现的问题):
[?&](page|per_page|pageSize|limit|offset|cursor|after|before)=
针对按 IP 限流器的代理信任陷阱:
trust proxy|trustProxy|X-Forwarded-For
GraphQL 特征
introspection\s*:|GRAPHQL_INTROSPECTION|disableIntrospection|DisableIntrospection|introspectionEnabled
graphiql|playground|landingPage|LandingPage|GraphiQL
深度/复杂度控制(存在控制是好事;GraphQL 引导程序附近缺少控制即为发现的问题):
depthLimit|max_depth|maxDepth|QueryDepthLimiter|complexity|ComplexityLimit|costAnalysis|validationRules
批处理/持久化查询机制:
PersistedQuery|persistedQueries|APQ|shouldPersistedQuery|batching
gRPC 特征
reflection\.Register|enable_server_reflection|ProtoReflectionService|RegisterReflectionService
明文监听器:
insecure\.NewCredentials|InsecureServerCredentials|WithTransportCredentials\(insecure|h2c
身份验证拦截器(服务器构造附近缺少此项即为发现的问题):
UnaryInterceptor|StreamInterceptor|ServerInterceptors|ServerInterceptor|AddUnaryInterceptors|AddStreamInterceptors
消息大小策略:
MaxRecvMsgSize|MaxReceiveMessageSize|max_receive_message_length
REST 规范特征
方法覆盖机制:
_method|methodOverride|MethodOverride|X-HTTP-Method-Override|HttpMethodOverride
GET 请求中的状态变更(Express 风格;请手动对 Rails/Django/Flask 路由表重复扫描):
\.(get|GET)\(\s*["'][^"']*(delete|remove|destroy|update|edit|create|reset|send|confirm|approve)
版本泛滥:
/api/v[0-9]|["']/v[0-9]+/
文档暴露:
swagger-ui|swagger\.json|openapi\.json|api-docs|UseSwaggerUI|drf_yasg|drf-spectacular|flask_swagger_ui|springdoc
调试/内部标头的发送:
X-Powered-By|x-aspnet-version|x-debug-token|_profiler|exposeHeaders|Access-Control-Expose-Headers
CORS 通配符与来源反射:
Access-Control-Allow-Origin["']?\s*[,:]\s*["']?\*
origin\s*:\s*true|callback\(null,\s*req\.headers\.origin|SetIsOriginAllowed\(|AllowCredentials\(\)|credentials\s*:\s*true
Content-Type 混淆入口点(构造负载 -> DESER 模块):
xml-bodyparser|lxml\.etree|etree\.fromstring|fromstring\(|JAXBContext|JacksonXml|XmlSerializer|application/xml
错误响应体泄露:
err\.stack|error\.stack|traceback\.format_exc|printStackTrace|DEBUG\s*=\s*True|includeErrorDetails|DetailedErrors|includeStacktrace
API 密钥特征
从 URL/查询参数中接受密钥(会暴露在日志中的传输方式):
request\.args\.get\(["'](api_key|apikey|token)["']\)|query\.Get\("api_key"\)|params\[:(api_key|token)\]|Request\.Query\[["']api_key
硬编码凭据格式(知名公开前缀):
AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_\-]{35}|sk_live_[0-9a-zA-Z]{16,}|ghp_[0-9A-Za-z]{36}|xox[baprs]-[0-9A-Za-z\-]{10,}
范围限制执行标记(存在此项是好事):
scopes?\s*[=:]|hasScope|scope_required|check_scopes|permissions\.includes
WebSocket / SSE 特征
升级处理与来源验证:
WebSocketServer|io\.on\(["']connection|handleUpgrade|verifyClient|CheckOrigin|AllowedOrigins|OriginValidator|allowed_origins|origins
需要逐条消息进行授权检查的操作处理器:
socket\.on\(|conn\.on\(|OnConnected|receive\(|async_receive
需要进行 ACL 检查的房间/主题加入操作:
socket\.join\(|join_room|groups\.add|AddToGroupAsync|channels\.subscribe
SSE 接口:
text/event-stream|EventSource|Last-Event-ID
载荷速查表
批量赋值请求体(针对注册、个人资料、订单端点发送 POST/PUT/PATCH 请求):
{"email":"[email protected]","password":"Str0ng!passphrase","is_admin":true}
{"username":"a","price":0,"user_id":2,"role":"admin"}
{"nickname":"x","tenant_id":1,"balance":999999,"email_verified":true}
PATCH 合并探测(将任意键写入部分更新):
{"role":"admin","permissions":["*"],"status":"active"}
GraphQL 探测:
- 简短内省形式:
{"query":"{__schema{types{name}}}"}— 包含"types"和类型名称列表的 JSON 请求体可确认已启用内省。 - 存在性探测:
{"query":"{__typename}"}— 用于检查 GraphQL 端点是否存活的低成本探测。 - 字段建议探测:
{"query":"{users{usrnme}}"}— 错误中包含 "Did you mean" 表示泄露了架构名称。 - 深度炸弹结构(仅描述,不要发送超大载荷):查询的选择集嵌套 20–100 层,每层都选择另一个对象的连接字段,例如
user -> friends -> friends -> friends ...;还可以在每层添加重复别名以放大影响。只有静态确认不存在深度/复杂度限制后,才考虑进行动态测试。 - 批处理探测:
[{"query":"{__typename}"},{"query":"{__typename}"}]— 一个响应中包含两个答案,表示已启用操作批处理。
缺少分页的探测:
?page=999999&limit=all
?limit=1000000
?offset=0&count=-1
?perPage=99999999
一次性转储端点会为这些请求返回完整数据(而非 400/限制后的结果)。
_method 覆盖示例:
POST /users/1/delete route registered for GET only
form body: _method=DELETE&_token=<csrf> Rails/Laravel form override
header: X-HTTP-Method-Override: PUT sent against POST /api/items/1
query: /api/items/1?_method=DELETE query-string variant
WebSocket 跨源握手探测:
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Origin: https://evil.example" https://host/ws
对来自外部 Origin 的请求,若响应中包含 HTTP/1.1 101 Switching Protocols,则表示缺少升级来源验证。
污点追踪指南
将每个由客户端提供的键值对都视为在传输边界处带有污点,并追踪其流向持久化接收点。批量赋值关注的污点是键名,而不仅是值:攻击者可以控制要写入哪些属性,而不只是属性包含什么内容。
来源/接收点表
| 语言/技术栈 | 来源(由客户端控制) | 接收点(持久化) |
|---|---|---|
| Node/Express/Fastify | req.body、req.query、req.params、Fastify 解析后的请求体 | Model.create()、findByIdAndUpdate()、Object.assign(entity, ...)、Knex update(payload)、Prisma create(data) |
| NestJS | 使用 @Body() 装饰的 DTO 参数、守卫中的原始 request.body | 仓库 .save(partialEntity)、TypeORM preload()/merge() |
| Python DRF | request.data、序列化器输入 | serializer.save()、Model.objects.create(**validated_data) |
| Python Flask/FastAPI | request.get_json()、Pydantic 模型字段(model_dump()) | db.session.add(obj)、SQLAlchemy update(values) |
| Ruby Rails | params 哈希 | Model.new(params)、update(params)、assign_attributes、merge! |
| PHP Laravel | $request->all()、$request->input() | ::create($array)、->fill($array)、->forceFill() |
| Java Spring | 绑定到已注解参数的表单/查询参数、@RequestBody DTO | JPA 仓库 .save(entity)、setData(...) |
| C# ASP.NET | [FromBody] 模型、TryUpdateModelAsync | EF Core _context.Add()、CurrentValues.SetValues() |
| Go | json.NewDecoder(r.Body).Decode(&v)、gin c.ShouldBindJSON(&v) | GORM .Create(v)、.Updates(map)、根据结构体字段构建的 sqlx 插入操作 |
| GraphQL | 解析器 (root, args, ctx)、嵌套输入对象 | 与宿主语言相同的 ORM 接收点 |
流程
- 以来源为起点:列出「查找位置」中的所有请求体数据入口。
- 向前追踪:绑定的对象是否在没有经过字段缩减步骤的情况下到达任何接收点表格行?
- 会中断数据流的字段缩减步骤:显式允许列表(
permit、fields = [...]、whitelist:true、$fillable、投影 DTO、pick()辅助函数)。任何其他转换(验证、默认值、映射库)都不会中断数据流,除非它会创建一个带有命名字段的新对象。 - 在接收点,将可写属性集与敏感候选列表(「检查内容」中的 role/is_admin/price/user_id 等)进行差异比对。
- 对于 GraphQL,还要逐个解析器追踪:每个带有类似
id参数的解析器都有自己的来源到接收点路径;检查解析器或其数据加载器中的所有权谓词。 - 对于 gRPC,来源是 proto 消息字段;追踪消息字段直接填充实体字段的处理程序代码路径,尤其是应用部分掩码的更新 RPC。
中断数据流的结构参考
// VULNERABLE - spread preserves attacker keys
const user = await User.create({ ...req.body });
// FIXED - explicit pick
const { email, password, nickname } = req.body;
const user = await User.create({ email, password, nickname });
# VULNERABLE - dynamic kwargs
User.objects.create(**request.data)
# FIXED - named fields
User.objects.create(email=request.data["email"], nickname=request.data.get("nickname", ""))
利用与复现
先通过静态分析确认;只有在明确获准测试的环境中,才运行动态步骤。数量刻意保持适中。
批量赋值导致权限提升
- 找到注册或个人资料更新端点,以及它绑定的实体(静态步骤)。
- 发送正常注册请求,并添加一个敏感字段:
curl -s -X POST https://host/api/register -H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"Str0ng!passphrase","is_admin":true}'
- 以该用户身份进行身份验证,并请求仅限管理员访问的端点(例如
GET /api/admin/users)。 - 真阳性的预期可观察结果:敏感字段被静默接受(没有指出未知属性的 400 错误),且管理员端点返回 200,而不是 401/403。如果服务器以 400/422 拒绝未知字段,则说明控制措施有效;请记录这一点。
- 无需声称已获得权限即可验证:比较携带
"role":"admin"的 PATCH 前后GET /api/me的结果,或者只需确认未知键被接受并且代码中存在接收点,即足以报告。
PATCH 任意键合并
curl -s -X PATCH https://host/api/users/me -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" -d '{"role":"admin","tenant_id":1}'
预期可观察结果:返回 200 OK,且响应正文回显注入的字段。
缺少限流
- 选择身份验证端点(登录端点最理想)。
for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://host/api/login \
-H 'Content-Type: application/json' -d '{"email":"[email protected]","password":"wrong"}'
done | sort | uniq -c
- 缺少限流的预期可观察结果:连续二十次收到
200/401响应,且没有429/403限流响应。只要出现一次429,就说明存在限流器;接下来检查其键和作用范围。
缺少分页/一次性转储
curl -s "https://host/api/users?page=999999&limit=all" | head -c 400
curl -s "https://host/api/users" | wc -c
预期可观察结果:返回完整数据负载(不是 400,也未限制为最大页大小),且响应大小随表中内容增加。
GraphQL 内省
curl -s https://host/graphql -H 'Content-Type: application/json' \
-d '{"query":"{__schema{types{name}}}"}'
预期可观察结果:JSON 中包含一个 types 数组,列出架构类型名称(Query、Mutation、自定义类型)。类似 "GraphQL introspection is disabled" 的错误属于否定情况;请记录该控制措施。
暴露 GraphQL Playground / GraphiQL
curl -s https://host/graphql -H 'Accept: text/html' | grep -io 'graphiql\|playground'
预期可观察结果:HTML 着陆页标记(GraphiQL、Playground、ApolloServerPluginLandingPage 输出)。未暴露时(返回 JSON 错误而不是 HTML)属于否定情况。
字段建议泄露
curl -s https://host/graphql -H 'Content-Type: application/json' \
-d '{"query":"{users{usrnme}}"}'
预期可观察结果:errors[0].message 包含 "Did you mean ...?" 和实际字段名称。
gRPC 反射
grpcurl -plaintext host:50051 list
预期可观察结果:无需凭据即可转储服务列表。连接被拒绝/要求 TLS 表示明文连接安全态势问题,应单独记录。
方法覆盖导致的路由遮蔽
curl -s -X POST "https://host/api/items/1" -H 'Content-Type: application/x-www-form-urlencoded' \
--data '_method=DELETE' -b "session=$SESSION"
预期可观察结果:通过仅支持 POST 的路由删除条目。静态确认已挂载哪些覆盖中间件,以及覆盖动词是否设有允许列表并经过 CSRF 检查。
暴露 OpenAPI/Swagger
按顺序探测:GET /swagger-ui.html、/swagger/index.html、/swagger/v1/swagger.json、/api-docs、/openapi.json、/docs、/graphql(HTML Accept)。预期可观察结果:无需身份验证即可返回交互式文档 UI 的 HTML 或原始规范 JSON。交叉检查规范内容中的内部主机/端点。
WebSocket 跨源升级
使用 Payload Cheat Sheet 中的握手探测,并设置 Origin: https://evil.example。缺少保护时的预期可观测结果:101 Switching Protocols。然后,如果已获授权,尝试在未通过身份验证的情况下发送操作消息;若成功,即可证明缺少逐消息授权检查。
确认 URL 中的 API 密钥
静态检查:查找是否接受查询字符串中的密钥。动态佐证:通过 ?api_key=... 调用端点,然后检查访问日志配置(nginx 的 $request_uri 日志记录默认开启)——密钥会出现在日志记录的任何位置。
修复措施
序列化器 / DTO 字段白名单
| 框架 | 风险标记 | 正确做法 |
|---|---|---|
| Django REST Framework | fields = "__all__" | 显式设置 fields = ("email", "nickname");将计算列标记为 read_only=True |
| NestJS | 裸用 ValidationPipe() | new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }) |
| Spring MVC | 将 @ModelAttribute 绑定到实体 | 针对每种用例使用投影 DTO;通过 @InitBinder 设置 setAllowedFields;保持 Jackson 的未知属性失败设置开启(spring.jackson.deserialization.fail-on-unknown-properties=true) |
| Rails API | Model.new(params) | params.require(:user).permit(:email, :nickname) |
| Laravel | 将 $request->all() 传入 Eloquent | 在模型上严格设置 $fillable;仅使用 FormRequest 的 validated();绝不从客户端输入调用 forceFill |
| FastAPI | Pydantic 配置决策 | 写入操作:为请求模型设置 model_config = ConfigDict(extra="forbid");读取操作可以保持 ignore |
| ASP.NET Core | [FromBody] Entity | 使用单独的 DTO + 显式 AutoMapper 配置;避免从未经筛选的 DTO 调用 SetValues |
| Express/Fastify | 展开 req.body | 解构指定字段,或使用 Fastify 路由架构设置 additionalProperties: false,让 ajv 删除额外属性 |
| Go | 解码到领域结构体 | 使用仅包含客户端可写字段的专用请求结构体 |
// VULNERABLE
app.useGlobalPipes(new ValidationPipe());
// FIXED
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
# VULNERABLE (DRF)
class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = "__all__"
# FIXED
class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = ("email", "nickname")
read_only_fields = ("is_admin", "role", "user_id")
# FIXED (Rails)
def user_params
params.require(:user).permit(:email, :nickname)
end
// FIXED (Laravel)
class User extends Model {
protected $fillable = ['email', 'nickname'];
}
// controller consumes only validated data
$user = User::create($request->validated());
// FIXED (ASP.NET Core) - bind a DTO, not the entity
public record UpdateUserDto(string Nickname); // no Role, no TenantId
Pydantic extra 的选择建议:端点允许客户端写入时,选择 forbid(将过度提交转为 422);对于仅供服务端内部使用的负载,可接受 ignore;绝不使用 allow。
网关层选项:如果服务前方有 API 网关,可在网关层进行 JSON Schema 请求验证(按路由配置请求体架构,以删除或拒绝未知属性),作为代码层字段白名单逐步推出期间可接受的补偿措施。
速率限制与分页上限
在平台层和代码中都应用限流器(纵深防御)。适用范围较广的令牌桶默认值:身份验证端点按用户名 + IP 限制为每分钟 5 次请求;常规 API 按用户或 API 密钥限制为每分钟 60-120 次请求,并允许短时突发;导出 / 搜索限制为每分钟 10 次请求。优先按用户 ID / API 密钥标记,其次按 IP 标记。
// Express - express-rate-limit
const { rateLimit } = require("express-rate-limit");
const loginLimiter = rateLimit({ windowMs: 60_000, max: 5, standardHeaders: true });
app.use("/api/login", loginLimiter);
// NestJS - @nestjs/throttler
@Module({ imports: [ThrottlerModule.forRoot([{ ttl: 60_000, limit: 100 }])] })
// plus @Throttle({ default: { limit: 5, ttl: 60_000 } }) on auth controllers
# Flask - slowapi / DRF settings
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": ["rest_framework.throttling.ScopedRateThrottle"],
"DEFAULT_THROTTLE_RATES": {"login": "5/min", "search": "30/min", "export": "10/hour"},
}
# Rails - Rack::Attack
Rack::Attack.throttle("login/ip", limit: 5, period: 60) { |req| req.ip if req.path == "/api/login" }
// ASP.NET Core (.NET 7+) built-in rate limiting
builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter("auth", w => {
w.Window = TimeSpan.FromMinutes(1); w.PermitLimit = 5;
}));
无论客户端提示如何,均设置服务端分页上限:
// FIXED - clamp before querying
const limit = Math.min(parseInt(req.query.limit ?? "20", 10), 100);
const page = Math.max(parseInt(req.query.page ?? "1", 10), 1);
在注册、投票和兑换流程中添加验证码 / 工作量证明或同等的防护措施;在网关或由套餐等级表支持的中间件中强制执行每个 API 密钥的配额。
GraphQL 加固
- Apollo Server:在生产环境中设置
introspection: false;省略落地页插件(ApolloServerPluginLandingPageGraphQLPlayground),或采用类似ApolloServerPluginUsageReporting的生产环境门控;通过formatError/错误脱敏禁用字段建议。 - 深度/复杂度:在
validationRules: [depthLimit(10)]中使用graphql-depth-limit;进行成本分析(graphql-cost-analysis)或使用 ApolloestimateComplexity;在库支持的情况下限制别名数量。 - graphql-yoga:通过
plugins: [useDepthLimit({ maxDepth: 10 })]使用深度限制插件;默认启用maskedErrors,请保持启用。 - Strawberry:在 schema 的扩展列表中使用
strawberry.extensions.QueryDepthLimiter(max_depth=10)。 - Graphene:添加在执行前实施深度或成本限制的验证规则/中间件。
- 持久化查询:针对操作注册表允许列表运行 AutomaticPersistedQueries;拒绝来自公开客户端的未注册哈希。
- 批处理:如果接受操作数组,请限制数组长度和总复杂度,或对公开客户端禁用批处理。
- Resolver 授权:在 resolver/数据加载器中执行对象级检查(参见 AUTHZ);绝不要依赖“该 schema 是内部的”这一说法。
// VULNERABLE
const server = new ApolloServer({ typeDefs, resolvers });
// FIXED
const server = new ApolloServer({
typeDefs, resolvers,
introspection: false,
validationRules: [depthLimit(10)],
});
gRPC 加固
- 在生产构建中禁用反射(通过环境变量/构建标签控制);反射会向攻击者暴露完整的描述符集合。
- 在服务或负载均衡器处终止 TLS;绝不要向公众开放明文 h2c 端口。优先在服务本身配置 TLS 凭据:
// VULNERABLE
lis, _ := net.Listen("tcp", ":50051")
s := grpc.NewServer() // plaintext
// FIXED
creds, _ := credentials.NewServerTLSFromFile("cert.pem", "key.pem")
s := grpc.NewServer(grpc.Creds(creds))
- 使用拦截器强制执行涵盖所有已注册服务的逐 RPC 身份验证;豁免项应明确且范围狭窄(仅限健康检查):
// FIXED - unary interceptor validating metadata
func AuthInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, _ := metadata.FromIncomingContext(ctx)
if len(md.Get("authorization")) == 0 { return nil, status.Error(codes.Unauthenticated, "missing token") }
return handler(ctx, req)
}
grpc.NewServer(grpc.UnaryInterceptor(AuthInterceptor), grpc.ChainStreamInterceptor(StreamAuth))
Python:使用 interceptors=[...] 构建(例如 grpc-interceptor)。Java:对每个服务使用 ServerBuilder.addService(...),并配合 ServerInterceptors.intercept(service, authInterceptor)。
- 设置合理的消息大小上限(
MaxRecvMsgSize、Java 的maxInboundMessageSize),验证枚举值,避免将google.protobuf.Any用于安全相关的分发,并在会将存储的消息回传的持久化路径中剔除未知字段。
OpenAPI/文档暴露策略
将 API 文档置于身份验证之后提供,或限制为仅内部网络可访问;拆分外部和内部规范,确保内部端点不会出现在公开文件中。框架配置:在生产环境中设置 springdoc.api-docs.enabled=false(或通过 profile 控制);将 DRF drf-spectacular/drf-yasg 视图包装在 IsAdminUser 权限类中;将 Swashbuckle UseSwaggerUI() 放在 if (app.Environment.IsDevelopment()) 中;在身份验证中间件之后挂载 swagger-ui-express。移除 X-Powered-By/框架标识(app.disable("x-powered-by")、移除 Program.cs 中的标头),并确保 Symfony profiler 不会在生产环境中暴露。
WebSocket/SSE 加固
在升级期间根据允许列表验证 Origin;对操作按消息进行身份验证;按用户授权加入主题/房间;每次发送事件时重新检查授权。
// VULNERABLE (ws)
const wss = new WebSocket.Server({ port: 8080 }); // no origin check
// FIXED
const wss = new WebSocket.Server({
port: 8080,
verifyClient: (info) => allowlist.includes(new URL(info.origin).host),
});
wss.on("connection", (ws) => {
ws.on("message", (raw) => {
const msg = JSON.parse(raw);
if (!canAct(ws.user, msg.action, msg.targetId)) return; // per-message authz
});
});
// socket.io - origin allowlist + handshake token
new Server({ cors: { origin: ["https://app.example.com"], credentials: true } })
.use((socket, next) => next(jwt.verify(socket.handshake.auth.token) ? undefined : new Error("unauthorized")));
SignalR:AllowedOrigins("https://app.example.com")。Django Channels:在 ASGI 应用外层使用 OriginValidator,并在每个 consumer 中进行 scope 级权限检查。SSE:对流请求进行身份验证,并验证 Last-Event-ID 重放内容仍属于请求者自己的租户。
API 密钥管理
仅通过标头传递密钥(X-API-Key、Authorization: Bearer);拒绝查询字符串中的密钥参数。为每个密钥限定权限集,并在请求时强制执行;为每个环境和每种集成分别签发密钥;实现撤销功能(每次请求都检查状态标记)和轮换端点;仅存储密钥哈希,绝不存储明文。移动端/SDK 构建仅包含公开客户端标识符——任何可用于保密的内容都应放在后端前端(backend-for-frontend)之后。
验证与校验
GIVEN/WHEN/THEN 验收场景
- GIVEN 序列化器允许列表,WHEN 客户端 POST
{"email":"[email protected]","password":"x","is_admin":true},THEN 服务端响应 400/422 并指出被拒绝的属性(禁止模式),或保存对象时剥离额外字段(剥离模式);后续读取结果中不应包含is_admin。 - GIVEN 合法客户端,WHEN 它们仅发送文档中说明的字段,THEN 请求应与修复前一样成功(反向测试:不得过度拦截)。
- GIVEN 速率限制器,WHEN 短时间内收到 20 次登录尝试,THEN 从约第 6 次尝试开始返回 429,而普通用户间隔发送的请求永远不会收到 429。
- GIVEN 分页上限,WHEN 收到
?limit=1000000,THEN 响应包含的条目不超过限制后的页面大小,并报告该上限。 - GIVEN 在生产环境中禁用内省,WHEN 发送
{"query":"{__schema{types{name}}}"},THEN 端点返回错误,且不泄露任何类型名称;WHEN 已注册客户端发送持久化查询哈希,THEN 查询仍可成功执行。 - GIVEN 已配置深度限制,WHEN 执行超过限制深度的嵌套选择,THEN 在运行任何解析器之前,验证错误会指出深度规则。
- GIVEN 已安装 gRPC 身份验证拦截器,WHEN 未携带授权元数据调用 RPC,THEN 所有服务(包括流式服务)的调用均以
Unauthenticated失败。 - GIVEN 文档访问控制,WHEN 未经身份验证的客户端获取
/openapi.json或/swagger-ui.html,THEN 客户端收到 401/403/重定向。 - GIVEN WebSocket 来源验证,WHEN 握手中提供
Origin: https://evil.example,THEN 服务端拒绝升级(不返回 101)。 - GIVEN 逐消息授权,WHEN 已连接的套接字尝试对其他用户的资源 ID 执行动作,THEN 该动作被拒绝。
回归契约测试伪代码
# CI contract test: unknown fields must never reach persistence
def test_register_rejects_unknown_fields(api_client):
payload = {"email": "[email protected]", "password": "Str0ng!passphrase", "is_admin": True}
r = api_client.post("/api/register", json=payload)
assert r.status_code in (400, 422) or "is_admin" not in r.json()
login = api_client.post("/api/login", json={"email": "[email protected]", "password": "Str0ng!passphrase"})
me = api_client.get("/api/me", headers=auth(login))
assert me.json().get("is_admin") is not True
def test_login_throttled():
codes = [post_login(wrong_password=True).status_code for _ in range(20)]
assert 429 in codes
def test_list_endpoint_capped():
body = get_json("/api/users?limit=1000000")
assert len(body["items"]) <= 100
手动检查清单
- 每个路由组都应用了身份验证和流量限制中间件(对比各组的中间件列表)。
- 每个写入端点都使用字段列表明确的 DTO/序列化器绑定请求数据。
- 每个列表端点都在服务端限制页面大小。
- GraphQL 引导配置会关闭内省和着陆页,启用深度/复杂度限制,并隐藏建议。
- gRPC 服务端不会在生产路径中的任何位置注册反射;服务端使用 TLS 或位于 TLS 终止层之后,并为每个服务链式应用身份验证拦截器。
- 不存在更改状态的 GET 路由;重写中间件受到限制且符合 CSRF 安全要求。
- 文档端点受访问控制;静态资源/公共存储桶中不存在规范文件。
- 密钥仅通过请求头接收;请求时会检查作用域;存在撤销机制。
- WebSocket 升级会验证 Origin;消息处理器会授权操作;加入房间时会检查访问控制列表。
修复后重新运行的 Grep 命令
rg -n 'fields\s*=\s*["'"'"']__all__["'"'"']' --type py
rg -n '\$request->all\(\)'
rg -n 'ValidationPipe\(\)'
rg -n 'introspection\s*:\s*true|GRAPHQL_INTROSPECTION'
rg -n 'reflection\.Register|enable_server_reflection|ProtoReflectionService'
rg -n 'insecure\.NewCredentials|WithTransportCredentials\(insecure|h2c'
rg -n '_method|X-HTTP-Method-Override'
rg -n '[\?&]api[_-]?key='
预期的修复后状态:每个查询仅返回经过审核且接受的匹配项(由开发环境门控、加入允许列表或记录为例外)。
严重程度评估
评分标准
| 发现 | 基准严重性 | CVSS v3.1 向量 | CWE |
|---|---|---|---|
批量赋值写入 is_admin/role(权限提升) | 严重 | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H | CWE-915 |
对非安全字段(price、user_id)进行批量赋值 | 高 | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L | CWE-915 |
| 单独缺少限流(未证明可绕过锁定) | 低至中,视具体情况而定 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L | CWE-770, CWE-799 |
| 缺少分页/一次性导出敏感表 | 根据数据类别评为中至高 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N | CWE-770 |
| 暴露内省、游乐场和文档 | 低至中,信息泄露 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N | CWE-200 |
| 字段建议泄露/堆栈跟踪错误响应 | 低至中 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N | CWE-200 |
| API 启用带凭据的 CORS 通配符 | 存在会话凭据时为高 | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N | CWE-942 |
| 在 URL 中传递 API 密钥 | 高 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N | CWE-798 |
| 共享的内嵌 SDK 密钥可授予账户级访问权限 | 高 | 根据随附二进制文件的可达性评估 | CWE-798 |
| WebSocket 劫持(未检查来源、未进行逐消息授权) | 高 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L | CWE-346, CWE-862 |
| gRPC 使用明文,且业务 RPC 缺少逐 RPC 授权 | 可从外部访问时为高 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N | CWE-306 |
| 已弃用的 API 版本仍在线,且未应用当前修复 | 中,资产清单风险 | CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N | CWE-200 |
调整因素
当存在漏洞的攻击面可从互联网访问且无需身份验证时,将严重性提高一个等级。如果利用漏洞需要处于内部网络(gRPC 端口)、需要具备已拥有等效权限的低权限身份验证上下文,或已有补偿性平台控制措施(网关限流、WAF)能够确实弥补这一缺口,则将严重性降低一个等级——无论哪种情况,都应在发现项中引用控制文件。限流问题维持为低至中,除非与凭据填充、枚举或金融交易流程结合;这些情况会使严重性升至高。
常见误报
- DRF 中仅供管理员使用或只读的视图集设置
fields = "__all__":标记前先检查路由的权限类;使用IsAdminUser的内部专用序列化器可以接受。 - NestJS DTO 未启用
whitelist,但处理程序在持久化前会解构出具名字段:绑定较宽松,但数据接收点是安全的;应追踪数据接收点,而不只是管道。 - 网关处理限流:nginx
limit_req、Envoy 限流过滤器、Cloudflare 规则都能满足这一控制要求;检查基础设施配置,并注明补偿性控制,而不是报告缺少该控制。 - 在确实公开只读的 API 上有意公开内省:标记前确认不存在变更操作面或敏感类型;将其作为信息项报告。
- Go/JSON 严格性:Go 解码器会静默丢弃未知的 JSON 键,因此即使绑定整个请求体,解码到只包含安全导出字段的结构体也不属于批量赋值。只有当结构体导出敏感字段(
IsAdmin、Role)时才标记。 - Pydantic 默认的
extra=ignore:未知字段会被丢弃,因此 FastAPI 端点很少会出现过度提交问题,除非设置了extra="allow",或原始字典流向数据接收点。 - Rails/Laravel 中的
_method/覆盖中间件:内置实现会将覆盖限制为安全动词,并集成 CSRF 保护;应标记自定义或不受限制的覆盖处理,而不是框架功能本身。 - 不携带凭据的通配符 CORS:浏览器会拒绝通配符与凭据组合;真正的问题是启用凭据时反射来源(矩阵参见 CONFIG)。
- 即使没有查询参数,数据库层也可能通过游标默认值或 ORM
LIMIT默认值实现分页:应查看实际的查询构造。 - gRPC 反射通过构建标签/环境门控启用,且能证明生产构建会排除该功能:引用该门控作为控制措施的证据。
- 版本繁杂,但
/v1路由通过相同的处理程序集和当前中间件代理:处理程序共享意味着不存在补丁缺口;仅报告代码路径有差异的情况。 - 错误堆栈跟踪仅在显式调试标志启用时可见,且生产配置中默认关闭:验证已部署的配置值。
参考资料
OWASP API Security Top 10(2023)映射
项目主页:https://owasp.org/www-project-api-security/
| OWASP 项目 | 本模块的覆盖范围 |
|---|---|
| API1:2023 对象级授权失效 | GraphQL 解析器的对象级缺口;WebSocket 主题/操作;启用了 IDOR 的数据接收点(主要由 AUTHZ 模块覆盖) |
| API2:2023 身份验证失效 | 身份验证端点的平台限流;密钥传输/作用域(锁定机制细节见 AUTHN 模块) |
| API3:2023 对象属性级授权失效 | 端到端覆盖批量赋值/过度提交类别 |
| API4:2023 不受限制的资源消耗 | 速率限制、分页上限、深度/复杂度限制、消息大小上限 |
| API5:2023 功能级授权失效 | 路由组中间件一致性;通过文档/内省暴露隐藏端点 |
| API6:2023 对敏感业务流的不受限制访问 | 注册/兑换/投票流程中的反自动化门槛 |
| API7:2023 服务端请求伪造 | 不在此范围内——由 DESER/SSRF 模块覆盖 |
| API8:2023 安全配置错误 | 内省/操场/文档/调试标头/CORS/内容类型混淆/gRPC 反射 |
| API9:2023 清单管理不当 | 版本繁杂、规范过时、已弃用版本仍在线 |
| API10:2023 不安全的 API 消费 | 部分覆盖(面向第三方的密钥作用域);完整覆盖暂缓 |
CWE 条目
- CWE-915 对动态确定的对象属性进行不当控制的修改:https://cwe.mitre.org/data/definitions/915.html
- CWE-770 未限制或限流的资源分配:https://cwe.mitre.org/data/definitions/770.html
- CWE-799 对交互频率控制不当:https://cwe.mitre.org/data/definitions/799.html
- CWE-306 关键功能缺少身份验证:https://cwe.mitre.org/data/definitions/306.html
- CWE-862 缺少授权:https://cwe.mitre.org/data/definitions/862.html
- CWE-639 通过用户可控的键绕过授权:https://cwe.mitre.org/data/definitions/639.html
- CWE-200 向未经授权的主体暴露敏感信息:https://cwe.mitre.org/data/definitions/200.html
- CWE-400 未受控的资源消耗:https://cwe.mitre.org/data/definitions/400.html
- CWE-798 使用硬编码凭据:https://cwe.mitre.org/data/definitions/798.html
- CWE-346 来源验证错误:https://cwe.mitre.org/data/definitions/346.html
- CWE-942 对不可信域名采用过于宽松的跨域策略:https://cwe.mitre.org/data/definitions/942.html
延伸阅读
- OWASP 备忘单系列 — GraphQL:https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
- OWASP 备忘单系列 — REST 安全:https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html
- OWASP 备忘单系列 — 拒绝服务:https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html
- OWASP 备忘单系列 — HTML5(WebSocket 来源指南):https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html
- RFC 6455 第 10 节(WebSocket 来源注意事项):https://datatracker.ietf.org/doc/html/rfc6455#section-10
- GraphQL 规范安全说明:https://graphql.org/learn/security/
- socket.io CORS 处理:https://socket.io/docs/v4/handling-cors/
- gRPC 身份验证指南:https://grpc.io/docs/guides/auth/