{"record":{"id":"5a55505e71525943","repo":"Tencent/WeKnora","slug":"external-user-id-contains-invalid-characters","errorCode":null,"errorMessage":"external user id contains invalid characters","messagePattern":"external user id contains invalid characters","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/middleware/auth.go","lineNumber":669,"sourceCode":"\tsub, _ := claims[\"sub\"].(string)\n\tsub = strings.TrimSpace(sub)\n\tif sub == \"\" {\n\t\treturn \"\", errors.New(\"missing subject\")\n\t}\n\treturn sub, nil\n}\n\nfunc validateExternalUserID(id string) error {\n\tid = strings.TrimSpace(id)\n\tif id == \"\" {\n\t\treturn errors.New(\"empty external user id\")\n\t}\n\tif len(id) > maxExternalUserIDLen {\n\t\treturn fmt.Errorf(\"external user id too long (max %d)\", maxExternalUserIDLen)\n\t}\n\tfor _, r := range id {\n\t\tif r < 0x20 || r == 0x7f {\n\t\t\treturn errors.New(\"external user id contains invalid characters\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc apiPrincipalAuthErrorMessage(err error) string {\n\tswitch {\n\tcase errors.Is(err, errMissingDirectHeader):\n\t\treturn \"Unauthorized: missing external user id header\"\n\tcase errors.Is(err, errInvalidExternalUserID):\n\t\treturn \"Unauthorized: invalid external user id\"\n\tcase errors.Is(err, errInvalidExternalUserToken):\n\t\treturn \"Unauthorized: invalid external user token\"\n\tdefault:\n\t\treturn \"Unauthorized: invalid external user token\"\n\t}\n}\n","sourceCodeStart":651,"sourceCodeEnd":687,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/middleware/auth.go#L651-L687","documentation":"validateExternalUserID enforces a character whitelist on external user IDs: any rune below 0x20 (control characters) or 0x7f (DEL) causes rejection. This prevents control characters from corrupting storage, logs, and downstream lookups.","triggerScenarios":"resolveAPIPrincipal path where the external user ID (from the JWT sub or another source) contains control bytes — e.g. embedded newlines, tabs (allowed? no: \\t=0x09 < 0x20 is rejected), null bytes, or ANSI escape sequences.","commonSituations":"IDs copy-pasted from terminals or binary sources containing escape sequences; payloads built by concatenating raw bytes; malicious clients probing for log-injection or storage anomalies.","solutions":["Sanitize the ID at its origin: strip or reject control characters before the token/parameter is produced.","Have the client send the ID percent-encoded or as a clean string and decode properly server-side.","If legacy IDs contain control chars, migrate them to sanitized equivalents in the external user store.","For debugging, hex-encode the failing ID to see exactly which control rune is present."],"exampleFix":"// before: passing raw header value through\nid := c.GetHeader(\"X-External-User-Id\")\n// after: strip control characters before validation\nid := strings.Map(func(r rune) rune {\n    if r < 0x20 || r == 0x7f {\n        return -1\n    }\n    return r\n}, c.GetHeader(\"X-External-User-Id\"))","handlingStrategy":"validation","validationCode":"func sanitizeID(id string) string {\n    return strings.Map(func(r rune) rune {\n        if r < 0x20 || r == 0x7f { return -1 }\n        return r\n    }, strings.TrimSpace(id))\n}\n// apply before sending the ID","typeGuard":"func isPrintableID(id string) bool {\n    for _, r := range id {\n        if r < 0x20 || r == 0x7f { return false }\n    }\n    return id != \"\"\n}","tryCatchPattern":null,"preventionTips":["Whitelist-encode IDs (e.g. [A-Za-z0-9._-]) at generation time.","Never build IDs by concatenating raw bytes or binary output.","Log failing IDs hex-encoded to identify the offending rune quickly."],"tags":["auth","validation","input-sanitization"],"backgroundTag":"invalid-identifier-characters","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}