{"record":{"id":"2bdf2c993f5bbccf","repo":"Billionmail/BillionMail","slug":"unexpected-signing-method-v","errorCode":null,"errorMessage":"unexpected signing method: %v","messagePattern":"unexpected signing method: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/batch_mail/jwt.go","lineNumber":154,"sourceCode":"\t\t},\n\t}\n\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)\n\treturn token.SignedString([]byte(cfg.secret))\n}\n\n// ParseUnsubscribeJWT 解析退订JWT\nfunc ParseUnsubscribeJWT(tokenString string) (*UnsubscribeClaims, error) {\n\tif tokenString == \"\" {\n\t\treturn nil, errors.New(\"empty token string\")\n\t}\n\n\tcfg := getConfig()\n\n\ttoken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {\n\t\t// Validate signing method\n\t\tif _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {\n\t\t\treturn nil, fmt.Errorf(\"unexpected signing method: %v\", token.Header[\"alg\"])\n\t\t}\n\t\treturn []byte(cfg.secret), nil\n\t})\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse JWT: %w\", err)\n\t}\n\n\tif claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {\n\t\tresult := &UnsubscribeClaims{}\n\n\t\t// Extract email\n\t\tif email, ok := claims[\"email\"].(string); ok {\n\t\t\tresult.Email = email\n\t\t} else {\n\t\t\treturn nil, errors.New(\"JWT missing email claim\")\n\t\t}\n","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/batch_mail/jwt.go#L136-L172","documentation":"jwt.Parse rejects the token because its 'alg' header is not an HMAC method (HS256/HS384/HS512). This repo's keyfunc intentionally returns an error when the signing method type-asserts to anything other than *jwt.SigningMethodHMAC, a standard protection against algorithm-confusion attacks (e.g. RS256/none). The wrapped message 'failed to parse JWT' is then produced by the caller.","triggerScenarios":"A unsubscribe JWT was signed with a non-HMAC algorithm (e.g. RS256, ES256, or 'none'), or crafted/externally generated tokens hit ParseUnsubscribeJWT with a foreign alg header.","commonSituations":"Tokens minted by a different service using asymmetric keys; an attacker probing the endpoint with alg=none; a library upgrade where the default signing method changed; mixing the subscribe-confirm token flow with the unsubscribe token flow.","solutions":["Sign the token with jwt.SigningMethodHS256 (or another HMAC variant) matching the secret in getConfig().secret","Confirm the token producer uses the same batch_mail JWT flow (GenerateUnsubscribeJWT), not a different JWT issuer","If asymmetric signing is genuinely needed, extend the keyfunc to accept that method and return the corresponding public key"],"exampleFix":"// before\ntoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)\n// after\ntoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)","handlingStrategy":"validation","validationCode":"func isHMAC(token *jwt.Token) bool {\n\t_, ok := token.Method.(*jwt.SigningMethodHMAC)\n\treturn ok\n}\n// call jwt.Parse first and check err via errors.As\nvar algErr interface{ Error() string }\n_ = algErr","typeGuard":"func hasAlg(token *jwt.Token, want string) bool {\n\talg, ok := token.Header[\"alg\"].(string)\n\treturn ok && alg == want\n}","tryCatchPattern":null,"preventionTips":["Always sign with jwt.SigningMethodHS256 in this codebase","Keep the HMAC-only keyfunc — do not loosen it without adding a key-per-method allowlist","Add a unit test asserting tokens signed with RS256/none are rejected","Share one token-generation helper so producer and verifier agree on alg"],"tags":["jwt","go","security","auth"],"backgroundTag":"jwt-unexpected-signing-method","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}