package runtimelogs import ( "os" "path/filepath" "regexp" "strings" ) var filenamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9_.-]*\.log$`) // ValidateFilename checks basename allowlist for runtime log files. func ValidateFilename(filename string) error { name := strings.TrimSpace(filename) if name == "" || len(name) > 128 { return ErrInvalidFilename } if name != filepath.Base(name) { return ErrInvalidFilename } if strings.Contains(name, "..") { return ErrInvalidFilename } if !filenamePattern.MatchString(name) { return ErrInvalidFilename } return nil } // ResolveLogPath maps a validated basename to an absolute path under root. func ResolveLogPath(root, filename string) (string, error) { if err := ValidateFilename(filename); err != nil { return "", err } rootAbs, err := filepath.Abs(root) if err != nil { return "", err } rootAbs = filepath.Clean(rootAbs) candidate := filepath.Join(rootAbs, filename) resolved, err := filepath.EvalSymlinks(candidate) if err != nil { if os.IsNotExist(err) { resolved = filepath.Clean(candidate) } else { return "", err } } resolved = filepath.Clean(resolved) if !pathUnderRoot(resolved, rootAbs) { return "", ErrInvalidFilename } if st, err := os.Lstat(resolved); err == nil { if st.IsDir() { return "", ErrNotAFile } } return resolved, nil } func pathUnderRoot(path, root string) bool { path = filepath.Clean(path) root = filepath.Clean(root) if path == root { return false } sep := string(os.PathSeparator) return strings.HasPrefix(path+sep, root+sep) }