{"record":{"id":"0ca23271246cbd96","repo":"Tencent/WeKnora","slug":"invalid-file-name-w","errorCode":null,"errorMessage":"invalid file name: %w","messagePattern":"invalid file name: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/application/service/file/local.go","lineNumber":224,"sourceCode":"\t\treturn \"\", fmt.Errorf(\"failed to copy file content: %w\", err)\n\t}\n\n\trelPath, _ := filepath.Rel(s.baseDir, dstPath)\n\tnewPath := localScheme + filepath.ToSlash(relPath)\n\tlogger.Infof(ctx, \"Copied local file %s to %s\", srcPath, newPath)\n\treturn newPath, nil\n}\n\n// SaveBytes saves bytes data to a file and returns the file path\n// temp parameter is ignored for local storage (no auto-expiration support)\n// fileName 仅允许安全文件名，禁止路径遍历（如 ../../）\nfunc (s *localFileService) SaveBytes(ctx context.Context, data []byte, tenantID uint64, fileName string, temp bool) (string, error) {\n\tlogger.Infof(ctx, \"Saving bytes data: fileName=%s, size=%d, tenantID=%d, temp=%v\", fileName, len(data), tenantID, temp)\n\n\tsafeName, err := secutils.SafeFileName(fileName)\n\tif err != nil {\n\t\tlogger.Errorf(ctx, \"Invalid fileName for SaveBytes: %v\", err)\n\t\treturn \"\", fmt.Errorf(\"invalid file name: %w\", err)\n\t}\n\n\t// Create storage directory with tenant ID\n\tdir := filepath.Join(s.baseDir, fmt.Sprintf(\"%d\", tenantID), \"exports\")\n\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\tlogger.Errorf(ctx, \"Failed to create directory: %v\", err)\n\t\treturn \"\", fmt.Errorf(\"failed to create directory: %w\", err)\n\t}\n\n\t// Generate unique filename using timestamp\n\text := filepath.Ext(safeName)\n\tbaseName := safeName[:len(safeName)-len(ext)]\n\tuniqueFileName := fmt.Sprintf(\"%s_%d%s\", baseName, time.Now().UnixNano(), ext)\n\tfilePath := filepath.Join(dir, uniqueFileName)\n\n\t// Write data to file\n\tif err := os.WriteFile(filePath, data, 0o644); err != nil {\n\t\tlogger.Errorf(ctx, \"Failed to write file: %v\", err)","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/application/service/file/local.go#L206-L242","documentation":"SaveBytes validates the caller-supplied fileName with secutils.SafeFileName before storing bytes under <baseDir>/<tenantID>/exports. When the name contains path separators, traversal sequences (../), illegal characters, or is empty, SafeFileName returns an error and SaveBytes wraps it as 'invalid file name'.","triggerScenarios":"Calling SaveBytes with fileName containing '/' or '\\\\', '..' path segments, control/illegal characters, an empty string, or a name that fails the sanitizer's allowlist (e.g. reserved characters on the target OS).","commonSituations":"Storing user-uploaded filenames verbatim (browser sends odd names like 'report/../x.pdf' or a filename with a full path from Windows clients); test harnesses passing empty names; i18n filenames with characters rejected by the sanitizer.","solutions":["Sanitize the filename before calling SaveBytes: strip directory components (filepath.Base) and replace disallowed characters with '_' or a generated UUID.","Never build storage names from raw user input — pass a server-generated name (UUID/timestamp + safe extension) and keep the original only as metadata.","Inspect the wrapped error to see which rule SafeFileName enforced and adjust the input accordingly.","If a valid name is being rejected, compare it against the sanitizer's allowlist in secutils.SafeFileName and normalize (NFC, strip controls) beforehand."],"exampleFix":"// before\nnewPath, err := svc.SaveBytes(ctx, data, tenantID, rawUserFilename, false)\n// after\nsafe := filepath.Base(rawUserFilename)\nsafe = strings.Map(func(r rune) rune {\n    if r < 32 || strings.ContainsRune(\"/\\\\:*?\\\"<>|\", r) {\n        return '_'\n    }\n    return r\n}, safe)\nif safe == \"\" || safe == \".\" || safe == \"..\" {\n    safe = fmt.Sprintf(\"upload-%d\", time.Now().UnixNano())\n}\nnewPath, err := svc.SaveBytes(ctx, data, tenantID, safe, false)","handlingStrategy":"validation","validationCode":"func validFileName(name string) bool {\n    if name == \"\" || len(name) > 255 {\n        return false\n    }\n    if name != filepath.Base(name) || name == \".\" || name == \"..\" {\n        return false\n    }\n    return !strings.ContainsAny(name, \"/\\\\:*?\\\"<>|\") && !strings.ContainsFunc(name, func(r rune) bool { return r < 32 || r == 127 })\n}\n// call validFileName(fileName) before SaveBytes","typeGuard":"func isSafeFileName(s string) bool {\n    return validFileName(s) // reuses validation above\n}","tryCatchPattern":"newPath, err := svc.SaveBytes(ctx, data, tenantID, fileName, temp)\nif err != nil {\n    if strings.HasPrefix(err.Error(), \"invalid file name\") {\n        // fall back to a server-generated name\n        fileName = fmt.Sprintf(\"upload-%d%s\", time.Now().UnixNano(), filepath.Ext(fileName))\n        newPath, err = svc.SaveBytes(ctx, data, tenantID, fileName, temp)\n    }\n    if err != nil {\n        return fmt.Errorf(\"save failed: %w\", err)\n    }\n}","preventionTips":["Never store raw user-supplied filenames; generate server-side names (UUID/timestamp) and keep originals as metadata only.","Apply filepath.Base and character whitelisting to any filename crossing a trust boundary.","Handle multipart upload metadata: browsers may send full client paths or exotic Unicode names.","Add unit tests for SaveBytes with traversal ('../'), empty, and illegal-character inputs."],"tags":["validation","filename","path-traversal","security"],"backgroundTag":"invalid-file-name","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}