Bug 40579: Fix CSV formula injection vulnerabilities

This patch addresses CSV formula injection vulnerabilities by ensuring
all Text::CSV instances use formula protection.

Changes made:
* Add formula => 'empty' to all Text::CSV constructors in:
  - Koha::Patrons::Import
  - Koha::BackgroundJob::ImportKBARTFile
  - Koha::ERM::EUsage::CounterFile

The 'empty' formula mode causes potentially dangerous formulas (starting
with =, +, -, @) to be treated as empty strings rather than executed,
preventing CSV injection attacks while maintaining data integrity.

Security impact:
* Prevents execution of malicious formulas in CSV imports
* Protects against data exfiltration via CSV formula injection
* Maintains backward compatibility for legitimate CSV data

To test:
1. Apply the previous patch (author test)
2. Run:
   $ ktd --shell
  k$ prove xt/author/Text_CSV_Various.t
=> FAIL: Should show 3 files without formula protection
3. Apply this patch
4. Repeat 2
=> SUCCESS: All tests should now pass
5. Try importing CSV with formula content (=cmd|"/c calc",test,data)
=> SUCCESS: Formula content is rejected/emptied, not executed
6. Import normal CSV data
=> SUCCESS: Normal data imports correctly
7. Sign off :-D

Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Tomas Cohen Arazi 2025-08-01 12:03:17 -03:00 committed by Lucas Gass
parent c3a41742af
commit ca8ef54949
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
3 changed files with 6 additions and 5 deletions

View File

@ -235,7 +235,8 @@ sub read_file {
sep_char => $delimiter,
quote_char => $quote_char,
binary => 1,
allow_loose_quotes => 1
allow_loose_quotes => 1,
formula => 'empty'
}
);
my $headers_to_check = $csv->getline($fh);

View File

@ -304,7 +304,7 @@ sub validate {
my ($self) = @_;
open my $fh, "<", \$self->file_content or die;
my $csv = Text::CSV_XS->new( { binary => 1, always_quote => 1, eol => $/, decode_utf8 => 1 } );
my $csv = Text::CSV_XS->new( { binary => 1, always_quote => 1, eol => $/, decode_utf8 => 1, formula => 'empty' } );
$csv->column_names(qw( header_key header_value ));
my @header_rows = $csv->getline_hr_all( $fh, 0, 12 );
@ -331,7 +331,7 @@ sub _set_report_type_from_file {
my ($self) = @_;
open my $fh, "<", \$self->file_content or die;
my $csv = Text::CSV_XS->new( { binary => 1, always_quote => 1, eol => $/, decode_utf8 => 1 } );
my $csv = Text::CSV_XS->new( { binary => 1, always_quote => 1, eol => $/, decode_utf8 => 1, formula => 'empty' } );
$csv->column_names(qw( header_key header_value ));
my @header_rows = $csv->getline_hr_all( $fh, 0, 12 );
@ -354,7 +354,7 @@ sub _get_rows_from_COUNTER_file {
my ($self) = @_;
open my $fh, "<", \$self->file_content or die;
my $csv = Text::CSV_XS->new( { binary => 1, always_quote => 1, eol => $/, decode_utf8 => 1 } );
my $csv = Text::CSV_XS->new( { binary => 1, always_quote => 1, eol => $/, decode_utf8 => 1, formula => 'empty' } );
my $header_columns = $csv->getline_all( $fh, 13, 1 );
$csv->column_names( @{$header_columns}[0] );

View File

@ -65,7 +65,7 @@ has 'today_iso' => (
has 'text_csv' => (
is => 'rw', lazy => 1,
default => sub { Text::CSV->new( { binary => 1, } ); },
default => sub { Text::CSV->new( { binary => 1, formula => 'empty' } ); },
);
sub import_patrons {