{"record":{"id":"83affebd38df338a","repo":"flipped-aurora/gin-vue-admin","slug":"path-root-is-empty","errorCode":null,"errorMessage":"path root is empty","messagePattern":"path root is empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/utils/plugin_security.go","lineNumber":24,"sourceCode":"\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nvar pluginNamePattern = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)\n\n// ValidatePluginName restricts plugin names to lowercase ASCII Go identifiers.\nfunc ValidatePluginName(name string) error {\n\tif !pluginNamePattern.MatchString(name) || token.IsKeyword(name) {\n\t\treturn errors.New(\"invalid plugin name\")\n\t}\n\treturn nil\n}\n\n// JoinWithinRoot joins path elements while ensuring the result stays below root.\nfunc JoinWithinRoot(root string, elems ...string) (string, error) {\n\tif strings.TrimSpace(root) == \"\" {\n\t\treturn \"\", errors.New(\"path root is empty\")\n\t}\n\trootAbs, err := filepath.Abs(root)\n\tif err != nil {\n\t\treturn \"\", errors.New(\"failed to resolve path root\")\n\t}\n\tfor _, elem := range elems {\n\t\tif filepath.IsAbs(elem) || filepath.VolumeName(elem) != \"\" || strings.HasPrefix(elem, \"/\") || strings.HasPrefix(elem, `\\`) {\n\t\t\treturn \"\", errors.New(\"path escapes root\")\n\t\t}\n\t}\n\tparts := append([]string{rootAbs}, elems...)\n\ttargetAbs, err := filepath.Abs(filepath.Join(parts...))\n\tif err != nil {\n\t\treturn \"\", errors.New(\"failed to resolve target path\")\n\t}\n\trel, err := filepath.Rel(rootAbs, targetAbs)\n\tif err != nil || filepath.IsAbs(rel) || rel == \"..\" || strings.HasPrefix(rel, \"..\"+string(filepath.Separator)) {\n\t\treturn \"\", errors.New(\"path escapes root\")","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/flipped-aurora/gin-vue-admin/blob/3136500ef380842b0eb6c4daa87c3f8a47fcf9e0/server/utils/plugin_security.go#L6-L42","documentation":"JoinWithinRoot safely joins path elements under a root directory, guaranteeing the result stays below the root. It requires a non-empty root; an empty or whitespace-only root returns 'path root is empty' because no containment base exists.","triggerScenarios":"Calling JoinWithinRoot(\"\", elem...) or with a root of only spaces/tabs — typically when the root comes from an unset config value or an empty variable.","commonSituations":"Config key for a plugin/base directory left blank in yaml; environment variable not set so the root resolves to empty string; refactored code passing an unassigned variable; caught in unit tests (TestJoinWithinRoot).","solutions":["Provide a concrete root directory before calling JoinWithinRoot (e.g. filepath.Join(baseDir, pluginRoot)).","Set the missing config value / environment variable that supplies the root and reload config.","Add an early check or default: if root == \"\" fall back to a known-safe base directory."],"exampleFix":"// before\nroot := os.Getenv(\"PLUGIN_DIR\") // \"\"\np, err := utils.JoinWithinRoot(root, name) // path root is empty\n// after\nroot := os.Getenv(\"PLUGIN_DIR\")\nif root == \"\" {\n    root = \"./plugin\"\n}\np, err := utils.JoinWithinRoot(root, name) // ok","handlingStrategy":"validation","validationCode":"if strings.TrimSpace(root) == \"\" {\n    return errors.New(\"root directory must be configured before joining paths\")\n}","typeGuard":null,"tryCatchPattern":"p, err := utils.JoinWithinRoot(root, elems...)\nif err != nil {\n    // empty root or failed resolution: fail the operation, never fall back to raw join\n    return \"\", fmt.Errorf(\"safe join failed: %w\", err)\n}","preventionTips":["Give plugin/base directories a non-empty default in config and validate at startup","Fail fast on missing environment variables that feed root paths","Prefer JoinWithinRoot over filepath.Join for any path built from user/plugin-supplied names"],"tags":["filesystem","security","go","path-traversal"],"backgroundTag":"empty-path-base","analyzedSha":"3136500ef380842b0eb6c4daa87c3f8a47fcf9e0","analyzedAt":"2026-08-31T13:50:02.721Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}