关于 sast-jwt
sast-jwt 是用于检测不安全 JWT 实现的定向安全审计技能。它先梳理令牌签发、验证和配置,再分析验证位置,并要求预先存在 `sast/architecture.md`。
主要功能
- 01两阶段 JWT 审计
- 02梳理 JWT 生命周期
- 03分析令牌验证位置
- 04检查常见 JWT 弱点
适用场景
- 01审计 JWT 身份验证代码
- 02检查签名和声明验证
- 03查找不安全的密钥或算法处理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ utkusen/ sast-skills --skill sast-jwt安装后在 Claude Code 中输入/sast-jwt确认已加载。
详细介绍
JWT 漏洞检测
你正在进行有针对性的安全评估,以查找不安全的 JSON Web Token(JWT)实现。此技能采用由子 Agent 执行的两阶段方法:先进行侦察(梳理 JWT 的完整生命周期——签发、验证和配置),再进行分析(找出这些验证位置中所有可利用的弱点)。
前提条件:必须存在 sast/architecture.md。如果不存在,请先运行分析技能。
什么是不安全的 JWT 实现
JWT 由三个经过 Base64URL 编码的部分组成:header.payload.signature。头部声明签名算法(alg),载荷包含声明(例如 sub、role、exp),签名则是用于证明完整性的密码学凭证。如果服务器信任令牌自身对签名方式的声明、完全不验证签名、使用容易猜测的密钥,或信任令牌中嵌入的攻击者可控密钥材料,就会产生漏洞。
核心模式:服务器在信任 JWT 的声明之前,没有充分验证其真实性和完整性。
什么属于 JWT 漏洞
1. 算法混淆——alg: none
服务端接受头部声明 "alg": "none" 的 JWT,完全绕过签名验证。攻击者构造任意载荷,将 alg 设为 none,并省略签名。如果库处理了该令牌,就会接受这个伪造令牌。
2. 算法混淆——RS256 → HS256 配置为使用 RS256 的服务端(非对称算法:使用私钥签名,使用公钥验证)可能会被诱骗切换到 HS256 模式,前提是库允许令牌指定算法。由于公钥通常可以获取,攻击者可以使用服务端的公钥作为 HMAC 密钥,通过 HS256 为伪造令牌签名。服务端使用同一个公钥验证 HMAC,并接受该令牌。
3. 缺少或禁用签名验证 服务端解码 JWT 载荷,但实际上不验证签名。常见模式:
- Python(PyJWT):
jwt.decode(token, options={"verify_signature": False}) - Node.js(jsonwebtoken):使用
jwt.decode(token),而不是jwt.verify(token, secret) - 手动对载荷进行 base64 解码,但不检查签名
- 解码调用中接受
algorithms=["none"]
4. 弱 HMAC 密钥或硬编码的 HMAC 密钥
服务端使用短、容易猜到或硬编码的密钥签署令牌(例如 "secret"、"password"、"changeme"、"jwt-secret-key")。攻击者截获有效令牌后,可以使用 hashcat 或 jwt_tool 等工具离线暴力破解密钥,然后伪造任意令牌。
5. 嵌入式 JWK(jwk 头部注入)
令牌头部包含嵌入式 JSON Web Key(jwk 参数)。如果验证代码信任嵌入的密钥来验证令牌自身的签名,攻击者就可以生成自己的密钥对,使用私钥为伪造令牌签名,并将公钥嵌入头部。服务端使用攻击者嵌入的公钥验证签名,并接受该令牌。
6. JKU / X5U 头部注入
jku(JWK 集 URL)或 x5u(X.509 证书 URL)头部值用于从 URL 获取验证密钥。如果服务端不根据允许列表验证 URL,攻击者就可以将其指向自己托管了精心构造的密钥集的服务端。
7. 密钥 ID(kid)头部注入
kid 头部用于查找签名密钥,通常从数据库或文件系统中查找。如果未经清理就将 kid 值插入 SQL 查询,它就会成为 SQL 注入途径。如果将其拼接到文件路径中,它就会成为路径遍历途径。
8. 缺少声明验证
- 未检查
exp→ 过期令牌会永远有效 - 未检查
iss(颁发者)→ 接受其他服务颁发的令牌 - 未检查
aud(受众)→ 接受面向其他服务的令牌 - 未检查
nbf(生效时间)→ 在有效时间窗口开始前使用令牌
9. 没有令牌撤销机制 没有令牌黑名单或撤销机制。被盗或已登出的令牌会一直有效,直到过期。令牌有效期较长时,这一点尤其重要。
什么不属于 JWT 漏洞
不要将以下问题标记为 JWT 漏洞:
- IDOR:更改
user_id声明以访问其他用户的数据属于授权缺陷,而非 JWT 伪造——只有在令牌本身可以被伪造时才标记 - 通过 JWT 载荷导致 XSS:在之后未经转义就被渲染的声明中注入
<script>——这是 XSS,不是 JWT 漏洞 - CSRF:在没有
SameSite的情况下将 JWT 放入 Cookie——这是 CSRF 问题,不是 JWT 完整性问题 - 验证限制得当:使用强密钥的
jwt.verify(token, secret, { algorithms: ['HS256'] })——不存在漏洞
防止 JWT 漏洞的模式
1. 在验证调用中设置算法允许列表
# Python — PyJWT: explicitly specify allowed algorithms
payload = jwt.decode(token, secret, algorithms=["HS256"])
# Node.js — jsonwebtoken: restrict algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] })
# Java — jjwt: specify expected algorithm
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)
# (jjwt does not use the header's alg; it uses the key type)
2. 使用强随机密钥
# Strong secret: at least 256 bits of entropy, not hardcoded
import secrets
SECRET_KEY = secrets.token_hex(32) # load from env in production
3. 完整验证声明
payload = jwt.decode(
token, secret, algorithms=["HS256"],
options={"require": ["exp", "iss", "aud"]},
issuer="https://myapp.example.com",
audience="myapp-api"
)
4. 使用不存在算法歧义的非对称密钥
// Use RS256 with public key for verification; never accept HS256 on the same endpoint
jwt.verify(token, publicKey, { algorithms: ['RS256'] })
5. 为 JWK/JKU URL 设置允许列表
# Only fetch keys from a known, trusted JWKS endpoint
ALLOWED_JWKS_URLS = {"https://accounts.google.com/.well-known/jwks.json"}
if jku not in ALLOWED_JWKS_URLS:
raise ValueError("Untrusted JWK URL")
漏洞示例与安全示例
Python — PyJWT
# VULNERABLE: signature verification disabled
def get_current_user(token: str):
payload = jwt.decode(token, options={"verify_signature": False})
return payload["user_id"]
# VULNERABLE: accepts alg:none because no algorithm restriction
def get_current_user(token: str):
payload = jwt.decode(token, SECRET_KEY) # PyJWT < 2.x default: accepts any alg
return payload["user_id"]
# VULNERABLE: weak hardcoded secret
SECRET_KEY = "secret"
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
# SECURE: algorithm restricted, strong secret from env
SECRET_KEY = os.environ["JWT_SECRET"] # strong, random, from environment
def get_current_user(token: str):
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return payload["user_id"]
Node.js — jsonwebtoken
// VULNERABLE: jwt.decode() — no signature verification
function getUser(token) {
const payload = jwt.decode(token); // decode only, never verify
return payload.userId;
}
// VULNERABLE: algorithms not restricted — susceptible to alg:none or RS256→HS256
function getUser(token) {
const payload = jwt.verify(token, SECRET); // no algorithms option
return payload.userId;
}
// VULNERABLE: weak hardcoded secret
const SECRET = "password123";
jwt.verify(token, SECRET, { algorithms: ['HS256'] });
// SECURE: algorithm restricted, strong secret from env
const SECRET = process.env.JWT_SECRET;
function getUser(token) {
const payload = jwt.verify(token, SECRET, { algorithms: ['HS256'] });
return payload.userId;
}
Java — jjwt
// VULNERABLE: deprecated parser (accepts alg from header)
Jwts.parser().setSigningKey(key).parseClaimsJws(token);
// VULNERABLE: no expiry check — the library default may not enforce exp
Claims claims = Jwts.parserBuilder()
.setSigningKey(key).build()
.parseClaimsJws(token).getBody();
// claims.getExpiration() never checked
// SECURE: parserBuilder (does not trust header alg; uses key type)
Claims claims = Jwts.parserBuilder()
.requireIssuer("myapp")
.requireAudience("myapp-api")
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
Go — golang-jwt / dgrijalva/jwt-go
// VULNERABLE: accepts any algorithm including "none"
token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil // no algorithm check
})
// VULNERABLE: weak secret
var jwtKey = []byte("secret")
// SECURE: validate signing method before returning key
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtKey, nil
})
kid 标头 SQL 注入
# VULNERABLE: kid used in SQL query without sanitization
def get_signing_key(kid):
result = db.execute(f"SELECT key FROM jwt_keys WHERE id = '{kid}'")
return result.fetchone()[0]
token_header = jwt.get_unverified_header(token)
key = get_signing_key(token_header["kid"]) # attacker controls kid
jwt.decode(token, key, algorithms=["HS256"])
# SECURE: kid validated against allowlist or parameterized lookup
def get_signing_key(kid):
result = db.execute("SELECT key FROM jwt_keys WHERE id = %s", (kid,))
row = result.fetchone()
if not row:
raise ValueError("Unknown key id")
return row[0]
嵌入式 JWK 注入
// VULNERABLE: trusts the jwk embedded in the token header
const { publicKey } = getPublicKeyFromHeader(decoded.header); // attacker-supplied
jwt.verify(token, publicKey);
// SECURE: only use keys from a pre-configured, trusted source
const trustedKey = loadKeyFromConfig();
jwt.verify(token, trustedKey, { algorithms: ['RS256'] });
执行
此技能分两个阶段运行,并使用子 Agent。将 sast/architecture.md 的内容作为上下文传递给两个子 Agent。
阶段 1:梳理 JWT 生命周期
使用以下说明启动一个子 Agent:
目标:梳理应用如何创建、传输和验证 JWT。找出所有 JWT 签发和验证位置、使用的库、签名算法和密钥/密钥字符串配置,以及用于授权的声明。将结果写入
sast/jwt-recon.md。背景:你将获得项目的架构摘要。请据此了解技术栈、身份验证层和中间件模式。
搜索内容:
1. JWT 库导入 — 确定使用了哪个 JWT 库:
- Python:
import jwt、from jose import、from authlib import、import python_jose- Node.js:
require('jsonwebtoken')、import jwt from 'jsonwebtoken'、jose、@nestjs/jwt- Java:
io.jsonwebtoken、com.auth0.jwt、nimbus-jose-jwt- Go:
github.com/golang-jwt/jwt、github.com/dgrijalva/jwt-go、github.com/lestrrat-go/jwx- Ruby:
jwtgem(require 'jwt')- PHP:
firebase/php-jwt、lcobucci/jwt- C#:
System.IdentityModel.Tokens.Jwt、Microsoft.AspNetCore.Authentication.JwtBearer2. JWT 签名 / 签发位置 — 查找令牌的创建位置:
jwt.encode(...)、jwt.sign(...)、Jwts.builder().signWith(...)、JWT.create().sign(...)- 记录使用的算法(
HS256、RS256等)以及密钥/密钥字符串的来源(环境变量、配置、硬编码)3. JWT 验证 / 解码位置 — 查找令牌的使用位置:
jwt.decode(...)、jwt.verify(...)、Jwts.parserBuilder()...parseClaimsJws(...)、JWT::decode(...)- 记录传入的选项:
algorithms、options、verify_signature、verify_exp- 注意是否使用原始的
decode(不验证)而不是verify调用4. 令牌提取 — 查找从传入请求中读取令牌的位置:
- Authorization 标头:
request.headers.get("Authorization")、req.headers['authorization']- Cookie:
request.cookies.get("token")、req.cookies.token- 查询参数:
request.args.get("token")、req.query.token5. 授权中间件 / 装饰器 — 查找集中式 JWT 检查:
@jwt_required、@login_required、requireAuth、JwtAuthGuard、[Authorize]、中间件函数- 记录受保护和未受保护的路由
6. 签名密钥 / 密钥配置:
- 查找 HMAC 密钥或 RSA/EC 密钥的定义和加载位置(环境变量、配置文件、硬编码字符串)
- 判断其强度如何(较长的随机字符串还是较弱的短字符串、常见词)
7. 声明使用情况:
- 查找提取并用于授权的声明(
user_id、role、permissions、sub)- 查找是否检查
exp、iss、aud、nbf输出格式 — 写入
sast/jwt-recon.md:# JWT Recon: [项目名称] ## Summary 此代码库[使用 / 未使用] JWT。 库:[库名称及可见的版本] 算法:[HS256 / RS256 / 等] ## Issuance Sites ### 1. [描述性名称 — 例如“登录端点中的令牌生成”] - **File**:`path/to/file.ext`(第 X-Y 行) - **Function / endpoint**:[函数名称或路由] - **Algorithm**:[例如 HS256] - **Secret/key source**:[环境变量名称 / 硬编码字符串 / 配置键] - **Claims set**:[载荷中添加的声明列表] - **Code snippet**:[签名调用]
## Verification Sites ### 1. [描述性名称 — 例如“身份验证中间件中的令牌验证”] - **File**:`path/to/file.ext`(第 X-Y 行) - **Function / middleware**:[函数名称] - **Verification call**:[jwt.decode / jwt.verify / parseClaimsJws / 等] - **Algorithm restriction**:[algorithms=["HS256"] / 无限制 / 未知] - **Signature verification**:[已启用 / 已禁用 / 不明确] - **Claims validated**:[exp / iss / aud / 无 / 未知] - **Token source**:[Authorization 标头 / Cookie / 查询参数] - **kid/jwk/jku used**:[是 — 描述其用法 / 否] - **Code snippet**:[验证调用及其上下文]
## Secret / Key Configuration - **Secret source**:[环境变量 / 硬编码 / 配置文件] - **Apparent strength**:[强(较长的随机字符串) / 弱(较短或常见字符串) / 未知] - **Code snippet**(如果是硬编码或可疑配置):[相关代码]
## Authorization Middleware Coverage - **Protected routes**:[列表或说明] - **Unprotected routes**:[列表或“未发现”]
第一阶段之后:继续前先检查 JWT 的使用情况
第一阶段完成后,阅读 sast/jwt-recon.md。如果摘要指出未使用 JWT(未发现签发或验证位置),则完全跳过第二阶段。改为将以下内容写入 sast/jwt-results.md,然后停止:
# JWT Analysis Results
No JWT usage detected in this codebase.
只有在第一阶段发现至少一个 JWT 验证位置时,才继续第二阶段。
第二阶段:分析 JWT 验证位置的漏洞
第一阶段完成后,启动第二个子 Agent,并附上以下指令:
目标:针对
sast/jwt-recon.md中的每个 JWT 验证位置,判断其是否可被利用。检查算法混淆、缺少签名验证、弱密钥、头部注入攻击以及缺少声明验证。将最终结果写入sast/jwt-results.md。背景:你将获得项目架构摘要和第 1 阶段的侦察结果。请结合两者了解完整的令牌生命周期,然后分析每个位置。
请针对每个验证位置检查以下内容:
检查 1——算法限制
- 验证调用中是否明确指定了允许的算法?
- 如果没有算法限制,是否可以将令牌的
alg头设为none,以跳过签名验证?- 如果服务使用非对称算法(RS256、ES256),验证代码是否也接受 HMAC 算法(HS256)?如果是,服务可能容易受到 RS256→HS256 混淆攻击。
检查 2——是否启用了签名验证
- 令牌是否传入了实际检查签名的 verify/parse 调用,还是仅传入了只解码的调用?
- 检查
verify_signature: False、complete=False等选项,或使用jwt.decode()(Node.js)而非jwt.verify()- 仅手动对载荷进行 base64 解码而不检查签名,始终存在漏洞
检查 3——HMAC 密钥强度
- 密钥是否硬编码在源代码中?如果是,是否为常见词语或短字符串?
- 密钥是否从环境变量或配置中加载?即便如此,也要注意默认值或示例值是否过弱
- 长度少于 32 个字符或由字典词语组成的密钥可能容易被暴力破解
检查 4——内嵌 JWK / JKU / X5U 头部注入
- 验证代码是否从令牌头中读取
jwk字段,并用它验证同一个令牌?- 代码是否获取
jku或x5u头中指定 URL 的密钥,却没有根据允许列表验证该 URL?- 任一情况成立,验证机制都可被完全绕过
检查 5——
kid头部注入
- 验证前是否从令牌中提取
kid头的值并用于查找密钥?- 是否将
kid值插入未参数化的 SQL 查询中?→ SQL 注入- 是否使用未经清理的
kid值构造文件路径?→ 路径遍历 / 密钥替换检查 6——声明验证
- 是否检查
exp(过期时间)?如果没有,过期令牌将永远有效- 是否检查
iss(签发者)?如果没有,将接受其他签发者的令牌- 是否检查
aud(受众)?如果没有,将接受面向其他服务的令牌- 是否存在
role或permissions等安全敏感声明,却未根据服务端数据源进行验证?检查 7——令牌撤销
- 是否存在令牌黑名单、撤销端点,或短期令牌 + 刷新令牌模式?
- 如果令牌有效期较长(数小时或更久)且没有撤销机制,被盗令牌仍会保持有效
分类:
- 存在漏洞:弱点明确存在,且没有有效缓解措施——攻击路径可直接利用。
- 可能存在漏洞:弱点很可能存在,但需要确认次要条件(例如库版本行为、默认选项值)。
- 不存在漏洞:实现正确处理了此项检查。
- 需要人工审查:仅凭静态分析无法有把握地判断漏洞状态。
输出格式——写入
sast/jwt-results.md:# JWT 分析结果:[项目名称] ## 执行摘要 - 已分析的验证位置:[N] - 存在漏洞:[N] - 可能存在漏洞:[N] - 不存在漏洞:[N] - 需要人工审查:[N] ## 发现 ### [存在漏洞] 描述性名称 - **文件**:`path/to/file.ext`(第 X-Y 行) - **端点 / 函数**:[路由或函数名称] - **漏洞类别**:[例如,“缺少签名验证” / “接受 alg:none” / “弱 HMAC 密钥” / “JWK 头部注入” / “kid SQL 注入” / “缺少 exp 验证”] - **问题**:[清楚描述错误之处] - **攻击场景**:[分步说明:攻击者如何操作、如何构造或修改令牌、可以获得什么访问权限] - **影响**:[攻击者可以实现什么——伪造任意身份、提升权限、访问其他用户的数据等] - **修复建议**:[具体修复方法——添加算法限制、启用 verify_signature、从环境变量加载密钥、固定 JWKS URL、参数化 kid 查询、添加 exp 验证等] - **动态测试**:[使用 jwt_tool、hashcat 或 curl 编写概念验证。 展示可复现问题的准确命令。 示例:
- jwt_tool -X a (测试 alg:none)
- jwt_tool -X s (测试 RS256→HS256 混淆)
- hashcat -a 0 -m 16500 wordlist.txt (暴力破解 HMAC 密钥)
- 手动操作:修改载荷,将 alg 设为 none,并向端点发送]
### [可能存在漏洞] 描述性名称 - **文件**:`path/to/file.ext`(第 X-Y 行) - **端点 / 函数**:[路由或函数名称] - **漏洞类别**:[类别] - **问题**:[看起来有误之处] - **不确定性**:[需要确认的内容——例如,“库版本会影响默认行为”] - **修复建议**:[修复方法] - **动态测试**:[尝试利用的载荷或命令]
### [不存在漏洞] 描述性名称 - **文件**:`path/to/file.ext`(第 X-Y 行) - **端点 / 函数**:[路由或函数名称] - **原因**:[例如,“算法限制为 HS256,使用从环境变量加载的强密钥;并且验证 exp”] ### [需要人工审查] 描述性名称 - **文件**:`path/to/file.ext`(第 X-Y 行) - **端点 / 函数**:[路由或函数名称] - **不确定性**:[无法通过静态分析确定漏洞状态的原因] - **建议**:[需要人工检查的内容——例如,“确认安装的 JWT 库版本;较旧版本的 PyJWT 默认接受 alg:none”]
重要提醒
- 阅读
sast/architecture.md,并将其内容作为上下文传递给两个子 Agent。 - 第 2 阶段必须在第 1 阶段完成后运行——它依赖侦察结果。
- 第 1 阶段纯粹用于发现:定位所有 JWT 签发、验证和配置位置。不要在第 1 阶段评估安全性——这是第 2 阶段的工作。
- 第 2 阶段纯粹用于分析:针对第 1 阶段发现的每个验证位置,系统地检查所有漏洞类别。不要在第 2 阶段搜索新位置——应聚焦于第 1 阶段发现的内容。
- 如果第 1 阶段未发现 JWT 用法,则完全跳过第 2 阶段,并写入一份“No JWT usage detected”结果文件。
- 最关键的检查包括:禁用签名验证、未限制算法(
alg:none/ RS256→HS256 混淆)以及较弱或硬编码的 HMAC 密钥。这些问题会直接导致完全绕过身份验证。 - Node.js 的
jsonwebtoken库中的jwt.decode()仅用于解码,绝不会验证签名。只有jwt.verify()才会验证签名。混淆这两者是常见且严重的错误。 - 在 PyJWT 2.0 之前的版本中,默认接受
alg: none,且不要求提供algorithms参数。如果代码库未固定版本或限制算法,请将其标记出来。 - 算法混淆(RS256→HS256)需要满足以下条件:(a) 服务端使用带密钥对的 RS256;(b) 公钥可被访问;(c) 验证代码未限制算法。三个条件必须同时满足。
kid注入经常被忽视:只要令牌头中存在kid,就务必检查密钥查找的实现方式。- 如有疑问,应归类为“需人工审查”,而非“无漏洞”。在安全评估中,漏报比误报更糟糕。