mcpField
S
sast-jwt

sast-jwt

github.com/utkusen安全1.3k66 次安装5 天前更新
代码仓库

关于 sast-jwt

sast-jwt 是用于检测不安全 JWT 实现的定向安全审计技能。它先梳理令牌签发、验证和配置,再分析验证位置,并要求预先存在 `sast/architecture.md`。

主要功能

  1. 01两阶段 JWT 审计
  2. 02梳理 JWT 生命周期
  3. 03分析令牌验证位置
  4. 04检查常见 JWT 弱点

适用场景

  1. 01审计 JWT 身份验证代码
  2. 02检查签名和声明验证
  3. 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:jwt gem(require 'jwt')
  • PHP:firebase/php-jwt、lcobucci/jwt
  • C#:System.IdentityModel.Tokens.Jwt、Microsoft.AspNetCore.Authentication.JwtBearer

2. 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.token

5. 授权中间件 / 装饰器 — 查找集中式 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,就务必检查密钥查找的实现方式。
  • 如有疑问,应归类为“需人工审查”,而非“无漏洞”。在安全评估中,漏报比误报更糟糕。

评论

支持 Markdown