AWS API Gateway尾部斜杠鉴权绕过原理与多层修复方案
发布时间:2026/8/28 23:09:24 作者:尧图编辑部 阅读量:1,286

打开 API 网关日志的时候我们经常能见到一种诡异的 401同一个接口客户端只要在路径末尾多敲一个斜杠身份校验好像就“隐身”了。更让人头疼的是这种问题不是你代码里写错了鉴权逻辑而是 AWS API Gateway 在路径匹配上给你开了一个意料之外的口子。很多人第一反应是“这不就是个路由容错吗”但在生产环境里它可能直接演变成一次未授权访问。这篇文章我会把 Trailing Slash尾部斜杠如何影响 AWS API Gateway 授权链路的原理讲透然后用可复现的配置和请求示例演示绕过过程最后给出从网关配置、WAF 规则到代码层的多层修复方案。无论你是用 REST API、HTTP API还是 SAM/CloudFormation 管理基础设施这篇文章都值得收藏备用。1. 这篇文章真正要解决的问题先说结论当 API Gateway 中存在/{proxy}通配资源或者资源路径中同时存在/protected与/protected/两种路径定义时授权器Authorizer可能只在其中一种路径上生效。攻击者通过构造尾部带有斜杠的请求让请求落入未承载授权器的路径分支从而绕过 Lambda Authorizer、Cognito User Pools 或 IAM 鉴权直接命中后端服务。这既不是“后端代码写错了”也不是“密钥泄露”而是API 网关的路径匹配机制与授权器绑定方式之间的空隙。问题的隐蔽性在于网关的访问日志里请求确实到达了 API后端服务可能因为路由框架自动忽略尾部斜杠照样处理请求但从网关授权器的角度看这条请求根本没有触发鉴权。文章剩余部分我会围绕一个最小复现项目展开。你将看到同样是访问/protected普通请求返回 200 且要求身份凭证加上一个斜杠变成/protected/却可能直接返回 200完全不校验身份。2. 从一次“多敲了个斜杠”谈起问题为什么值得警惕很多开发者在设计 API 时并不会刻意区分GET /users和GET /users/。在我们的直觉里这是同一个资源。但在 AWS API Gateway 的资源模型中/users和/users/是两个完全不同的路径。如果你在控制台里手动创建资源会注意到一个细节创建users资源后如果继续在它下面创建子资源路径会拼接成/users/{id}而如果你想创建/users/这种“尾部斜杠资源”实际上需要把路径写成子资源路径的根或者利用代理资源来捕获。问题的核心在于API Gateway 的授权器是绑定到具体的方法Method上的。也就是说GET /users配置了 Lambda Authorizer不代表GET /users/也被保护。这种“方法级”鉴权模型天然给尾部斜杠绕过留下了空间。如果只是网关层面多暴露一个未鉴权端点也就算了更麻烦的是后端。很多后端框架Spring、Express、Flask、ASP.NET Core默认会做路径规范化把/users/当成/users处理。于是请求链路就变成攻击者发送GET /users/API Gateway 匹配到未绑定授权器的方法或代理网关把请求转发给后端后端忽略尾部斜杠差异执行了GET /users的业务逻辑返回 200敏感数据泄露。从用户的角度看访问的是同一个资源从后端角度看执行的也是同一个逻辑但从安全角度看唯一的“安全关口”——API Gateway 授权器——被绕过了。3. AWS API Gateway 的授权机制与路径匹配原理3.1 路径匹配的基本规则AWS API Gateway 根据资源的路径结构来匹配传入请求。以 REST API 为例资源路径是一棵逻辑树/ ├── /protected (绑定 Lambda Authorizer 后端 Lambda) ├── /public (无鉴权开放访问) └── /{proxy} (代理资源)当一个请求进来API Gateway 会尝试把 URL 路径匹配到这棵树上。匹配规则类似“最长路径前缀匹配”{proxy}可以匹配任意路径。3.2 授权器如何绑定在 API Gateway REST API 中你可以选择四种授权方式授权方式鉴权主体典型场景Lambda Authorizer自定义 Lambda 返回 IAM 策略需要对接内部 SSO、JWT、多租户隔离Cognito User PoolsAWS Cognito 颁发令牌移动端或 Web 用户登录IAM 授权AWS SigV4 签名内部服务间调用无授权器任何请求直接通过公开接口、静态资源Lambda Authorizer 最终的返回结果是一份 IAM Policy里面包含Resource字段。API Gateway 会根据这个策略决定是否放行。3.3 “方法级”绑定带来的盲区默认情况下你在控制台为GET /protected绑定了授权器但你继续往下看这个授权器并不会自动套用到自动生成的任何“变体路径”。如果你创建了一个/protected/{proxy}的子代理资源并且不小心为它配置了新的方法该方法的授权器可能是默认的None。常见的错误配置包括手动创建了/{proxy}资源作为后端服务的通用转发入口但没有把默认授权器设置为AWS_IAM或某个 Lambda Authorizer使用 OpenAPI/Swagger 导入 API 定义时某个路径后缀带了/导入后自动生成了新资源但安全配置丢失使用 SAM 模板开发时Events里给/protected配置了Auth但给/{proxy}配置时漏掉了Auth。这些问题在平时根本不会暴露因为常规测试不会刻意去访问带尾部斜杠的路径。只有等到被扫描器或攻击者尝试时才会发现问题。4. 绕过复现环境搭建与最小配置为了说明问题我们搭建一个最小环境后端 Lambda返回 200只负责模拟业务逻辑资源路径/protected和/{proxy}授权器给GET /protected配置一个校验Authorization头的 Lambda AuthorizerGET /{proxy}不配置授权器。4.1 后端业务 Lambda先写一个简单的后端函数它接收来自 API Gateway 的请求并返回当前路径# 文件路径functions/backend.py import json def lambda_handler(event, context): # API Gateway 传进来的请求上下文 request_context event.get(requestContext, {}) path request_context.get(path, unknown) return { statusCode: 200, headers: {Content-Type: application/json}, body: json.dumps({ message: Hello from backend, path: path }) }这个 Lambda 不关心请求是否通过鉴权它只会如实在响应体里返回 API Gateway 看到的访问路径。4.2 授权器 Lambda我们用一个最简单的 Lambda Authorizer它检查Authorization头是否为固定 Token# 文件路径functions/authorizer.py import json def lambda_handler(event, context): # event 中 methodArn 的格式 # arn:aws:execute-api:{region}:{accountId}:{apiId}/{stageName}/{httpVerb}/{resourcePath} method_arn event.get(methodArn, ) print(methodArn:, method_arn) authorization event.get(headers, {}).get(Authorization, ) if authorization Bearer valid-token: # 允许访问 policy { principalId: user, policyDocument: { Version: 2012-10-17, Statement: [ { Action: execute-api:Invoke, Effect: Allow, Resource: method_arn } ] } } return policy # 默认拒绝 raise Exception(Unauthorized)注意实际的“拒绝”可以是抛出Unauthorized异常返回 401也可以返回 Deny 策略返回 403。这里我们选择直接抛异常符合大多数 JWT 校验失败的场景。4.3 SAM 模板使用 AWS SAM 定义资源# 文件路径template.yaml AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Globals: Function: Runtime: python3.12 Timeout: 10 MemorySize: 128 Resources: BackendFunction: Type: AWS::Serverless::Function Properties: CodeUri: functions/backend.py Handler: backend.lambda_handler Events: ProtectedPath: Type: Api Properties: Path: /protected Method: get Auth: Authorizer: MyLambdaAuthorizer CatchAll: Type: Api Properties: Path: /{proxy} Method: get # 注意这里没有配置 Auth默认不受保护 AuthorizerFunction: Type: AWS::Serverless::Function Properties: CodeUri: functions/authorizer.py Handler: authorizer.lambda_handler MyApi: Type: AWS::Serverless::Api Properties: StageName: prod Auth: DefaultAuthorizer: NONE Authorizers: MyLambdaAuthorizer: FunctionPayloadType: REQUEST FunctionArn: !GetAtt AuthorizerFunction.Arn Identity: Headers: - Authorization4.4 部署并获取调用地址执行下面的命令完成打包和部署sam build sam deploy --guided部署完成后SAM 会输出一个Outputs其中包含 API 的基础 URL类似https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod至此你拥有一个表面上“受保护”的 API。下面我们用 curl 验证绕过。5. 完整示例从 401 到 200 的绕过过程5.1 正常请求访问/protected不带 Tokencurl -i https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/protected因为没带Authorization我们预期授权器抛出UnauthorizedAPI Gateway 返回HTTP/1.1 401 Unauthorized {message:Unauthorized}这里的关键是Lambda Authorizer 执行了它发现Authorization头缺失于是拒绝。符合预期。5.2 正常请求访问/protected带正确 Tokencurl -i https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/protected \ -H Authorization: Bearer valid-token预期返回HTTP/1.1 200 OK {message:Hello from backend,path:/prod/protected}5.3 绕过尝试访问/protected/不带 Tokencurl -i https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/protected/这里会发生什么API Gateway 会尝试匹配路径/protected/。因为/protected这个资源无法直接匹配带已结束斜杠的路径它会往下走到/{proxy}这个代理资源。而GET /{proxy}没有绑定授权器所以请求直接放行到后端函数。于是响应变成HTTP/1.1 200 OK {message:Hello from backend,path:/prod/protected/}后端返回 200且完全没有身份验证。攻击者只需要在受保护资源路径后加一个/就完成了鉴权绕过。5.4 通过浏览器工具复现你可以直接用浏览器访问https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/protected→ 401 Unauthorizedhttps://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/protected/→ 200 OK如果生产环境中有管理接口比如/admin、/internal、/api/orders攻击者只要在地址栏加一个尾部斜杠就能绕过登录页进入后端逻辑。6. 受影响场景哪些 API 最容易中招并不是所有 API Gateway 配置都会遇到这个问题。以下三类场景是最容易中招的高危对象。6.1 同时存在“精确路径”和“代理路径”的 REST API这是最典型的场景。当 API 中既有精确路径如/protected又有/{proxy}或/v1/{proxy}时尾部斜杠请求会滑入代理资源。很多微服务架构中团队会把不需要网关层处理的路由全部交给/{proxy}转发例如/{proxy}转发到 Nginx 或 ALB/v1/{proxy}转发到某个微服务集群/admin/{proxy}转发到管理后台前端。如果精确路径配置了鉴权但代理资源没有配置那/admin/就等于直接暴露给公网。6.2 不同资源的“语义相同”但“路径不同”另一个容易被忽略的场景是开发阶段某同事先创建了/users资源后来另一位同事担心路径冲突又手动创建/users/资源或者通过子资源配置出尾部斜杠效果。两个路径映射到了同一个后端 Lambda但只有其中一个方法绑定了授权器。这类问题在代码评审中很难发现因为 CloudFormation/SAM 模板里往往只看到两个看似相同的路径不仔细看 URL 编码根本分辨不出。6.3 网关前存在路径规范化组件即便 API Gateway 配置正确只要前端有 CDN、负载均衡器或反向代理做了“自动补齐尾部斜杠”也可能改变匹配结果。例如CloudFront 默认会转发原始路径但如果你用 S3 作为源站它可能会对某些路径做重定向Nginxproxy_pass配上rewrite后也可能改变路径原文。一旦网关前的组件把普通路径改成了带尾部斜杠的路径原本用于匹配/{proxy}的逻辑就会生效鉴权又被跳过。7. 修复方案从网关到代码的多层防御修复尾部斜杠绕过不是只改一个配置就能高枕无忧。建议按照从外层到内层、从基础设施到业务代码的顺序逐步加固。7.1 在 API Gateway 层面禁用尾部斜杠路由最直接的方式不要为同一资源同时保留“精确路径”和“代理路径”。在资源设计上只保留一种规范化路径。如果你使用 OpenAPI 导入务必检查所有 path 是否以/结尾。例如paths: /protected: get: ... security: - MyAuthorizer: []如果发现/protected/这种 path 定义应立即删除或改为不含尾部斜杠的版本。导入后去 API Gateway 控制台“资源”页面确认没有出现重复资源。7.2 用 Lambda Authorizer 校验请求路径即使 API 资源树存在多个变体你也可以在授权器里做一道“路径白名单”校验。在 REQUEST 类型授权器中事件里带有methodArn你可以从methodArn中解析出资源路径。# 文件路径functions/authorizer_hardened.py import re # 需要保护的路径列表 PROTECTED_PREFIXES [ /protected, /admin, /internal, /v1/users, ] def _deny(method_arn): return { principalId: anonymous, policyDocument: { Version: 2012-10-17, Statement: [ { Action: execute-api:Invoke, Effect: Deny, Resource: method_arn } ] } } def lambda_handler(event, context): method_arn event.get(methodArn, ) # arn 格式arn:aws:execute-api:{region}:{accountId}:{apiId}/{stageName}/{httpVerb}/{resourcePath} parts method_arn.split(/) # 通过倒序找到HttpVerb再拼接资源路径 http_verb parts[-2] resource_path / /.join(parts[-3:-1]) # 这是资源路径的近似还原 # 这里也要做规范化去掉尾部斜杠 normalized_path resource_path.rstrip(/) or / for prefix in PROTECTED_PREFIXES: if normalized_path.startswith(prefix): authorization event.get(headers, {}).get(Authorization, ) if authorization ! Bearer valid-token: return _deny(method_arn) # 非受保护路径放行 return { principalId: anonymous, policyDocument: { Version: 2012-10-17, Statement: [ { Action: execute-api:Invoke, Effect: Allow, Resource: method_arn } ] } }但要注意从methodArn还原路径并不完全等价于原始 URL 中的路径因为它拿到的是 API Gateway 内部解析后的资源路径。所以更稳妥的做法是直接校验event里的path字段REQUEST 类型会带上。# 在 REQUEST 类型授权器中event 字段更丰富 def lambda_handler(event, context): request_path event.get(path, ) # 例如 /prod/protected/ normalized_path request_path.rstrip(/) or / # 只要路径命中受保护前缀就校验 Authorization if any(normalized_path.startswith(prefix) for prefix in PROTECTED_PREFIXES): authorization event.get(headers, {}).get(Authorization, ) if authorization ! Bearer valid-token: raise Exception(Unauthorized) return { principalId: anonymous, policyDocument: { Version: 2012-10-17, Statement: [ { Action: execute-api:Invoke, Effect: Allow, Resource: event.get(methodArn) } ] } }这里的关键动作是rstrip(/)。它把所有/admin/变成/admin然后落入同一个鉴权判断逻辑彻底消除尾部斜杠带来的差异。7.3 在 API Gateway 网关映射中显式拒绝尾部斜杠如果你不想在授权器里做额外逻辑也可以在集成层面把/protected/这类路径直接映射到 403。这在 SAM 模板里可以通过额外添加一个“精确的尾部斜杠资源”方法配合Mock集成实现StrictPathHandler: Type: AWS::Serverless::Function Properties: InlineCode: | def handler(event, context): return { statusCode: 400, headers: {Content-Type: application/json}, body: Trailing slash is not allowed } Handler: index.handler Runtime: python3.12 Events: BlockTrailingSlash: Type: Api Properties: Path: /protected/ Method: get # 不绑定授权器因为这就是一个入口级别的拒绝响应这样/protected/直接返回 400不会进入业务后端也就谈不上绕过。7.4 使用 AWS WAF 拦截尾部斜杠特征AWS WAF 可以部署在 API Gateway 前用来统一拦截异常路径。你需要创建一条规则匹配 “URI path 以斜杠结尾” 的请求并对其执行 Block。具体操作路径进入 AWS WAF 控制台创建 Web ACL绑定 API Gateway 阶段添加 Rule规则类型选择 “Rule builder”检查项选择UriPath匹配类型选择 “Ends with”匹配值填写/Action 选择Block。这样所有路径以/结尾的请求都会在网关外层被拦截。虽然 WAF 会额外产生费用但它能提供一层与环境无关的通用防护。7.5 后端服务也做尾部斜杠归一化不要只依赖网关层。后端应该在入口处统一处理路径Spring Boot可通过server.servlet.context-path或 Filter 做路径清理Express使用app.use((req, res, next) { if (req.path.endsWith(/) req.path.length 1) { res.redirect(301, req.path.slice(0, -1)); } else { next(); } });Flask借助strict_slashesFalse或中间件做处理。如果后端把/admin/当成/admin处理同时网关没有拦截那么攻击者仍然可以拿到预期响应。所以后端必须同步加固确保“尾部斜杠等价”这个逻辑只在安全控制之后完成。8. 常见问题与排查方法问题现象可能原因排查方式解决方案/protected返回 401/protected/返回 200/{proxy}或/protected/路径未绑定授权器检查 API Gateway 资源树逐个方法查看授权器配置删除重复路径或给代理资源补授权器所有带尾部斜杠的请求都返回 403WAF 规则误拦了合法请求查看 WAF 日志确认命中规则调整规则只对受保护前缀开启 Block授权器中没有看到path字段授权器类型是 TOKEN 而不是 REQUEST检查授权器配置改为 REQUEST 类型在Identity中指定 Header 来源并确保事件带路径后端收到/protected/请求并正常处理后端框架忽略尾部斜杠查看后端访问日志在后端入口对路径做严格校验或重定向增加 WAF 后/actuator/health/健康检查也被拦截WAF 规则过于宽泛在规则里增加not条件排除/health/等路径使用白名单前缀或正则精确匹配SAM 部署后找不到定义的尾部斜杠资源SAM/API Gateway 可能对路径做了规范化在控制台“资源”页面确认最终路径在 OpenAPI 定义中显式处理或改用代理资源/protected/*路径本意是开放却被 WAF 拦截WAF 按“以斜杠结尾”拦截所有带参数路径的结尾也会被拦检查实际请求 URL改为只拦截特定前缀 结尾斜杠的组合排错第一原则先打开 API Gateway 的 执行日志Execution Logs查看MethodArn、Authorizer状态和Integration Status。你可以快速确认请求是否被授权器处理过以及最终路由到了哪个资源。9. 从安全架构角度理解为什么这类问题很难被测试发现尾部斜杠绕过最具迷惑性的一点是它不会影响正常的测试用例。大部分开发者的接口测试遵循“文档里的路径”而文档里通常只包含不带尾部斜杠的版本。安全测试如果不刻意做路径变形也不会发现。更深层原因是网关的路径匹配逻辑和后端业务逻辑对“路径等价”的理解不一致。API Gateway 把/a和/a/当作两个不同资源而后端框架往往把两者映射到同一个控制器方法。这种语义差异就是“规范化不一致Normalization Discrepancy”造成的安全边界漏洞。这类漏洞不只在 AWS API Gateway 出现。在 Nginx、Spring Cloud Gateway、Kong 等网关中也存在类似情况。理解它的本质后你会明白查漏补缺不能只盯授权器配置还要关注所有“改写路径”的中间层。10. 最佳实践与工程建议10.1 用 OpenAPI 定义收敛资源路径所有 API 的定义尽量使用 OpenAPI/Swagger 文件管理并且做 CI 校验# 检查 OpenAPI 定义中是否有以 / 结尾的路径 grep -E ^ /.*/$ openapi.yaml如果 grep 结果不为空说明存在尾部斜杠资源定义CI 直接失败。10.2 设置“默认拒绝”授权器在 SAM/CloudFormation 中建议将默认授权器设为AWS_IAM或一个自定义 Lambda Authorizer而不是NONE。Globals: Api: Auth: DefaultAuthorizer: MyLambdaAuthorizer这样即使你忘了给某个方法配置显式授权器它也会沿用默认授权器。默认拒绝白名单放行是安全配置的基本原则。10.3 对所有受保护前缀做统一 WAF 规则WAF 规则不要只拦截/admin/而是拦截/admin开头且以/结尾或包含//、/./、/../等路径穿越特征这样能同时防住多种路径变形攻击。10.4 定期巡检 API Gateway 资源树写一个脚本遍历 API Gateway 的所有资源和方法检查是否存在同一路径有多个变体资源路径以/结尾任何方法没有授权器配置方法授权器类型与预期不符。# 获取 API 列表 aws apigateway get-rest-apis --region us-east-1 # 导出 API 资源 aws apigateway get-resources \ --rest-api-id api-id \ --region us-east-1 \ --query items[].[path,id] \ --output text10.5 关注网关访问日志启用 API Gateway Access Logging把path、status、authorizerStatus输出到 CloudWatch Logs。然后通过 CloudWatch Logs Insights 查询尾部斜杠请求fields timestamp, message | filter message like /\/$/ | sort timestamp desc | limit 100如果发现大量尾部斜杠请求很可能有人在悄悄探测你的鉴权边界。10.6 对授权器做单元测试如果你使用的是 Lambda Authorizer建议为它写独立的单元测试测试重点包括带/路径触发校验不带/路径触发校验多个斜杠//admin/的归一化路径中有 URL 编码字符如%2F、%2f时是否会被解码后正确匹配。测试用例示例# 文件路径tests/test_authorizer.py import json from functions import authorizer def test_trailing_slash_request_is_authorized(): event { methodArn: arn:aws:execute-api:us-east-1:123456789012:abc123/prod/GET/protected/, path: /prod/protected/, headers: { Authorization: Bearer valid-token } } result authorizer.lambda_handler(event, None) assert result[policyDocument][Statement][0][Effect] Allow def test_trailing_slash_without_token_is_denied(): event { methodArn: arn:aws:execute-api:us-east-1:123456789012:abc123/prod/GET/admin/, path: /prod/admin/, headers: {} } try: authorizer.lambda_handler(event, None) assert False, should raise Unauthorized except Exception: pass10.7 不要只依赖一层防护最稳妥的架构是WAF 拦截异常路径 API Gateway 授权器校验身份 后端统一做路径归一化。三层中哪怕某一层被绕过其他层也能兜底。11. 总结与后续学习方向尾部斜杠绕过 AWS API Gateway 授权本质上是资源匹配模型与开发直觉不一致导致的配置盲区。通过本文的复现你已经清楚/protected与/protected/在 API Gateway 中可能是两条不同的路径如果其中一条没有绑定授权器就会被当作“防御缺口”利用。需要留意的是这只是 API Gateway 路径规范化问题的冰山一角。后续建议你继续研究//双斜杠、/.、/..、URL 编码字符%2F在 API Gateway 中的匹配行为API Gateway REST API 与 HTTP API 在授权器上的行为差异Lambda Authorizer 的缓存策略是否会放大这类绕过风险CloudFront、ALB、API Gateway 三层路径传递时的完整攻击面。安全加固不能只看代码网关层的路径匹配逻辑同样需要纳入日常巡检。建议你把本文提到的检查项整理进团队的安全清单并在每次 API 变更时跑一遍。收藏备用下次遇到 401 变成 200 这种“灵异事件”你就知道先去查哪里了。