(filePath, resolveBase, securityBase string)
| 263 | } |
| 264 | |
| 265 | func resolveAndValidateLocalIncludePath(filePath, resolveBase, securityBase string) (string, error) { |
| 266 | if stripped, ok := strings.CutPrefix(filepath.ToSlash(filePath), "/"); ok { |
| 267 | if !strings.HasPrefix(stripped, constants.GithubDir) && !strings.HasPrefix(stripped, ".agents/") { |
| 268 | remoteLog.Printf("Security: Path not within .github or .agents: %s", filePath) |
| 269 | return "", fmt.Errorf("security: path %s must be within .github or .agents folder", filePath) |
| 270 | } |
| 271 | } |
| 272 | fullPath := filepath.Join(resolveBase, filePath) |
| 273 | normalizedSecurityBase := filepath.Clean(securityBase) |
| 274 | normalizedFullPath := filepath.Clean(fullPath) |
| 275 | relativePath, err := filepath.Rel(normalizedSecurityBase, normalizedFullPath) |
| 276 | if err != nil || relativePath == ".." || strings.HasPrefix(relativePath, ".."+string(filepath.Separator)) || filepath.IsAbs(relativePath) { |
| 277 | allowedFolder := filepath.Base(normalizedSecurityBase) |
| 278 | remoteLog.Printf("Security: Path escapes allowed folder: %s (resolves to: %s)", filePath, relativePath) |
| 279 | return "", fmt.Errorf("security: path %s must be within %s folder (resolves to: %s)", filePath, allowedFolder, relativePath) |
| 280 | } |
| 281 | |
| 282 | if _, err := os.Stat(fullPath); os.IsNotExist(err) { |
| 283 | remoteLog.Printf("Local file not found: %s", fullPath) |
| 284 | // Return a simple error that will be wrapped with source location by the caller |
| 285 | return "", fmt.Errorf("file not found: %s", fullPath) |
| 286 | } |
| 287 | remoteLog.Printf("Resolved to local file: %s", fullPath) |
| 288 | return fullPath, nil |
| 289 | } |
| 290 | |
| 291 | // IsWorkflowSpec checks if a path looks like a workflowspec (owner/repo/path[@ref]). |
| 292 | func IsWorkflowSpec(path string) bool { |
no test coverage detected