{"record":{"id":"28c13e04b10f803b","repo":"Billionmail/BillionMail","slug":"empty-token-string","errorCode":null,"errorMessage":"empty token string","messagePattern":"empty token string","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"core/internal/service/batch_mail/jwt.go","lineNumber":146,"sourceCode":"\tclaims := UnsubscribeClaims{\n\t\tEmail:      email,\n\t\tTemplateId: templateId,\n\t\tTaskId:     taskId,\n\t\tGroupId:    GroupId,\n\t\tRegisteredClaims: jwt.RegisteredClaims{\n\t\t\tExpiresAt: jwt.NewNumericDate(time.Now().Add(cfg.expiry)),\n\t\t\tIssuedAt:  jwt.NewNumericDate(time.Now()),\n\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{}","sourceCodeStart":128,"sourceCodeEnd":164,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/batch_mail/jwt.go#L128-L164","documentation":"ParseUnsubscribeJWT validates and parses the signed unsubscribe JWT. Before touching the token it rejects an empty tokenString with errors.New(\"empty token string\"). This is an input-validation guard so the JWT library never receives a blank string.","triggerScenarios":"Unsubscribe or UnsubscribeNew receives a request whose unsubscribe token query/form parameter is missing or blank, then calls ParseUnsubscribeJWT with \"\".","commonSituations":"Users clicking an unsubscribe link truncated by an email client; links generated by an old template before tokens were added; forwarding/copy-pasting the link without the query string; proxy stripping unknown query params.","solutions":["Return HTTP 400 with a clear 'invalid unsubscribe link' page instead of a 500","Regenerate the unsubscribe link/token from the contact record","Audit email templates to ensure the token is always included in unsubscribe URLs","Guard call sites: check the token parameter for non-empty before parsing"],"exampleFix":"// before\ntokenStr := r.Get(\"token\").String()\nclaims, err := ParseUnsubscribeJWT(tokenStr)\n// after\ntokenStr := r.Get(\"token\").String()\nif tokenStr == \"\" {\n    r.Response.WriteStatus(400, \"invalid unsubscribe link: missing token\")\n    return\n}\nclaims, err := ParseUnsubscribeJWT(tokenStr)","handlingStrategy":"validation","validationCode":"tokenStr := r.Get(\"token\").String()\nif tokenStr == \"\" {\n    return errors.New(\"missing unsubscribe token\")\n}","typeGuard":"func hasUnsubscribeToken(params map[string]string) bool {\n    return params[\"token\"] != \"\"\n}","tryCatchPattern":"claims, err := ParseUnsubscribeJWT(tokenStr)\nif err != nil {\n    switch {\n    case err.Error() == \"empty token string\":\n        // respond 400 invalid link\n    default:\n        // expired/invalid signature: respond 401 and offer resubscribe page\n    }\n    return\n}","preventionTips":["Validate the token param exists in the handler before parsing","Generate unsubscribe URLs with tokens always present; template-test them","Render a friendly error page for truncated/missing tokens","Log empty-token hits to detect email-client link truncation issues"],"tags":["jwt","validation","input"],"backgroundTag":"empty-jwt-token","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"}