{"record":{"id":"63291c1c25e0753b","repo":"XTLS/Xray-core","slug":"asset-path-must-stay-in-asset-directory","errorCode":null,"errorMessage":"asset path must stay in asset directory","messagePattern":"asset path must stay in asset directory","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"common/platform/filesystem/file.go","lineNumber":57,"sourceCode":"\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn NewFileReader(path)\n}\n\nfunc StatAsset(file string) (os.FileInfo, error) {\n\t_, info, err := getAssetFileLocation(file)\n\treturn info, err\n}\n\nfunc ResolveAsset(file string) (string, error) {\n\tpath, _, err := getAssetFileLocation(file)\n\treturn path, err\n}\n\nfunc getAssetFileLocation(file string) (string, os.FileInfo, error) {\n\tif !filepath.IsLocal(file) || file == \".\" {\n\t\treturn \"\", nil, errors.New(\"asset path must stay in asset directory\")\n\t}\n\tlocal, err := filepath.Localize(file)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tpath := platform.GetAssetLocation(local)\n\tinfo, err := os.Stat(path)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tif !info.Mode().IsRegular() {\n\t\treturn \"\", nil, errors.New(\"asset is not a regular file\")\n\t}\n\treturn path, info, nil\n}\n\nfunc ReadCert(file string) ([]byte, error) {\n\tif filepath.IsAbs(file) {","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/XTLS/Xray-core/blob/7d214f8b094f75322fa3990f8aadad1c912f24f5/common/platform/filesystem/file.go#L39-L75","documentation":"Returned by getAssetFileLocation (backing ResolveAsset/StatAsset) when the requested relative path fails filepath.IsLocal or is exactly '.'. This is a path-traversal guard: asset reads must stay inside the platform asset directory, so absolute paths, '..' escapes, Windows drive letters, or empty/'.' inputs are rejected.","triggerScenarios":"Calling ResolveAsset/StatAsset with a path containing '..' (e.g. '../xray.geoip.dat'), an absolute path ('/etc/passwd'), a UNC/drive path on Windows, or the literal '.'.","commonSituations":"Geodata/rule asset filenames built from untrusted config input, config values written as absolute paths when the API expects a bare filename, or sanitization bugs that let user-supplied names traverse directories.","solutions":["Pass only a bare filename (e.g. 'geosite.dat'), not an absolute or parent-relative path","Sanitize any user-controlled asset name with filepath.Base() before calling the API","If the asset truly lives elsewhere, read it directly with filesystem.ReadFile instead of the asset resolver"],"exampleFix":"// before\npath, err := filesystem.ResolveAsset(userProvidedName) // \"../secret.dat\"\n\n// after\npath, err := filesystem.ResolveAsset(filepath.Base(userProvidedName))","handlingStrategy":"validation","validationCode":"name := filepath.Base(userInput)\nif !filepath.IsLocal(name) || name == \".\" || name == string(filepath.Separator) {\n    return errors.New(\"asset name must be a plain filename\")\n}","typeGuard":"func isSafeAssetName(name string) bool { return filepath.IsLocal(name) && name != \".\" && filepath.Base(name) == name }","tryCatchPattern":null,"preventionTips":["Always sanitize external filenames with filepath.Base","Treat this error as a security signal - log the offending path"],"tags":["security","path-traversal","filesystem","validation"],"backgroundTag":null,"analyzedSha":"7d214f8b094f75322fa3990f8aadad1c912f24f5","analyzedAt":"2026-08-15T14:26:24.325Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}