2023 年,某电商平台因一个简单的 IDOR 漏洞泄露了 3700
万条用户记录。攻击者没有使用任何高级技术——只是把请求中的
user_id=10086 改成了
user_id=10087,服务端没有校验请求者是否有权访问该用户的数据,整个数据库就这样被遍历了一遍。这类漏洞在
OWASP API Security Top 10 中被归类为 BOLA(Broken Object
Level
Authorization,对象级授权失效),连续两版占据榜首,但在日常代码审查中却很少被当作安全问题来对待。
传统的 Web 安全思路——部署一台 WAF(Web Application Firewall,Web 应用防火墙),配好规则,定期扫描——在 API 安全领域已经失效。原因很简单:WAF 擅长识别已知的攻击模式(SQL 注入的特征字符串、XSS 的脚本标签),但 BOLA 的请求跟正常请求在语法上完全一样,WAF 看不出任何异常。API 安全的核心挑战不在于识别”坏数据”,而在于回答”这个人有没有权限做这件事”——这是一个业务逻辑问题,不是一个模式匹配问题。
在 上一篇 中我们讨论了零信任架构的基本原则,本文聚焦 API 层面的安全设计——当零信任提供了身份验证和网络分段的基础设施之后,API 安全要解决的是:如何在每一个接口、每一次请求的粒度上,确保授权、输入验证和滥用防护三道防线都到位。
适用范围说明 本文基于 OWASP API Security Top 10 2023 版本。代码示例基于 Go 1.21 和 Java 17。WAF 讨论以 ModSecurity 3.x 和云厂商托管 WAF 为参考。API 网关部分以 Kong、Envoy 为例。
一、问题场景
先看三个真实的 API 安全事故场景,它们分别代表三种不同的攻击类别。
场景一:水平越权
一个 SaaS 平台的订单查询接口:
GET /api/v1/orders/12345
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
服务端校验了 JWT
令牌的有效性——确认请求者是一个合法用户——但没有校验订单 12345
是否属于该用户。攻击者遍历订单
ID,获取了全部用户的订单数据。修复成本很低(加一行
WHERE user_id = ?),但在事故发生之前,没有人认为这是一个安全问题。
场景二:SSRF 通过 Webhook
一个协作工具允许用户配置 Webhook(网络钩子) URL,当事件触发时向该 URL 发送通知:
POST /api/v1/webhooks
{
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
"events": ["document.updated"]
}
用户把 Webhook URL 指向了 AWS 元数据服务(AWS Metadata Service)的内网地址。服务端没有做出站地址过滤(Egress Filtering),直接向该地址发起请求,把 IAM 临时凭证返回给了攻击者。攻击者拿到凭证后横向移动,访问了 S3 存储桶中的全部客户数据。
场景三:GraphQL 深度查询攻击
一个内容平台提供 GraphQL(图查询语言) 接口:
query {
user(id: "1") {
posts {
comments {
author {
posts {
comments {
author {
posts { title }
}
}
}
}
}
}
}
}这个嵌套 6 层的查询在语法上完全合法,但它会在数据库层面触发指数级的 JOIN 操作。10 个并发请求就能打垮一台数据库实例。WAF 对此无能为力,因为请求体不包含任何已知的攻击特征。
这三个场景有一个共同特点:攻击请求与正常请求在 HTTP 协议层面完全一样。没有恶意字符,没有异常头部,没有可疑的 User-Agent。传统的边界防御(WAF、IP 黑名单、速率限制)对它们基本无效。这就是 API 安全区别于传统 Web 安全的根本原因——防御逻辑必须深入到业务层面。
二、OWASP API Security Top 10
OWASP(Open Worldwide Application Security Project,开放全球应用安全项目) 在 2023 年发布了 API Security Top 10 的第二版。与 2019 年的第一版相比,新增了三个类别,反映了 API 安全威胁的演变方向。
| 排名 | 2023 版标识 | 中文名称 | 核心风险 |
|---|---|---|---|
| 1 | API1 | 对象级授权失效(BOLA) | 攻击者通过篡改对象 ID 访问其他用户的资源 |
| 2 | API2 | 认证机制失效 | 令牌泄露、弱密码策略、会话管理缺陷 |
| 3 | API3 | 对象属性级授权失效 | 批量赋值(Mass Assignment),返回过多字段 |
| 4 | API4 | 无限制资源消耗 | 缺少速率限制,大负载请求,资源耗尽 |
| 5 | API5 | 功能级授权失效 | 普通用户访问管理员接口 |
| 6 | API6 | 服务端请求伪造(SSRF) | 服务端被诱导向内网发起请求(2023 新增) |
| 7 | API7 | 安全配置错误 | 默认配置、不必要的 HTTP 方法、CORS 过宽 |
| 8 | API8 | 缺乏对自动化威胁的防护 | 撞库、爬虫、API 滥用(2023 新增) |
| 9 | API9 | 资产管理不当 | 影子 API、废弃版本未下线 |
| 10 | API10 | API 的不安全消费 | 信任第三方 API 返回的数据(2023 新增) |
最容易被忽视的三个类别:
BOLA(API1):技术上最简单,防御上最困难。不需要任何攻击工具,改一个 ID 就行。但防御它需要在每一个数据访问点做所有权校验,这是一个工程管理问题,不是一个技术问题。
SSRF(API6):2019 版没有单独列出,2023 版新增。现代 API 大量使用 Webhook、文件导入(File Import)、URL 预览(URL Preview)等功能,每一个接受外部 URL 的接口都是潜在的 SSRF 入口。
不安全消费(API10):开发者通常会对用户输入做验证,但对第三方 API 返回的数据却直接信任。如果第三方 API 被入侵或返回恶意数据,整个链路都会被污染。
接下来的章节将逐一拆解这些类别中最关键的攻击模式和架构级防御方案。
三、BOLA 与对象级授权
BOLA 之所以连续两版占据 Top 10 榜首,是因为它满足三个条件:容易利用、难以检测、影响范围大。
3.1 攻击模式
BOLA 的本质是:服务端在处理数据访问请求时,只验证了”这个人是谁”(身份认证),没有验证”这个人能不能访问这条数据”(对象级授权)。
常见的 BOLA 模式:
GET /api/orders/{orderId} -- 水平越权:访问其他用户的订单
GET /api/users/{userId}/profile -- 水平越权:查看其他用户资料
DELETE /api/documents/{docId} -- 水平越权:删除其他用户的文档
PUT /api/accounts/{accountId} -- 水平越权:修改其他用户的账户
攻击者不需要猜测复杂的参数,只需要遍历数字 ID 或者分析前端代码找到 API 路径。如果 ID 是自增整数,遍历成本极低;即使用了 UUID(通用唯一标识符),泄露一个 ID 就能访问对应的资源。
3.2 架构级防御
防御原则:在数据访问层强制绑定所有权。
以 Go 为例,一个有 BOLA 漏洞的接口:
// 有漏洞的实现:只验证了令牌,没有验证所有权
func GetOrder(w http.ResponseWriter, r *http.Request) {
orderID := chi.URLParam(r, "orderID")
order, err := db.GetOrderByID(orderID)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(order)
}修复后的实现:
// 安全的实现:在查询条件中绑定当前用户
func GetOrder(w http.ResponseWriter, r *http.Request) {
orderID := chi.URLParam(r, "orderID")
userID := auth.GetUserID(r.Context()) // 从令牌中提取用户 ID
order, err := db.GetOrderByUserAndID(userID, orderID)
if err != nil {
// 不区分"不存在"和"无权限",统一返回 404
http.Error(w, "not found", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(order)
}对应的 SQL 查询:
-- 有漏洞的查询
SELECT * FROM orders WHERE id = ?;
-- 安全的查询:强制绑定 user_id
SELECT * FROM orders WHERE id = ? AND user_id = ?;关键设计决策:
在数据层面而非应用层面做绑定。不要先查出来再判断,而是在 SQL 的 WHERE 子句中直接加上所有权条件。这样即使应用层逻辑有遗漏,数据层也能兜底。
错误响应不泄露信息。无论是”资源不存在”还是”无权访问”,统一返回 404。如果返回 403,攻击者可以确认该资源存在,这本身就是信息泄露。
使用中间件统一处理。对于 RESTful API,可以抽象一个资源所有权中间件:
// 资源所有权中间件
func OwnershipMiddleware(resourceType string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
resourceID := chi.URLParam(r, "id")
hasAccess, err := authz.CheckOwnership(r.Context(), authz.CheckInput{
UserID: userID,
ResourceType: resourceType,
ResourceID: resourceID,
Action: r.Method,
})
if err != nil || !hasAccess {
http.Error(w, "not found", http.StatusNotFound)
return
}
next.ServeHTTP(w, r)
})
}
}
// 路由注册
r.Route("/api/v1/orders/{id}", func(r chi.Router) {
r.Use(OwnershipMiddleware("order"))
r.Get("/", GetOrder)
r.Put("/", UpdateOrder)
r.Delete("/", DeleteOrder)
})3.3 多租户场景的 BOLA 防御
在多租户(Multi-Tenant)系统中,BOLA 防御还需要加上租户隔离层:
// 多租户资源访问:同时绑定租户 ID 和用户 ID
func GetTenantResource(w http.ResponseWriter, r *http.Request) {
tenantID := auth.GetTenantID(r.Context())
userID := auth.GetUserID(r.Context())
resourceID := chi.URLParam(r, "id")
resource, err := db.GetResource(r.Context(), tenantID, userID, resourceID)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(resource)
}对应的查询必须同时包含两个约束:
SELECT * FROM resources
WHERE id = ? AND tenant_id = ? AND (owner_id = ? OR ? IN (
SELECT user_id FROM resource_shares WHERE resource_id = resources.id
));四、SSRF 防御
SSRF(Server-Side Request Forgery,服务端请求伪造)在 2023 版 Top 10 中被单独列为 API6,反映了现代 API 生态中这类攻击的普遍性。
4.1 攻击向量
现代 API 中,以下功能都可能成为 SSRF 入口:
| 功能 | 攻击方式 | 风险等级 |
|---|---|---|
| Webhook 回调 | 将回调 URL 指向内网服务 | 高 |
| 文件导入(URL) | 提交内网 URL 作为文件源 | 高 |
| URL 预览/缩略图 | 让服务端抓取内网页面 | 中 |
| PDF 生成 | 在 HTML 模板中嵌入内网资源引用 | 中 |
| RSS/Atom 订阅 | 订阅源指向内网地址 | 中 |
一个典型的 SSRF 攻击链:
1. 攻击者提交 Webhook URL:http://169.254.169.254/latest/meta-data/
2. 服务端向该 URL 发起 HTTP GET 请求
3. AWS 元数据服务返回 IAM 临时凭证
4. 服务端把响应内容返回给攻击者(或记录在日志中)
5. 攻击者使用凭证访问 S3、RDS 等内部服务
4.2 防御架构
SSRF 的防御需要多层配合,单一措施不够:
第一层:输入验证
import (
"net"
"net/url"
"strings"
)
var privateRanges = []net.IPNet{
parseCIDR("10.0.0.0/8"),
parseCIDR("172.16.0.0/12"),
parseCIDR("192.168.0.0/16"),
parseCIDR("169.254.0.0/16"), // 链路本地地址
parseCIDR("127.0.0.0/8"), // 回环地址
parseCIDR("::1/128"), // IPv6 回环
parseCIDR("fc00::/7"), // IPv6 唯一本地地址
parseCIDR("fe80::/10"), // IPv6 链路本地地址
}
func parseCIDR(cidr string) net.IPNet {
_, network, _ := net.ParseCIDR(cidr)
return *network
}
// ValidateExternalURL 校验 URL 是否指向外部地址
func ValidateExternalURL(rawURL string) error {
parsed, err := url.Parse(rawURL)
if err != nil {
return fmt.Errorf("URL 格式无效")
}
// 只允许 HTTPS
if parsed.Scheme != "https" {
return fmt.Errorf("仅允许 HTTPS 协议")
}
// 解析主机名为 IP 地址
host := parsed.Hostname()
ips, err := net.LookupIP(host)
if err != nil {
return fmt.Errorf("无法解析主机名: %s", host)
}
// 检查所有解析出的 IP 是否为内网地址
for _, ip := range ips {
for _, privateRange := range privateRanges {
if privateRange.Contains(ip) {
return fmt.Errorf("不允许访问内网地址")
}
}
}
return nil
}第二层:DNS 重绑定(DNS Rebinding)防护
输入验证有一个经典绕过手法:DNS 重绑定。攻击者控制一个域名,第一次解析返回外网 IP(通过验证),第二次解析返回内网 IP(实际请求时)。防御方式是在建立连接时再次检查 IP:
// 安全的 HTTP 客户端:在连接阶段检查目标 IP
func NewSafeHTTPClient() *http.Client {
transport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
// 在连接阶段再次解析并校验 IP
ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
for _, ip := range ips {
for _, privateRange := range privateRanges {
if privateRange.Contains(ip.IP) {
return nil, fmt.Errorf("连接目标为内网地址,已拒绝")
}
}
}
// 使用解析后的 IP 直接连接,避免二次解析
dialer := &net.Dialer{Timeout: 10 * time.Second}
return dialer.DialContext(ctx, network, net.JoinHostPort(ips[0].IP.String(), port))
},
}
return &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 3 {
return fmt.Errorf("重定向次数超过限制")
}
// 对重定向目标也做 URL 校验
return ValidateExternalURL(req.URL.String())
},
}
}第三层:网络级出站过滤(Egress Filtering)
即使应用层代码有漏洞,网络层也应该限制服务端能够访问的地址范围:
# 出站规则示例(安全组 / 网络策略)
# 允许:DNS 查询
ALLOW outbound TCP/UDP 53 to DNS-servers
# 允许:HTTPS 到公网
ALLOW outbound TCP 443 to 0.0.0.0/0
# 拒绝:所有到内网的出站流量
DENY outbound ALL to 10.0.0.0/8
DENY outbound ALL to 172.16.0.0/12
DENY outbound ALL to 192.168.0.0/16
DENY outbound ALL to 169.254.0.0/16
在 Kubernetes 环境中,使用 NetworkPolicy(网络策略)限制 Pod 的出站流量:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: webhook-sender-egress
spec:
podSelector:
matchLabels:
app: webhook-sender
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 169.254.0.0/16
ports:
- protocol: TCP
port: 443
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
app: kube-dns
ports:
- protocol: UDP
port: 534.3 SSRF 防御的完整策略
三层防御缺一不可:
- 输入验证可以拦截大部分简单攻击,但会被 DNS 重绑定绕过。
- 连接阶段校验可以防 DNS 重绑定,但如果代码有遗漏(比如某个新功能忘了用安全客户端),就会失效。
- 网络级过滤是兜底层,即使应用代码有漏洞,流量也出不去。
五、注入攻击防御
注入攻击(Injection Attack)是最古老的 Web 安全威胁,但在 API 场景下有了新的变种。
5.1 SQL 注入
传统的 SQL 注入在使用 ORM 或参数化查询的项目中已经很少见,但在以下场景仍然常见:
- 动态拼接排序字段:
ORDER BY user_input - 动态拼接过滤条件:
WHERE status IN (user_input) - 原生 SQL 查询中的字符串拼接
// 有漏洞的代码:动态排序字段
func ListUsers(sortBy string) ([]User, error) {
// sortBy 来自用户输入,直接拼接到 SQL 中
query := fmt.Sprintf("SELECT * FROM users ORDER BY %s", sortBy)
return db.Query(query)
}
// 攻击输入:sortBy = "1; DROP TABLE users;--"安全的实现使用白名单(Allowlist)校验:
// 安全的实现:白名单校验排序字段
var allowedSortFields = map[string]string{
"name": "name",
"created_at": "created_at",
"email": "email",
}
func ListUsers(sortBy string) ([]User, error) {
column, ok := allowedSortFields[sortBy]
if !ok {
column = "created_at" // 默认排序
}
query := fmt.Sprintf("SELECT id, name, email FROM users ORDER BY %s", column)
return db.Query(query)
}5.2 NoSQL 注入
MongoDB 的查询操作符注入是最常见的 NoSQL 注入形式:
// 正常登录请求
{"username": "admin", "password": "secret123"}
// 注入攻击:使用 $ne 操作符绕过密码校验
{"username": "admin", "password": {"$ne": ""}}如果服务端直接把请求体传给 MongoDB
查询,{"$ne": ""}
会匹配所有非空密码,攻击者无需知道密码就能登录。
防御方式:对输入做类型校验,确保密码字段是字符串而非对象:
type LoginRequest struct {
Username string `json:"username" validate:"required,alphanum,min=3,max=32"`
Password string `json:"password" validate:"required,min=8,max=128"`
}
func Login(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "请求格式错误", http.StatusBadRequest)
return
}
// 使用 validator 库做结构体验证
if err := validate.Struct(req); err != nil {
http.Error(w, "参数验证失败", http.StatusBadRequest)
return
}
// req.Password 已经被确认为 string 类型,不可能是 MongoDB 操作符
user, err := db.FindUser(r.Context(), req.Username, hashPassword(req.Password))
// ...
}5.3 GraphQL 特有威胁
GraphQL 引入了几种 REST API 中不存在的攻击面:
深度查询攻击:前面场景三已经展示过,嵌套查询可以导致指数级的数据库负载。
批量查询攻击(Batching Attack):GraphQL 通常支持在一个 HTTP 请求中发送多个查询:
[
{"query": "{ user(id: \"1\") { email } }"},
{"query": "{ user(id: \"2\") { email } }"},
{"query": "{ user(id: \"3\") { email } }"},
// ... 重复 10000 次
]字段建议泄露(Field Suggestion Leaking):部分 GraphQL 实现在报错时会提示”你是不是要查 xxx 字段”,攻击者可以利用这个特性枚举 Schema 中的所有字段。
防御措施汇总:
// GraphQL 安全配置示例(使用 gqlgen 框架)
func NewGraphQLHandler() http.Handler {
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{
Resolvers: &resolver.Resolver{},
}))
// 限制查询深度
srv.Use(extension.FixedComplexityLimit(200))
// 自定义复杂度计算
srv.AroundFields(func(ctx context.Context, next graphql.Resolver) (interface{}, error) {
fc := graphql.GetFieldContext(ctx)
if fc.IsResolver {
complexity := graphql.GetOperationContext(ctx).Stats.Complexity
if complexity > 500 {
return nil, fmt.Errorf("查询复杂度超过限制")
}
}
return next(ctx)
})
// 禁用内省查询(生产环境)
srv.Use(apollotracing.Tracer{})
return srv
}六、API 网关安全架构
API 网关(API Gateway)是 API 安全的第一道集中式防线。一个设计良好的 API 网关应该在请求到达业务服务之前,完成身份认证、速率限制、输入校验等通用安全功能。
6.1 安全架构分层
以下是一个典型的 API 安全架构分层图:
graph TB
Client[客户端] --> CDN[CDN / DDoS 防护]
CDN --> WAF[WAF 层]
WAF --> Gateway[API 网关]
subgraph API 网关安全功能
Gateway --> AuthN[身份认证<br/>JWT 验证 / OAuth2]
AuthN --> RateLimit[速率限制<br/>令牌桶 / 滑动窗口]
RateLimit --> InputVal[输入校验<br/>Schema 验证]
InputVal --> AuthZ[粗粒度授权<br/>角色/权限检查]
AuthZ --> Transform[请求转换<br/>脱敏 / 裁剪]
end
Transform --> ServiceA[业务服务 A<br/>细粒度授权 BOLA 防御]
Transform --> ServiceB[业务服务 B<br/>细粒度授权 BOLA 防御]
Transform --> ServiceC[业务服务 C<br/>细粒度授权 BOLA 防御]
ServiceA --> DB[(数据库<br/>行级安全)]
ServiceB --> DB
ServiceC --> DB
style Gateway fill:#f0f4ff,stroke:#3b82f6,stroke-width:2px
style AuthN fill:#dbeafe,stroke:#3b82f6
style RateLimit fill:#dbeafe,stroke:#3b82f6
style InputVal fill:#dbeafe,stroke:#3b82f6
style AuthZ fill:#dbeafe,stroke:#3b82f6
style Transform fill:#dbeafe,stroke:#3b82f6
各层职责划分:
| 层级 | 职责 | 典型工具 |
|---|---|---|
| CDN / DDoS 防护 | 吸收流量攻击,缓存静态内容 | Cloudflare、AWS Shield |
| WAF | 已知攻击模式过滤(SQL 注入特征、XSS 标签) | ModSecurity、AWS WAF |
| API 网关 | 认证、速率限制、Schema 校验、粗粒度授权 | Kong、Envoy、APISIX |
| 业务服务 | BOLA 防御、业务逻辑授权、数据过滤 | 应用代码 |
| 数据库 | 行级安全(Row-Level Security)、字段级加密 | PostgreSQL RLS |
6.2 速率限制架构
速率限制(Rate Limiting)不只是防 DDoS,更重要的是防止 API 滥用和暴力破解。一个有效的速率限制系统需要多个维度:
// 多维度速率限制配置
type RateLimitConfig struct {
// 全局限制:保护基础设施
GlobalRPS int `yaml:"global_rps"` // 每秒请求数
// 用户级限制:防止单用户滥用
PerUserRPS int `yaml:"per_user_rps"`
PerUserBurst int `yaml:"per_user_burst"`
// 接口级限制:保护高成本接口
EndpointLimits map[string]EndpointLimit `yaml:"endpoint_limits"`
}
type EndpointLimit struct {
Path string `yaml:"path"`
RPS int `yaml:"rps"`
BurstSize int `yaml:"burst_size"`
// 针对敏感接口的额外限制
PerIPLimit int `yaml:"per_ip_limit"`
RequireCaptcha bool `yaml:"require_captcha"` // 超限后要求验证码
}配置示例:
rate_limits:
global_rps: 10000
per_user_rps: 100
per_user_burst: 200
endpoint_limits:
login:
path: "/api/v1/auth/login"
rps: 5
burst_size: 10
per_ip_limit: 3
require_captcha: true
password_reset:
path: "/api/v1/auth/reset-password"
rps: 2
burst_size: 5
per_ip_limit: 1
export:
path: "/api/v1/data/export"
rps: 1
burst_size: 26.3 请求校验
API 网关应该根据 OpenAPI(开放 API 规范)定义校验每一个请求的格式,在请求到达业务服务之前就拒绝格式不合法的请求:
# OpenAPI 定义中的安全相关约束
paths:
/api/v1/users/{userId}:
get:
parameters:
- name: userId
in: path
required: true
schema:
type: string
format: uuid
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/UserResponse"
# 响应 Schema 也要定义,防止数据过度暴露在 API 网关层做 Schema 校验的好处:
- 不合法的请求在网关层就被拒绝,不会到达业务服务,减少攻击面。
- 业务服务不需要重复实现参数格式校验。
- Schema 是可版本化、可审计的安全策略。
七、WAF 的局限
WAF 在传统 Web 安全中发挥了重要作用,但在 API 安全领域,WAF 的局限性越来越明显。
7.1 WAF 的工作原理
WAF 的核心是模式匹配:维护一组规则(正则表达式或签名),对每一个请求的 URL、头部、请求体进行匹配,命中规则的请求被拦截。
# ModSecurity 规则示例:检测 SQL 注入
SecRule ARGS "@rx (?i)(union\s+select|or\s+1\s*=\s*1|drop\s+table)" \
"id:1001,phase:2,deny,status:403,msg:'SQL Injection Detected'"
7.2 WAF 对 API 安全失效的原因
原因一:BOLA 请求与正常请求完全一样。
GET /api/orders/12345 HTTP/1.1
Authorization: Bearer valid-token
这个请求没有任何恶意特征。WAF 无法判断 token 的持有者是否有权访问订单 12345,因为这需要查询业务数据库——而 WAF 没有这个能力。
原因二:JSON 结构化数据增加了绕过难度。
RESTful API 使用 JSON 作为数据格式,攻击载荷可以藏在深层嵌套的 JSON 结构中:
{
"filters": {
"advanced": {
"conditions": [
{"field": "name", "op": "eq", "value": "'; DROP TABLE users;--"}
]
}
}
}WAF 需要完整解析 JSON 结构并检查每一个叶子节点,这在性能上是一个挑战。很多 WAF 对 JSON 的解析深度有限制,超过限制的部分会被跳过。
原因三:误报导致规则被弱化。
API 场景中,合法的请求体可能包含 WAF
规则中的关键词。比如一个技术博客的 API,用户在文章内容中写了
SELECT * FROM users——这是正常内容,不是攻击。但
WAF 的 SQL
注入规则会将其拦截。为了减少误报,运维团队不得不放宽规则或增加白名单(Allowlist),这反过来又降低了检测率。
原因四:业务逻辑攻击无特征。
以下攻击在 WAF 看来都是正常请求:
- 用合法凭证大量调用数据导出接口(数据窃取)
- 在促销活动中用多个账号重复领取优惠(业务滥用)
- 通过 API 枚举手机号是否注册(信息收集)
这些都是业务逻辑层面的攻击,没有技术特征可以匹配。
7.3 WAF 的正确定位
WAF 不是没有用,而是不能作为 API 安全的主要防线。WAF 的合理定位是:
- 防御已知的通用攻击模式:SQL 注入、XSS、路径遍历等有明确特征的攻击。
- 虚拟补丁(Virtual Patching):在业务代码修复之前,通过 WAF 规则临时拦截特定漏洞的利用。
- 合规要求:部分行业合规标准(PCI DSS 等)要求部署 WAF。
但 API 安全的核心防线必须在应用层和网关层建立。
八、Shift-Left 安全策略
Shift-Left(左移)是指把安全检查从部署后的运行时阶段前移到开发和构建阶段。在 API 安全领域,Shift-Left 的核心工具是 API 规范验证(API Spec Validation)和安全扫描(Security Scanning)。
8.1 OpenAPI 安全规约检查
通过静态分析(Static Analysis) OpenAPI 规范文件,可以在代码编写之前就发现潜在的安全问题:
# .spectral.yaml -- API 安全规约规则
extends: ["spectral:oas"]
rules:
# 所有接口必须定义认证方式
operation-security-defined:
description: "每个操作必须定义安全方案"
given: "$.paths[*][get,post,put,delete,patch]"
then:
field: security
function: truthy
severity: error
# 禁止在 URL 中传递敏感参数
no-sensitive-query-params:
description: "敏感参数不应出现在查询字符串中"
given: "$.paths[*][*].parameters[?(@.in == 'query')]"
then:
field: name
function: pattern
functionOptions:
notMatch: "(?i)(password|token|secret|key|credential|api.?key)"
severity: error
# 响应必须定义 Schema,防止数据过度暴露
response-schema-defined:
description: "响应必须定义 Schema"
given: "$.paths[*][*].responses[*].content[*]"
then:
field: schema
function: truthy
severity: warn
# 路径参数必须有格式约束
path-param-format:
description: "路径参数必须定义 format 或 pattern"
given: "$.paths[*].parameters[?(@.in == 'path')].schema"
then:
function: schema
functionOptions:
schema:
anyOf:
- required: ["format"]
- required: ["pattern"]
severity: warn在 CI/CD 中集成:
# GitHub Actions 中的 API 安全检查
name: API Security Lint
on:
pull_request:
paths:
- "api/**/*.yaml"
- "api/**/*.json"
jobs:
spectral-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Spectral
uses: stoplightio/spectral-action@latest
with:
file_glob: "api/**/*.yaml"
spectral_ruleset: ".spectral.yaml"8.2 安全契约测试
契约测试(Contract Testing)不仅可以验证 API 的功能正确性,还可以验证安全属性:
// 安全契约测试示例
func TestOrderAPI_AuthorizationContract(t *testing.T) {
tests := []struct {
name string
userID string
orderOwner string
wantStatus int
}{
{
name: "用户访问自己的订单应返回 200",
userID: "user-001",
orderOwner: "user-001",
wantStatus: http.StatusOK,
},
{
name: "用户访问他人的订单应返回 404",
userID: "user-001",
orderOwner: "user-002",
wantStatus: http.StatusNotFound,
},
{
name: "无认证请求应返回 401",
userID: "",
orderOwner: "user-001",
wantStatus: http.StatusUnauthorized,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
order := createTestOrder(t, tt.orderOwner)
req := newRequest(t, "GET", "/api/v1/orders/"+order.ID)
if tt.userID != "" {
req.Header.Set("Authorization", "Bearer "+generateToken(tt.userID))
}
resp := executeRequest(req)
if resp.StatusCode != tt.wantStatus {
t.Errorf("期望状态码 %d,实际 %d", tt.wantStatus, resp.StatusCode)
}
})
}
}
// SSRF 防御契约测试
func TestWebhookAPI_SSRFProtection(t *testing.T) {
tests := []struct {
name string
url string
wantStatus int
}{
{"内网地址应被拒绝", "http://169.254.169.254/latest/meta-data/", http.StatusBadRequest},
{"回环地址应被拒绝", "http://127.0.0.1:8080/internal", http.StatusBadRequest},
{"私有地址应被拒绝", "http://10.0.0.1/admin", http.StatusBadRequest},
{"非 HTTPS 应被拒绝", "http://example.com/webhook", http.StatusBadRequest},
{"合法 HTTPS 地址应通过", "https://example.com/webhook", http.StatusCreated},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := fmt.Sprintf(`{"url": "%s", "events": ["order.created"]}`, tt.url)
req := newAuthRequest(t, "POST", "/api/v1/webhooks", body)
resp := executeRequest(req)
if resp.StatusCode != tt.wantStatus {
t.Errorf("URL %s:期望状态码 %d,实际 %d", tt.url, tt.wantStatus, resp.StatusCode)
}
})
}
}8.3 SAST 与 DAST 集成
静态应用安全测试(SAST,Static Application Security Testing)和动态应用安全测试(DAST,Dynamic Application Security Testing)应该集成到 CI/CD 流水线中:
# CI/CD 安全扫描流水线
name: Security Pipeline
on: [push, pull_request]
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run gosec (Go 安全扫描)
uses: securego/gosec@master
with:
args: "-exclude=G104 ./..."
dependency-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run dependency vulnerability scan
uses: aquasecurity/trivy-action@master
with:
scan-type: "fs"
scan-ref: "."
api-spec-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint OpenAPI specs
run: npx @stoplight/spectral-cli lint api/openapi.yaml --ruleset .spectral.yaml
dast:
needs: [sast, dependency-check, api-spec-lint]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to staging
run: ./deploy-staging.sh
- name: Run DAST scan
uses: zaproxy/action-full-scan@v0.10.0
with:
target: "https://staging-api.example.com"
rules_file_name: "zap-rules.tsv"8.4 Shift-Left 的投资回报
根据 IBM 的研究数据,安全缺陷在不同阶段被发现时的修复成本比:
| 发现阶段 | 相对修复成本 |
|---|---|
| 设计阶段 | 1x |
| 编码阶段 | 6.5x |
| 测试阶段 | 15x |
| 生产环境 | 100x |
Shift-Left 策略的核心价值不仅在于降低成本,更在于将安全从”事后检查”变为”设计约束”。当 API 规范要求每个接口都必须定义认证方式,当安全契约测试会检查每个数据接口的所有权校验,当 CI 流水线会扫描每一次代码提交——安全就不再依赖个人的安全意识,而是成为工程流程的一部分。
九、工程案例
FinSecure 支付平台的 API 纵深防御实践
FinSecure 是一家金融科技公司,提供 B2B 支付处理服务。2023 年初,一次安全审计发现其 API 平台存在多个 BOLA 漏洞和潜在的 SSRF 风险。以下是他们在 6 个月内建立纵深防御体系的工程实践。
背景:
- 200+ 个 API 接口,服务 500+ 企业客户
- 技术栈:Go 微服务,PostgreSQL,Kong API 网关,Kubernetes
- 每天处理 2000 万笔交易请求
- 需要满足 PCI DSS Level 1 合规要求
第一阶段:止血(第 1-2 周)
审计发现的最严重问题是商户资金查询接口的 BOLA 漏洞:商户 A 可以查询商户 B 的余额。团队用 48 小时完成了紧急修复:
- 在所有资金相关接口的 SQL 查询中加上
merchant_id约束。 - 在 Kong 网关层增加临时速率限制:资金查询接口每商户每分钟 10 次。
- 排查审计日志,确认该漏洞未被利用。
第二阶段:系统性修复(第 3-8 周)
团队开发了一个授权中间件框架,强制所有数据访问都经过所有权校验:
// FinSecure 的资源授权框架
type ResourcePolicy struct {
ResourceType string
OwnerField string // 数据库中的所有者字段名
AccessRules []AccessRule
}
type AccessRule struct {
Role string // 角色
Actions []string // 允许的操作
Conditions []string // 附加条件
}
var policies = map[string]ResourcePolicy{
"transaction": {
ResourceType: "transaction",
OwnerField: "merchant_id",
AccessRules: []AccessRule{
{Role: "merchant", Actions: []string{"read"}, Conditions: []string{"own"}},
{Role: "merchant_admin", Actions: []string{"read", "refund"}, Conditions: []string{"own"}},
{Role: "platform_admin", Actions: []string{"read", "refund", "freeze"}, Conditions: nil},
},
},
"settlement": {
ResourceType: "settlement",
OwnerField: "merchant_id",
AccessRules: []AccessRule{
{Role: "merchant", Actions: []string{"read"}, Conditions: []string{"own"}},
{Role: "finance", Actions: []string{"read", "approve"}, Conditions: nil},
},
},
}同时,团队对所有接受外部 URL 的接口(Webhook 回调、对账文件下载)做了 SSRF 加固:
- 部署了前文描述的三层 SSRF 防御(输入校验 + 连接阶段检查 + 网络策略)。
- Webhook 请求通过一个专用的出站代理(Outbound Proxy)发送,该代理只允许访问公网地址。
第三阶段:Shift-Left 建设(第 9-16 周)
- OpenAPI 规范强制化:所有新接口必须先编写 OpenAPI 定义,CI 流水线用 Spectral 检查安全规约。
- 安全契约测试:为每一类资源编写 BOLA 测试用例,纳入 CI 必须通过的测试集。
- 依赖扫描:集成 Trivy 扫描容器镜像和 Go 模块的已知漏洞。
- 安全指标仪表盘:追踪以下指标并设置告警阈值。
第四阶段:持续监控(第 17 周起)
部署了运行时 API 安全监控:
- 异常检测:识别偏离基线的 API 调用模式(如某商户突然高频调用查询接口)。
- 授权失败监控:统计每个接口的 403/404 比率,异常升高时触发告警。
- 影子 API 发现:对比 API 网关的访问日志与 OpenAPI 定义,识别未文档化的接口。
成效指标:
| 指标 | 修复前 | 修复后 |
|---|---|---|
| BOLA 漏洞数量 | 23 个 | 0 个(CI 持续检测) |
| SSRF 风险接口 | 8 个 | 0 个(全部加固) |
| 安全缺陷发现到修复平均时间 | 45 天 | 3 天(CI 阶段发现) |
| API 规范覆盖率 | 30% | 100% |
| 安全契约测试覆盖率 | 0% | 85% |
关键经验:
- BOLA 修复不能靠人工排查。200 个接口逐一审查不现实,必须通过框架和中间件在架构层面解决。
- SSRF 防御必须有网络层兜底。应用层代码的 URL 校验总会有遗漏的地方。
- Shift-Left 的前提是 API 规范化。如果没有 OpenAPI 定义,静态安全分析就无从下手。
- 安全指标要可观测。不能衡量的东西无法改进。
十、选型对比
在建设 API 安全体系时,团队需要在不同的安全方案之间做选择。以下对比表覆盖三种主要的安全实施层级。
10.1 安全实施层级对比
| 维度 | WAF | API 网关 | 应用层 |
|---|---|---|---|
| 防御 BOLA | 无法防御 | 有限(粗粒度角色检查) | 完全防御(对象级授权) |
| 防御 SQL 注入 | 良好(模式匹配) | 良好(Schema 校验) | 最佳(参数化查询) |
| 防御 SSRF | 有限 | 有限 | 良好(URL 校验 + 安全客户端) |
| 业务逻辑攻击 | 无法防御 | 有限(速率限制) | 需要定制化逻辑 |
| 误报率 | 高(尤其是 JSON 场景) | 低 | 最低 |
| 部署复杂度 | 低(独立部署) | 中(需要与服务集成) | 高(需要修改业务代码) |
| 性能影响 | 中(深度包检测) | 低(轻量代理) | 最低(内联逻辑) |
| 维护成本 | 中(规则维护) | 中(策略配置) | 高(代码维护) |
| 合规满足度 | 部分(PCI DSS 要求) | 良好 | 最佳 |
| 对新接口的覆盖 | 自动(全流量检查) | 需要配置路由 | 需要编写代码 |
10.2 推荐策略
不是选择其中一种,而是三层都要有,各自承担不同的职责。
WAF 层: 过滤已知的通用攻击模式(低成本、广覆盖、浅防御)
API 网关层: 认证、速率限制、Schema 校验(中成本、中覆盖、中防御)
应用层: BOLA 防御、业务逻辑安全(高成本、窄覆盖、深防御)
资源有限时的优先级:
- 首先做应用层的 BOLA 防御。这是影响最大、WAF 和网关都无法替代的部分。
- 然后做 API 网关的认证和速率限制。这是投入产出比最高的基础设施。
- 最后部署 WAF。如果有合规要求就部署,否则可以后置。
10.3 API 网关选型对比
| 维度 | Kong | Envoy | APISIX | AWS API Gateway |
|---|---|---|---|---|
| 认证插件 | JWT、OAuth2、OIDC | 通过 ext_authz 外部服务 | 内置多种认证插件 | IAM、Cognito 集成 |
| 速率限制 | 内置,支持 Redis | 内置,支持本地和分布式 | 内置,支持 Redis | 内置,按阶段配置 |
| Schema 校验 | 插件支持 | 需要自定义过滤器 | 插件支持 | 内置请求验证器 |
| WAF 集成 | 可集成 ModSecurity | 需要外部 WAF | 可集成多种 WAF | 内置 AWS WAF 集成 |
| 可观测性 | Prometheus、Datadog | 原生支持多种后端 | Prometheus、Skywalking | CloudWatch 集成 |
| 部署模式 | 独立部署 / Kubernetes | Sidecar / 独立部署 | 独立部署 / Kubernetes | 托管服务 |
| 社区生态 | 丰富 | 丰富 | 增长中 | 无(闭源) |
| 适用场景 | 通用 API 管理 | 服务网格、高性能代理 | 高性能网关 | AWS 技术栈 |
选型建议:
- 已经使用服务网格(Service Mesh)的团队,优先考虑 Envoy,避免引入额外组件。
- 需要丰富插件生态的团队,优先考虑 Kong 或 APISIX。
- AWS 全家桶用户,优先使用 AWS API Gateway,减少运维负担。
十一、总结
API 安全的核心挑战不在于识别”坏请求”,而在于回答”这个请求者有没有权限做这件事”。这个问题的答案藏在业务逻辑中,而非 HTTP 协议层面。
本文讨论的防御体系可以归纳为以下几个层次:
数据层:在 SQL 查询中强制绑定所有权条件,用行级安全(Row-Level Security)做兜底。这是防御 BOLA 的最后一道防线。
应用层:通过授权中间件在每个数据访问点做所有权校验;对所有接受外部 URL 的功能做三层 SSRF 防御(输入校验、连接阶段检查、网络策略);使用参数化查询和类型校验防御注入攻击。
网关层:集中处理身份认证、速率限制和请求 Schema 校验。网关不能替代应用层的细粒度授权,但可以拦截大量无效请求,降低后端服务的安全压力。
流程层:通过 Shift-Left 策略,将安全检查嵌入开发和构建流程。OpenAPI 安全规约检查、安全契约测试、SAST/DAST 扫描——这些工具把安全从个人责任变成工程流程的一部分。
最后一个容易被忽视的点:API 资产管理。OWASP Top 10 中的 API9(资产管理不当)提醒我们,未文档化的影子 API(Shadow API)、废弃但未下线的旧版本接口,往往是攻击者的首选目标。一个完整的 API 安全体系,从 API 的诞生(规范定义)到 API 的退役(版本下线),都应该有明确的生命周期管理。
下一篇 将讨论加密架构设计——当 API 层面的访问控制解决了”谁能看”的问题之后,加密要解决的是”看到了也读不懂”的问题。
参考资料
- OWASP. “OWASP API Security Top 10 - 2023.” OWASP Foundation, 2023. https://owasp.org/API-Security/editions/2023/en/0x11-t10/
- Madden, Neil. API Security in Action. Manning Publications, 2020.
- NIST. “SP 800-204A: Building Secure Microservices-based Applications Using Service-Mesh Architecture.” NIST, 2020.
- Yalon, Erez, and Inon Shkedy. “OWASP API Security Project.” OWASP Foundation. https://owasp.org/www-project-api-security/
- Kong Inc. “Kong Gateway Security Plugins Documentation.” https://docs.konghq.com/hub/#security
- Envoy Proxy. “External Authorization Filter.” https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter
- Stoplight. “Spectral - Open-Source API Style Guide Enforcer.” https://stoplight.io/open-source/spectral
- IBM. “Cost of a Data Breach Report 2023.” IBM Security, 2023.
- Zalewski, Michal. The Tangled Web: A Guide to Securing Modern Web Applications. No Starch Press, 2011.
- OWASP. “OWASP Testing Guide v4.2: Testing for BOLA.” OWASP Foundation, 2023.
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【系统架构设计百科】架构质量属性:不只是"高可用高性能"
需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。
【系统架构设计百科】告警策略:如何避免"狼来了"
大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。
【系统架构设计百科】复杂性管理:架构的核心战场
系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略
【系统架构设计百科】微服务架构深度审视:优势、代价与适用边界
微服务不是免费的午餐。本文从分布式系统八大谬误出发,拆解微服务真正解决的问题与引入的代价,梳理服务边界划分的工程方法论,还原 Amazon 和 Netflix 从单体到微服务的真实演进时间线,给出微服务适用与不适用的判断框架。