{"record":{"id":"8fc70a75d18d2e10","repo":"netbirdio/netbird","slug":"id-s-is-not-valid","errorCode":null,"errorMessage":"id '%s' is not valid","messagePattern":"id '(.+?)' is not valid","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/android/profile_manager.go","lineNumber":181,"sourceCode":"\t// Use ServiceManager (creates profile in profiles/ directory)\n\tprofile, err := pm.serviceMgr.AddProfile(profileName, androidUsername)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to add profile: %w\", err)\n\t}\n\n\tlog.Infof(\"created new profile: %s\", profile.ID)\n\treturn nil\n}\n\n// LogoutProfile logs out from a profile (clears authentication)\nfunc (pm *ProfileManager) LogoutProfile(id string) error {\n\tconfigPath, err := pm.getProfileConfigPath(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {\n\t\treturn fmt.Errorf(\"id '%s' is not valid\", id)\n\t}\n\n\t// Check if profile exists\n\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\treturn fmt.Errorf(\"profile '%s' does not exist\", id)\n\t}\n\n\t// Read current config using internal profilemanager\n\tconfig, err := profilemanager.ReadConfig(configPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read profile config: %w\", err)\n\t}\n\n\t// Clear authentication by removing private key and SSH key\n\tconfig.PrivateKey = \"\"\n\tconfig.SSHKey = \"\"\n\n\t// Save config using internal profilemanager","sourceCodeStart":163,"sourceCodeEnd":199,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/client/android/profile_manager.go#L163-L199","documentation":"LogoutProfile re-validates the profile id with IsValidProfileFilenameStem and rejects ids that are empty, longer than 64 characters, contain '/', '\\', '..', or any character outside letters/digits/underscore/hyphen — a path-traversal guard before the config path is used. Note that in the current code this exact check is shadowed: getProfileConfigPath runs the same validation first (line 175), so an invalid id normally surfaces earlier as 'id %q is not valid'.","triggerScenarios":"LogoutProfile called with a display name, a filename like \"work.json\" (the dot is not a legal stem character), or any path-like/free-form string instead of the raw ID returned by ListProfiles.","commonSituations":"Java/Kotlin UI passing the profile Name field instead of ID, hardcoded ids containing dots or spaces, ids from an older app version that predated sanitization.","solutions":["Pass only ids obtained from ListProfiles or GetActiveProfile","Use \"default\" for the default profile","Reject path-like free-form input in the UI layer before calling"],"exampleFix":"// before\npm.LogoutProfile(profile.Name) // display name; may contain dots/spaces → rejected\n\n// after\npm.LogoutProfile(profile.ID) // stem-safe id exactly as returned by ListProfiles","handlingStrategy":"validation","validationCode":"// Reject non-stem ids before calling LogoutProfile (mirrors IsValidProfileFilenameStem)\nif !isValidProfileID(id) {\n\t// do not call; ids must come from ListProfiles/GetActiveProfile\n}","typeGuard":"func isValidProfileID(id string) bool {\n\tif id == \"\" || len(id) > 64 {\n\t\treturn false\n\t}\n\tif strings.ContainsAny(id, `/\") || strings.Contains(id, \"..\") {\n\t\treturn false\n\t}\n\tif filepath.Base(id) != id {\n\t\treturn false\n\t}\n\tfor _, r := range id {\n\t\tif !(unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-') {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}","tryCatchPattern":"if err := pm.LogoutProfile(id); err != nil {\n\tif strings.Contains(err.Error(), \"is not valid\") {\n\t\t// caller bug: a display name or path was passed instead of a profile ID\n\t} else if strings.Contains(err.Error(), \"does not exist\") {\n\t\t// stale entry; refresh the list\n\t}\n}","preventionTips":["Never derive ids from user input; use ListProfiles/GetActiveProfile values","Use \"default\" for the default profile","Remember dot is not a legal stem character — filenames like work.json must be passed as work"],"tags":["android","go","profile","validation","path-traversal"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}