Validate performs comprehensive security checks on a file path. This is the core validation method that performs all security checks in the correct order to prevent TOCTOU attacks and other vulnerabilities. Validation sequence: 1. Path traversal check on original path 2. Symlink resolution to real
(path string)
| 181 | // |
| 182 | // Validate performs comprehensive security checks on a file path |
| 183 | func (v *SecurityValidator) Validate(path string) error { |
| 184 | // 1. Check for path traversal attempts BEFORE resolving symlinks |
| 185 | // This is critical - we must check the original path for ".." sequences |
| 186 | // before they get normalized away by EvalSymlinks |
| 187 | if err := v.checkPathTraversal(path); err != nil { |
| 188 | return err |
| 189 | } |
| 190 | |
| 191 | // 2. Resolve symlinks and get real path |
| 192 | realPath, err := filepath.EvalSymlinks(path) |
| 193 | if err != nil { |
| 194 | // Check if it's a symlink error vs file not found |
| 195 | if _, statErr := os.Lstat(path); statErr == nil { |
| 196 | // File exists but symlink resolution failed |
| 197 | return fmt.Errorf("invalid file path (broken symlink): %w", err) |
| 198 | } |
| 199 | return fmt.Errorf("invalid file path: %w", err) |
| 200 | } |
| 201 | |
| 202 | // 3. Check if symlink points to different location (potential security issue) |
| 203 | if !v.AllowSymlinks { |
| 204 | // Use Lstat to detect symlinks without following them |
| 205 | linkInfo, err := os.Lstat(path) |
| 206 | if err != nil { |
| 207 | // If Lstat fails, continue with regular validation |
| 208 | // The file might not exist or we don't have permissions |
| 209 | } else if linkInfo.Mode()&os.ModeSymlink != 0 { |
| 210 | // This is an actual symlink (not just path normalization) |
| 211 | return fmt.Errorf("symlinks are not allowed for security reasons: %s -> %s", path, realPath) |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | // 4. Get file info |
| 216 | info, err := os.Stat(realPath) |
| 217 | if err != nil { |
| 218 | return fmt.Errorf("cannot access file: %w", err) |
| 219 | } |
| 220 | |
| 221 | // 5. Check it's a regular file (not directory, device, socket, etc.) |
| 222 | if !info.Mode().IsRegular() { |
| 223 | return fmt.Errorf("not a regular file: %s (mode: %s)", path, info.Mode()) |
| 224 | } |
| 225 | |
| 226 | // 6. Check file size |
| 227 | if info.Size() > v.MaxFileSize { |
| 228 | return fmt.Errorf("file too large: %d bytes (max %d bytes)", info.Size(), v.MaxFileSize) |
| 229 | } |
| 230 | |
| 231 | // 7. Validate file extension |
| 232 | if err := v.validateExtension(realPath); err != nil { |
| 233 | return err |
| 234 | } |
| 235 | |
| 236 | // 8. Test read permissions |
| 237 | // G304: realPath is fully validated above via EvalSymlinks and security checks |
| 238 | file, err := os.Open(realPath) // #nosec G304 |
| 239 | if err != nil { |
| 240 | return fmt.Errorf("cannot open file: %w", err) |