署名を検証せずにユーザー指定の JWT を解析

署名を検証せずにユーザー指定の JWT を解析

説明

Parser.ParseUnverified は JWT のヘッダーとクレームをデコードしますが、署名は検証しません。golang-jwt/jwt/v5 の文書でも、返されたトークンの Token.Valid は false と明記されています。

ユーザーが指定したトークンのクレームを署名検証なしに認証や認可へ使うと、攻撃者は sub、ロール、権限などを偽造できます。クレームを信頼する前に独立した署名検証を行う、限定的な解析処理とは区別する必要があります。

未検証の解析を使う条件

ParseUnverified で読み取ったヘッダーとクレームは、検証済みの身元情報ではありません。以下のどちらのパーサー生成方法でも、署名を検証せずにデータを読み取ります。

go
parser := jwt.NewParser()
parser.ParseUnverified(rawToken, jwt.MapClaims{})

parser = &jwt.Parser{}
parser.ParseUnverified(rawToken, jwt.MapClaims{})

署名検証が既に済んでいるか、データを信頼する前に別途検証する場合だけ使ってください。以下の検証例は github.com/golang-jwt/jwt/v5 用です。以前のメジャーバージョンや github.com/dgrijalva/jwt-go は、サポート状況と API を別途確認してください。

想定される影響

  • 偽造した識別子、ロール、権限によって認証や認可を回避されるおそれがあります。
  • 未検証の alg や kid を信頼すると、アルゴリズムや鍵の選択を攻撃者に操作される可能性があります。
  • golang-jwt/jwt/v5 の 5.2.2 未満は CVE-2025-30204 の影響を受け、細工されたトークンの解析で過大なメモリ割り当てが発生する可能性があります。

対処方法

  • ParseUnverified を jwt.Parse または jwt.ParseWithClaims に置き換えてください。エラーがなく、トークンが nil ではなく、Valid が true の場合だけクレームを信頼してください。
  • jwt.WithValidMethods で許可する署名アルゴリズムを制限してください。検証鍵は信頼済みの設定や検証済みの JWKS から取得し、未検証の alg や kid だけで鍵やアルゴリズムを決めないでください。
  • アプリケーションが依存するクレームを明示的に検証してください。v5 では jwt.WithExpirationRequired、jwt.WithIssuer、jwt.WithAudience、jwt.WithAllAudiences を使えます。jwt.WithLeeway は明確な時刻ずれの許容方針がある場合だけ使ってください。
  • ParseUnverified は、署名検証が完了しているか、クレームを信頼して使う前に独立した検証を行う場合だけ使ってください。
  • 5.3.1 以降の、維持管理されている v5 リリースを使ってください。CVE-2025-30204 の最小修正バージョンは 5.2.2 ですが、その最小値へ長期固定せず、セキュリティ更新を適用してください。

例

変更前

go
func unverifiedClaims(r *http.Request) (jwt.MapClaims, error) {
    rawToken := r.FormValue("token")
    token, _, err := jwt.NewParser().ParseUnverified(rawToken, jwt.MapClaims{})
    if err != nil {
        return nil, err
    }
    return token.Claims.(jwt.MapClaims), nil
}

rawToken はユーザー要求から取得しますが、ParseUnverified は署名を検証しません。返されたクレームを信頼すると、値を偽造されるおそれがあります。

変更後

go
func verifiedToken(rawToken string, verificationKey *rsa.PublicKey) (*jwt.Token, error) {
    token, err := jwt.Parse(
        rawToken,
        func(token *jwt.Token) (any, error) {
            // 発行者に対応する信頼済み設定、または
            // 検証済みの JWKS キャッシュから鍵を選択
            return verificationKey, nil
        },
        jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()}),
        jwt.WithExpirationRequired(),
        jwt.WithIssuer("https://issuer.example"),
        jwt.WithAudience("api.example"),
    )
    if err != nil {
        return nil, fmt.Errorf("JWT verification failed: %w", err)
    }
    if token == nil || !token.Valid {
        return nil, errors.New("invalid JWT")
    }
    return token, nil
}

許可するアルゴリズムと必須クレームをパーサーで固定し、信頼済みの公開鍵で署名を検証して、有効なトークンだけを返します。独自のクレーム構造体が必要なら、同じオプションと鍵の方針を jwt.ParseWithClaims に適用してください。

参考資料