netbirdio/netbird · error

id %q is not valid

Error message

id %q is not valid

What it means

getProfileConfigPath — backing the exported GetConfigPath and used by LogoutProfile, RemoveProfile, and the email lookup — rejects ids that fail IsValidProfileFilenameStem: empty, over 64 characters, containing '/', '\', '..', or characters outside letters/digits/'_'/'-'. This is the package's path-traversal guard: no filesystem path is built from an unvalidated id. "default" is valid and maps to configDir/netbird.cfg instead of profiles/default.json.

Source

Thrown at client/android/profile_manager.go:252

	}

	// The account file is this package's, not the ServiceManager's, so it must
	// go here. The default profile has a fixed filename, so a recreated one
	// would otherwise inherit the deleted profile's email as its login_hint.
	// Not fatal: the profile itself is gone.
	if err := removeProfileEmail(configPath); err != nil {
		log.Warnf("failed to remove stored account email for profile %s: %v", id, err)
	}

	log.Infof("removed profile: %s", id)
	return nil
}

// getProfileConfigPath returns the config file path for a profile
// This is needed for Android-specific path handling (netbird.cfg for default profile)
func (pm *ProfileManager) getProfileConfigPath(id string) (string, error) {
	if !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {
		return "", fmt.Errorf("id %q is not valid", id)
	}

	if id == profilemanager.DefaultProfileName {
		// Android uses netbird.cfg for default profile instead of default.json
		// Default profile is stored in root configDir, not in profiles/
		return filepath.Join(pm.configDir, defaultConfigFilename), nil
	}

	profilesDir := filepath.Join(pm.configDir, profilesSubdir)
	return filepath.Join(profilesDir, id+".json"), nil
}

// GetConfigPath returns the config file path for a given profile id
// Java should call this instead of constructing paths with Preferences.configFile()
func (pm *ProfileManager) GetConfigPath(id string) (string, error) {
	return pm.getProfileConfigPath(id)
}

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Pass ids exactly as returned by ListProfiles or GetActiveProfile
  2. Use "default" for the default profile
  3. Validate free-form input against the stem rules (letters, digits, '_', '-', max 64) before calling

Example fix

// before
path, err := pm.GetConfigPath("work.json") // dots are invalid in a stem

// after
path, err := pm.GetConfigPath("default")   // or an id obtained from ListProfiles
Defensive patterns

Strategy: validation

Validate before calling

// Gate every path API on a stem-valid id
if !isValidProfileID(id) {
	return errors.New("reject before calling GetConfigPath")
}

Type guard

func isValidProfileID(id string) bool {
	if id == "" || len(id) > 64 {
		return false
	}
	if strings.ContainsAny(id, `/") || strings.Contains(id, "..") {
		return false
	}
	if filepath.Base(id) != id {
		return false
	}
	for _, r := range id {
		if !(unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-') {
			return false
		}
	}
	return true
}

Try / catch

path, err := pm.GetConfigPath(id)
if err != nil && strings.Contains(err.Error(), "is not valid") {
	// caller bug: a display name/filename was passed instead of a profile id
}

Prevention

When it happens

Trigger: GetConfigPath(id) — or LogoutProfile/RemoveProfile internally — with a display name, a filename like "work.json" (the dot is not a legal stem character), or any free-form string containing spaces, slashes, or dots.

Common situations: Java layer migrating off Preferences.configFile() passing constructed filenames instead of profile ids; ids from older app versions with unsanitized names.

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/94c90c01044b5044. Report an issue: GitHub.