CVE-2023-7101: Improper Control of Generation of Code ('Code Injection') (CWE-94)
value: Spreadsheet::ParseExcel version 0.65 is a Perl module used for parsing Excel files. Spreadsheet::ParseExcel is vulnerable to an arbitrary code execution (ACE) vulnerability due to passing unvalidated input from a file into a string-type “eval”. Specifically, the issue stems from the evaluation of Number format strings (not to be confused with printf-style format strings) within the Excel parsing logic. 

CWE description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
CVSS v3 base score: 7.8
Published: 2023-12-24T22:15Z
Repository: https://github.com/jmcnamara/spreadsheet-parseexcel
Language: Perl
Files changed: lib/Spreadsheet/ParseExcel/Utility.pm
Commit date: 2023-12-29 00:31:15 +0000
Commit message: Do not use string eval for conditional formatting

Diff: diff --git a/lib/Spreadsheet/ParseExcel/Utility.pm b/lib/Spreadsheet/ParseExcel/Utility.pm
index c0c26a0..d8cfb99 100644
--- a/lib/Spreadsheet/ParseExcel/Utility.pm
+++ b/lib/Spreadsheet/ParseExcel/Utility.pm
@@ -76,14 +76,16 @@ sub ExcelFmt {
 
     # Handle OpenOffice.org GENERAL format.
     $format_str = '@' if uc($format_str) eq "GENERAL";
 
     # Check for a conditional at the start of the format. See notes above.
-    my $conditional;
-    if ( $format_str =~ /^\[([<>=][^\]]+)\](.*)$/ ) {
-        $conditional = $1;
-        $format_str  = $2;
+    my $conditional_op;
+    my $conditional_value;
+    if ( $format_str =~ /^\[([<>=]+)([^\]]+)\](.*)$/ ) {
+        $conditional_op = $1;
+        $conditional_value = $2;
+        $format_str = $3;
     }
 
     # Ignore the underscore token which is used to indicate a padding space.
     $format_str =~ s/_/ /g;
 
@@ -164,16 +166,27 @@ sub ExcelFmt {
     else {
         $section = 0;
     }
 
     # Override the previous choice if the format is conditional.
-    if ($conditional) {
-
-        # TODO. Replace string eval with a function.
-        $section = eval "$number $conditional" ? 0 : 1;
+    if ($conditional_op) {
+        if ($conditional_op eq '>') {
+            $section = $number > $conditional_value ? 0 : 1;
+        } elsif ($conditional_op eq '>=') {
+            $section = $number >= $conditional_value ? 0 : 1;
+        } elsif ($conditional_op eq '<') {
+            $section = $number < $conditional_value ? 0 : 1;
+        } elsif ($conditional_op eq '<=') {
+            $section = $number <= $conditional_value ? 0 : 1;
+        } elsif ($conditional_op eq '=') {
+            $section = $number == $conditional_value ? 0 : 1;
+        } elsif ($conditional_op eq '==') {
+            $section = $number == $conditional_value ? 0 : 1;
+        } elsif ($conditional_op eq '<>') {
+            $section = $number != $conditional_value ? 0 : 1;
+        }
     }
-
     # We now have the required format.
     $format = $formats[$section];
 
     # The format string can contain one of the following colours:
     # [Black] [Blue] [Cyan] [Green] [Magenta] [Red] [White] [Yellow]

Vulnerable code:     my $conditional;
    if ( $format_str =~ /^\[([<>=][^\]]+)\](.*)$/ ) {
        $conditional = $1;
        $format_str  = $2;
    if ($conditional) {

        # TODO. Replace string eval with a function.
        $section = eval "$number $conditional" ? 0 : 1;

Fixed code:     my $conditional_op;
    my $conditional_value;
    if ( $format_str =~ /^\[([<>=]+)([^\]]+)\](.*)$/ ) {
        $conditional_op = $1;
        $conditional_value = $2;
        $format_str = $3;
    if ($conditional_op) {
        if ($conditional_op eq '>') {
            $section = $number > $conditional_value ? 0 : 1;
        } elsif ($conditional_op eq '>=') {
            $section = $number >= $conditional_value ? 0 : 1;
        } elsif ($conditional_op eq '<') {
            $section = $number < $conditional_value ? 0 : 1;
        } elsif ($conditional_op eq '<=') {
            $section = $number <= $conditional_value ? 0 : 1;
        } elsif ($conditional_op eq '=') {
            $section = $number == $conditional_value ? 0 : 1;
        } elsif ($conditional_op eq '==') {
            $section = $number == $conditional_value ? 0 : 1;
        } elsif ($conditional_op eq '<>') {
            $section = $number != $conditional_value ? 0 : 1;
        }