Security: Enforce strict validation for FFmpeg binary paths (#214)
This commit is contained in:
+52
-5
@@ -26,6 +26,49 @@ func decodeBase64(encoded string) (string, error) {
|
|||||||
return string(decoded), nil
|
return string(decoded), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateExecutable checks if the path points to a valid, safe executable
|
||||||
|
func ValidateExecutable(path string) error {
|
||||||
|
cleanedPath := filepath.Clean(path)
|
||||||
|
if cleanedPath == "" {
|
||||||
|
return fmt.Errorf("empty path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure path is absolute
|
||||||
|
if !filepath.IsAbs(cleanedPath) {
|
||||||
|
return fmt.Errorf("path must be absolute: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(cleanedPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stat file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
return fmt.Errorf("path is a directory: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check executable permission on Unix
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
if info.Mode()&0111 == 0 {
|
||||||
|
return fmt.Errorf("file is not executable: %s", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate filename allowlist
|
||||||
|
base := filepath.Base(cleanedPath)
|
||||||
|
validNames := map[string]bool{
|
||||||
|
"ffmpeg": true,
|
||||||
|
"ffmpeg.exe": true,
|
||||||
|
"ffprobe": true,
|
||||||
|
"ffprobe.exe": true,
|
||||||
|
}
|
||||||
|
if !validNames[base] {
|
||||||
|
return fmt.Errorf("invalid executable name: %s", base)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ffmpegWindowsURL = "aHR0cHM6Ly9naXRodWIuY29tL0J0Yk4vRkZtcGVnLUJ1aWxkcy9yZWxlYXNlcy9kb3dubG9hZC9sYXRlc3QvZmZtcGVnLW1hc3Rlci1sYXRlc3Qtd2luNjQtZ3BsLnppcA=="
|
ffmpegWindowsURL = "aHR0cHM6Ly9naXRodWIuY29tL0J0Yk4vRkZtcGVnLUJ1aWxkcy9yZWxlYXNlcy9kb3dubG9hZC9sYXRlc3QvZmZtcGVnLW1hc3Rlci1sYXRlc3Qtd2luNjQtZ3BsLnppcA=="
|
||||||
ffmpegLinuxURL = "aHR0cHM6Ly9naXRodWIuY29tL0J0Yk4vRkZtcGVnLUJ1aWxkcy9yZWxlYXNlcy9kb3dubG9hZC9sYXRlc3QvZmZtcGVnLW1hc3Rlci1sYXRlc3QtbGludXg2NC1ncGwudGFyLnh6"
|
ffmpegLinuxURL = "aHR0cHM6Ly9naXRodWIuY29tL0J0Yk4vRkZtcGVnLUJ1aWxkcy9yZWxlYXNlcy9kb3dubG9hZC9sYXRlc3QvZmZtcGVnLW1hc3Rlci1sYXRlc3QtbGludXg2NC1ncGwudGFyLnh6"
|
||||||
@@ -84,6 +127,10 @@ func IsFFprobeInstalled() (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ValidateExecutable(ffprobePath); err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Verify it's executable
|
// Verify it's executable
|
||||||
cmd := exec.Command(ffprobePath, "-version")
|
cmd := exec.Command(ffprobePath, "-version")
|
||||||
setHideWindow(cmd)
|
setHideWindow(cmd)
|
||||||
@@ -98,13 +145,9 @@ func IsFFmpegInstalled() (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = os.Stat(ffmpegPath)
|
if err := ValidateExecutable(ffmpegPath); err != nil {
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify it's executable
|
// Verify it's executable
|
||||||
cmd := exec.Command(ffmpegPath, "-version")
|
cmd := exec.Command(ffmpegPath, "-version")
|
||||||
@@ -425,6 +468,10 @@ func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) {
|
|||||||
return nil, fmt.Errorf("failed to get ffmpeg path: %w", err)
|
return nil, fmt.Errorf("failed to get ffmpeg path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ValidateExecutable(ffmpegPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid ffmpeg executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
installed, err := IsFFmpegInstalled()
|
installed, err := IsFFmpegInstalled()
|
||||||
if err != nil || !installed {
|
if err != nil || !installed {
|
||||||
return nil, fmt.Errorf("ffmpeg is not installed")
|
return nil, fmt.Errorf("ffmpeg is not installed")
|
||||||
|
|||||||
@@ -244,6 +244,10 @@ func readMetadataWithFFprobe(filePath string) (*AudioMetadata, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ValidateExecutable(ffprobePath); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid ffprobe executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Use ffprobe to get metadata in JSON format (both format and stream tags)
|
// Use ffprobe to get metadata in JSON format (both format and stream tags)
|
||||||
cmd := exec.Command(ffprobePath,
|
cmd := exec.Command(ffprobePath,
|
||||||
"-v", "quiet",
|
"-v", "quiet",
|
||||||
|
|||||||
@@ -541,6 +541,10 @@ func embedLyricsToM4A(filepath string, lyrics string) error {
|
|||||||
return fmt.Errorf("ffmpeg not found: %w", err)
|
return fmt.Errorf("ffmpeg not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ValidateExecutable(ffmpegPath); err != nil {
|
||||||
|
return fmt.Errorf("invalid ffmpeg executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Create temporary output file with proper extension so ffmpeg can detect format
|
// Create temporary output file with proper extension so ffmpeg can detect format
|
||||||
tmpOutputFile := strings.TrimSuffix(filepath, pathfilepath.Ext(filepath)) + ".tmp" + pathfilepath.Ext(filepath)
|
tmpOutputFile := strings.TrimSuffix(filepath, pathfilepath.Ext(filepath)) + ".tmp" + pathfilepath.Ext(filepath)
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|||||||
Reference in New Issue
Block a user