CVE-2022-4643: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') (CWE-78)
value: A vulnerability was found in docconv up to 1.2.0. It has been declared as critical. This vulnerability affects the function ConvertPDFImages of the file pdf_ocr.go. The manipulation of the argument path leads to os command injection. The attack can be initiated remotely. Upgrading to version 1.2.1 is able to address this issue. The name of the patch is b19021ade3d0b71c89d35cb00eb9e589a121faa5. It is recommended to upgrade the affected component. VDB-216502 is the identifier assigned to this vulnerability.
CWE description: The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.
CVSS v3 base score: 9.8
Published: 2022-12-21T22:15Z
Repository: https://github.com/sajari/docconv
Language: Go
Files changed: pdf_ocr.go, pdf_ocr_test.go
Commit date: 2022-07-07 14:16:47 +1000
Commit message: Fix remote code execution vulnerability in the PDF OCR converter (#110)

ENG-3252
Diff: diff --git a/pdf_ocr.go b/pdf_ocr.go
index 4d9d53a..a05c943 100644
--- a/pdf_ocr.go
+++ b/pdf_ocr.go
@@ -1,5 +1,6 @@
+//go:build ocr
 // +build ocr
 
 package docconv
 
 import (
@@ -8,10 +9,11 @@ import (
 	"io/ioutil"
 	"log"
 	"os"
 	"os/exec"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"sync"
 )
 
 var (
@@ -109,11 +111,12 @@ func ConvertPDFImages(path string) (BodyResult, error) {
 }
 
 // PdfHasImage verify if `path` (PDF) has images
 func PDFHasImage(path string) bool {
 	cmd := "pdffonts -l 5 %s | tail -n +3 | cut -d' ' -f1 | sort | uniq"
-	out, err := exec.Command("bash", "-c", fmt.Sprintf(cmd, path)).Output()
+	out, err := exec.Command("bash", "-c", fmt.Sprintf(cmd, shellEscape(path))).CombinedOutput()
+
 	if err != nil {
 		log.Println(err)
 		return false
 	}
 	if string(out) == "" {
@@ -157,5 +160,24 @@ func ConvertPDF(r io.Reader) (string, map[string]string, error) {
 	fullBody := strings.Join([]string{bodyResult.body, imageConvertResult.body}, " ")
 
 	return fullBody, metaResult.meta, nil
 
 }
+
+var shellEscapePattern *regexp.Regexp
+
+func init() {
+	shellEscapePattern = regexp.MustCompile(`[^\w@%+=:,./-]`)
+}
+
+// shellEscape returns a shell-escaped version of the string s. The returned value
+// is a string that can safely be used as one token in a shell command line.
+func shellEscape(s string) string {
+	if len(s) == 0 {
+		return "''"
+	}
+	if shellEscapePattern.MatchString(s) {
+		return "'" + strings.Replace(s, "'", "'\"'\"'", -1) + "'"
+	}
+
+	return s
+}
diff --git a/pdf_ocr_test.go b/pdf_ocr_test.go
new file mode 100644
index 0000000..73a0af5
--- /dev/null
+++ b/pdf_ocr_test.go
@@ -0,0 +1,30 @@
+//go:build ocr
+// +build ocr
+
+package docconv
+
+import (
+	"os"
+	"testing"
+)
+
+func TestPDFHasImage_CannotExecuteCode(t *testing.T) {
+	// Try to inject code by passing a bad file path.
+	// If the code was successful it will create a file called foo in the working directory
+	badFilePath := "$(id >> foo).pdf"
+	if got, want := PDFHasImage(badFilePath), false; got != want {
+		t.Errorf("got %v, want %v", got, want)
+	}
+
+	if got, want := fileExists("foo"), false; got != want {
+		t.Errorf("got bad file exists, want not file to exist")
+	}
+}
+
+func fileExists(filename string) bool {
+	info, err := os.Stat(filename)
+	if os.IsNotExist(err) {
+		return false
+	}
+	return !info.IsDir()
+}

Vulnerable code: 	out, err := exec.Command("bash", "-c", fmt.Sprintf(cmd, path)).Output()
Fixed code: //go:build ocr
	"regexp"
	out, err := exec.Command("bash", "-c", fmt.Sprintf(cmd, shellEscape(path))).CombinedOutput()


var shellEscapePattern *regexp.Regexp

func init() {
	shellEscapePattern = regexp.MustCompile(`[^\w@%+=:,./-]`)
}

// shellEscape returns a shell-escaped version of the string s. The returned value
// is a string that can safely be used as one token in a shell command line.
func shellEscape(s string) string {
	if len(s) == 0 {
		return "''"
	}
	if shellEscapePattern.MatchString(s) {
		return "'" + strings.Replace(s, "'", "'\"'\"'", -1) + "'"
	}

	return s
}
//go:build ocr
// +build ocr

package docconv

import (
	"os"
	"testing"
)

func TestPDFHasImage_CannotExecuteCode(t *testing.T) {
	// Try to inject code by passing a bad file path.
	// If the code was successful it will create a file called foo in the working directory
	badFilePath := "$(id >> foo).pdf"
	if got, want := PDFHasImage(badFilePath), false; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	if got, want := fileExists("foo"), false; got != want {
		t.Errorf("got bad file exists, want not file to exist")
	}
}

func fileExists(filename string) bool {
	info, err := os.Stat(filename)
	if os.IsNotExist(err) {
		return false
	}
	return !info.IsDir()
}