{"record":{"id":"fed836bcf9a3f3ab","repo":"router-for-me/CLIProxyAPI","slug":"invalid-auth-file-name","errorCode":null,"errorMessage":"invalid auth file name","messagePattern":"invalid auth file name","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/pluginhost/auth_callbacks.go","lineNumber":265,"sourceCode":"\t\tif os.IsNotExist(errRead) {\n\t\t\treturn nil, nil, fmt.Errorf(\"auth file not found for auth_index %s\", authIndex)\n\t\t}\n\t\treturn nil, nil, fmt.Errorf(\"failed to read auth file: %w\", errRead)\n\t}\n\tif len(bytesTrimSpace(data)) == 0 {\n\t\treturn nil, nil, fmt.Errorf(\"auth file is empty for auth_index %s\", authIndex)\n\t}\n\tvar metadata map[string]any\n\tif errUnmarshal := json.Unmarshal(data, &metadata); errUnmarshal != nil {\n\t\treturn nil, nil, fmt.Errorf(\"invalid auth file for auth_index %s: %w\", authIndex, errUnmarshal)\n\t}\n\treturn auth, data, nil\n}\n\nfunc validateHostAuthSaveRequest(req pluginapi.HostAuthSaveRequest) (string, []byte, error) {\n\tname := strings.TrimSpace(req.Name)\n\tif isUnsafeAuthFileName(name) {\n\t\treturn \"\", nil, fmt.Errorf(\"invalid auth file name\")\n\t}\n\tif !strings.HasSuffix(strings.ToLower(name), \".json\") {\n\t\treturn \"\", nil, fmt.Errorf(\"auth file name must end with .json\")\n\t}\n\trawJSON := bytesTrimSpace(req.JSON)\n\tif len(rawJSON) == 0 {\n\t\treturn \"\", nil, fmt.Errorf(\"json is required\")\n\t}\n\tvar metadata map[string]any\n\tif errUnmarshal := json.Unmarshal(rawJSON, &metadata); errUnmarshal != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"invalid auth json: %w\", errUnmarshal)\n\t}\n\treturn filepath.Base(name), rawJSON, nil\n}\n\nfunc (h *Host) saveAuthFile(ctx context.Context, name string, data []byte) (string, error) {\n\tauthDir := h.resolvedAuthDir()\n\tif authDir == \"\" {","sourceCodeStart":247,"sourceCodeEnd":283,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginhost/auth_callbacks.go#L247-L283","documentation":"Thrown by validateHostAuthSaveRequest when the requested save name fails isUnsafeAuthFileName. That guard rejects names with path separators, traversal segments, reserved device names, and similar unsafe components, so a plugin cannot write outside the auth directory.","triggerScenarios":"A plugin calls the host auth-save callback with a name like ../evil.json, /etc/cron.d/x.json, .json with backslashes, or another pattern the safety check rejects.","commonSituations":"Plugin bug building the filename from user input or provider IDs containing slashes; a malicious or misbehaving plugin attempting path traversal; Windows-style paths submitted on a name field.","solutions":["Sanitize the name to a plain base filename before calling save (strip directories)","Use an allow-list pattern such as ^[A-Za-z0-9._-]+$ plus the .json suffix","If the plugin is third-party, verify its save requests and report the non-compliant plugin"],"exampleFix":"// before\nreq := pluginapi.HostAuthSaveRequest{Name: filepath.Join(dir, name)} // rejected\n\n// after\nreq := pluginapi.HostAuthSaveRequest{Name: filepath.Base(name)} // plain base name","handlingStrategy":"validation","validationCode":"var safeNameRe = regexp.MustCompile(`^[A-Za-z0-9._-]+\\.json$`)\nif !safeNameRe.MatchString(name) {\n    name = filepath.Base(name) // or reject outright\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always derive save names from filepath.Base plus a strict allow-list","Never interpolate user input into auth file names"],"tags":["go","pluginhost","auth","security","validation","path-traversal"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}