260607.4 · 45ff5fe77dbd96f7332f31943102291f878f30b8
Make CVE changes
reading declarations
@@ -2,6 +2,11 @@ History file for ack 3. https://beyondgrep.com/ NEXT ========================================+[SECURITY]+CVE-2026-49147: filename ANSI escape sequences+CVE-2026-49146: project .ackrc -A -B -C memory exhaustion+CVE-2026-49145: project .ackrc --follow / --files-from file exfiltration+ [FIXES] Fixed a bug where types set in the .ackrc could not be overridden from the command line. Thanks, Dmitri Vereshchagin. (GH #393)
@@ -107,6 +107,7 @@ t/illegal-regex.t t/incomplete-last-line.t t/interactive.t t/invalid-ackrc.t+t/invalid-context.t t/invalid-options.t t/is-filter.t t/line-endings.t@@ -117,8 +118,10 @@ t/multiple-captures.t t/mutex-options.t t/mutex.yaml t/named-pipes.t+t/naughty-filenames.t t/needs-line-scan.t t/noackrc.t+t/no-re-eval.t t/noenv.t t/prescan-line-boundaries.t t/process-substitution.t@@ -153,6 +156,7 @@ t/swamp/MasterPage.master t/swamp/Rakefile t/swamp/Sample.ascx t/swamp/Sample.asmx+t/swamp/bad-ascii.tar.GZ t/swamp/blib/ignore.pm t/swamp/blib/ignore.pod t/swamp/c-header.h
@@ -7,6 +7,8 @@ our $VERSION = 'v3.9.0'; # Check https://beyondgrep.com/ for updates use 5.010001; +no re 'eval'; # Enforce default and block -Mre=eval to block RCE security problem.+ use File::Spec (); use File::Next (); use Getopt::Long ();@@ -297,6 +299,13 @@ MAIN: { exit 0; +# Sanitize a filename for display: replace control chars with '?'+sub _safe_filename {+ my $s = shift;+ $s =~ s/[^\x20-\x7E\t]/?/g;+ return $s;+}+ sub file_loop_fg { my $files = shift;@@ -307,10 +316,10 @@ sub file_loop_fg { App::Ack::show_types( $file ); } elsif ( $opt_g ) {- print_line_with_options( undef, $file->name, 0, $App::Ack::ors );+ print_line_with_options( undef, _safe_filename( $file->name ), 0, $App::Ack::ors ); } else {- App::Ack::say( $file->name );+ App::Ack::say( _safe_filename( $file->name ) ); } ++$nmatches; last if defined($opt_m) && ($nmatches >= $opt_m);@@ -615,7 +624,7 @@ sub file_loop_normal { sub print_matches_in_file { my $file = shift; - my $filename = $file->name;+ my $filename = _safe_filename($file->name); my $fh = $file->open; if ( !$fh ) {
@@ -13,6 +13,8 @@ A container for functions for the ack program. =cut +no re 'eval'; # enforce default, block -Mre=eval to block RCE security fail+ our $VERSION; our $COPYRIGHT; BEGIN {
@@ -373,8 +373,17 @@ sub get_arg_spec { sub _context_value { my $val = shift; - # Contexts default to 2.- return (!defined($val) || ($val < 0)) ? 2 : $val;+ # Contexts default to 2.+ return 2 if !defined($val) || $val < 0;++ if ( $val > 10_000 ) {+ App::Ack::die(+ "Context value $val exceeds the maximum allowed "+ . "value of 10000."+ );+ }++ return $val; } @@ -427,6 +436,7 @@ sub _process_other { $args_for_source = { %{$args_for_source}, 'pager:s' => $illegal,+ 'follow!' => $illegal, }; }
lib/App/Ack/Filter/Extension.pm
@@ -12,6 +12,8 @@ Implements filters based on extensions. use strict; use warnings;+no re 'eval'; # Enforce default and block -Mre=eval to block RCE security problem.+ use parent 'App::Ack::Filter'; use App::Ack::Filter ();
lib/App/Ack/Filter/FirstLineMatch.pm
@@ -13,6 +13,8 @@ The class that implements filtering files by their first line. use strict; use warnings;+no re 'eval'; # Enforce default and block -Mre=eval to block RCE security problem.+ use parent 'App::Ack::Filter'; sub new {
@@ -2,6 +2,9 @@ package App::Ack::Filter::Match; use strict; use warnings;++no re 'eval'; # Enforce default and block -Mre=eval to block RCE security problem.+ use parent 'App::Ack::Filter'; use App::Ack::Filter::MatchGroup ();
lib/App/Ack/Filter/MatchGroup.pm
@@ -13,6 +13,8 @@ into one container. See App::Ack::Filter::IsGroup for details. use strict; use warnings;+no re 'eval'; # Enforce default and block -Mre=eval to block RCE security problem.+ use parent 'App::Ack::Filter'; sub new {
@@ -1232,6 +1232,18 @@ sub adjust_executable { my $perl = caret_X(); + # ACK_TEST_MODULE_OPTS+ # If a .t test file wishes to load a module for debug or provocation before Ack starts+ # e.g.+ # $ENV{'ACK_TEST_MODULE_OPTS'} = 're=eval' ; attempts to enable runtime code injection in REs+ # (which we block, but this is needed to test that we block it)+ # $ENV{'ACK_TEST_MODULE_OPTS'} = 're=debug' ; would trace REs compilation and execution,+ # (which gets rather long so don't check in that way!)+ if ($ENV{'ACK_TEST_MODULE_OPTS'}){+ # warn "ACK_TEST_MODULE_OPTS=-M$ENV{'ACK_TEST_MODULE_OPTS'}" ; #############DEBUG #####+ unshift( @cmd, "-M$ENV{'ACK_TEST_MODULE_OPTS'}" );+ }+ if ( $ENV{'ACK_TEST_STANDALONE'} ) { unshift( @cmd, $perl ); }
@@ -105,3 +105,15 @@ stdout: | This is line 09 This is line 10 This is line 11++---+name: -C with huge context throws+args: -C 20000|30000 t/text/numbered-text.txt+exitcode: 1+stdout: ""++---+name: -B with huge context throws+args: -B 20000|30000 t/text/numbered-text.txt+exitcode: 1+stdout: ""
@@ -34,6 +34,7 @@ UNFILTERED: { sets_match( \@files, [qw( t/swamp/0+ t/swamp/bad-ascii.tar.GZ t/swamp/blib/ignore.pm t/swamp/blib/ignore.pod t/swamp/constitution-100k.pl
@@ -32,7 +32,7 @@ sub _test_project_ackrc { local $Test::Builder::Level = $Test::Builder::Level + 1; return subtest subtest_name() => sub {- plan tests => 3;+ plan tests => 4; my $base_obj = File::Temp->newdir; my $base = $base_obj->dirname;@@ -51,19 +51,20 @@ sub _test_project_ackrc { safe_chdir( $projectdir ); - # All three of these options are illegal in a project .ackrc.- for my $option ( qw( match output pager ) ) {+ # All four of these options are illegal in a project .ackrc.+ for my $option ( qw( match output pager follow ) ) { subtest $option => sub { plan tests => 2; # /tmp/x/project/.ackrc- _create_ackrc( $projectdir, "--$option=$option" );+ _create_ackrc( $projectdir, "--$option=$option" ) unless 'follow' eq $option;+ _create_ackrc( $projectdir, "--$option" ) if 'follow' eq $option; # Explicitly pass --env or else the test will ignore .ackrc. my ( $stdout, $stderr ) = run_ack_with_stderr( '-f', '--env' ); is_empty_array( $stdout, 'No output with the errors' );- if ( $option eq 'pager' ) {+ if ( ($option eq 'pager') || ($option eq 'follow') ) { first_line_like( $stderr, qr/\QOption --$option is forbidden in project .ackrc files/, "$option illegal" ); } else {
@@ -0,0 +1,23 @@+#!perl++use 5.010;+use strict;+use warnings;++use Test::More tests => 5;+use lib 't';+use Util;++prep_environment();+++my ( $stdout, $stderr ) = run_ack_with_stderr( '-C20000' );+is_empty_array( $stdout, 'No output because of our bad option' );++like( $stderr->[0], qr/ack(?:-standalone)?: Context value 20000 exceeds the maximum allowed value of 10000[.]/, 'First line is the specific error' );+like( $stderr->[1], qr/ack(?:-standalone)?: Invalid option on command line/, 'Second line is the general error' );+is( scalar @{$stderr}, 2, 'There are no more lines' );++is( get_rc(), 255, 'Should fail' );++exit 0;
@@ -0,0 +1,136 @@+#!perl++use 5.010;+use strict;+use warnings;++use Test::More tests => 1;++use File::Spec ();+use File::Temp ();++use lib 't';+use Util;++prep_environment();++# Global:+# /tmp/x/etc/.ackrc+# /tmp/x/swamp++my $wd = getcwd_clean();++_test_naughty_ansi_filenames();++exit 0;++# Test project directory+# ackrc in /tmp/x/project/subdir/{naughtyAnsiFiles}+#+sub _test_naughty_ansi_filenames {+ local $Test::Builder::Level = $Test::Builder::Level + 1;++ return subtest subtest_name() => sub {+ plan tests => 6;++ my $base_obj = File::Temp->newdir;+ my $base = $base_obj->dirname;++ # /tmp/x/project+ my $projectdir = File::Spec->catdir( $base, 'project' );+ safe_mkdir( $projectdir );++ # /tmp/x/project/subdir/+ my $projectsubdir = File::Spec->catdir( $projectdir, 'subdir' );+ safe_mkdir( $projectsubdir );++ # These files have evil names, in the sense of ANSI terminal manipulation+ # so are generated and removed rather than shipped in t/++ # /tmp/x/project/subdir/${forged}+ # (1) Newline injection: filename splits ack output into two lines+ # original bash equivalent:+ # # FORGED=$(printf 'real\nFORGED:999:injected_content.pl')+ # # printf 'foo\n' > "./$FORGED"+ #+ my $forged = "real\nFORGED:999:injected_content.pl";+ my $projectfile = File::Spec->catfile( $projectsubdir, $forged );+ write_file( $projectfile, "foo FORGED\n" );++ # /tmp/x/project/subdir/${ansi}+ # ANSI injection: ESC bytes change terminal appearance+ # # ANSI=$(printf 'file\033[31mRED\033[0m.pl')+ # # printf 'foo\n' > "./$ANSI"+ my $ansi = "file\033[31mRED\033[0m.pl";+ $projectfile = File::Spec->catfile( $projectsubdir, $ansi );+ write_file( $projectfile, "foo RED\n" );++ # /tmp/x/project/subdir/normal.pl+ # # printf 'foo\n' > .//normal.pl+ $projectfile = File::Spec->catfile( $projectsubdir, 'normal.pl' );+ write_file( $projectfile, "foo normal\n" );++ safe_chdir( $projectdir );++ # TO VIEW temp dir contents+ # system('ls', '-alr', 'subdir',);++ my %expect = (+ RED => qr{\Qsubdir/file?[31mRED?[0m.pl\E},+ normal => qr{\Qsubdir/normal.pl\E},+ FORGED => qr{\Qsubdir/real?FORGED:999:injected_content.pl\E},+ );+ for my $name (qw[ RED normal FORGED ]){+ subtest "5_01_filter_listing_$name" => sub {+ plan tests => 3;+ # /tmp/x/project/.ackrc+ # _create_ackrc( $projectdir, "--$option=$option" ) ???++ # Explicitly pass --env or else the test will ignore .ackrc.+ my ( $stdout, $stderr ) = run_ack_with_stderr( '-g', '--env', $name );++ # NOT is_empty_array( $stdout, 'No output with the errors' );+ is (scalar(@$stdout),1, "number of lines correct");+ like( $stdout->[0], $expect{$name}, "$name listing" );++ is_empty_array( $stderr, 'No errors with output' );+ # first_line_like( $stderr, qr/\Qsome error message/, "some error message" );+ };+ } # end for 1++ my %File = (+ RED => qq{subdir/file\033\[31mRED\033\[0m.pl},+ normal => qq{subdir/normal.pl},+ FORGED => qq{subdir/real\nFORGED:injected_content.pl},+ );+ %expect = (+ RED => qr{subdir/file[?][[]31mRED[?][[]0m[.]pl:1:foo},+ normal => qr{subdir/normal.pl:1:foo},+ FORGED => qr{subdir/real[?]FORGED:999:injected_content[.]pl:1:foo},+ );+ for my $name (qw[ RED normal FORGED ]){+ subtest "5_02_filter_match_$name" => sub {+ plan tests => 3;++ # /tmp/x/project/.ackrc+ # _create_ackrc( $projectdir, "--$option=$option" ) ???++ # Explicitly pass --env or else the test will ignore .ackrc.+ my ( $stdout, $stderr ) = run_ack_with_stderr( '--env', '--with-filename', "foo $name" , );++ # NOT is_empty_array( $stdout, 'No output with the errors' );+ is (scalar(@$stdout),1, "number of lines correct");+ first_line_like( $stdout, $expect{$name}, "on match, masked $name" );++ is_empty_array( $stderr, 'No errors with output' );+ # first_line_like( $stderr, qr/\Qsome error message/, "some error message" );++ };+ } # end for 2++ # Go back to working directory so the temporary directories can get erased.+ safe_chdir( $wd );+ };+}++
@@ -0,0 +1,55 @@+#!perl++use 5.010;+use strict;+use warnings;++use Test::More tests => 1;++use File::Spec ();+use File::Temp ();++use lib 't';+use Util;++prep_environment();++# Global:+# /tmp/x/etc/.ackrc+# /tmp/x/swamp++my $wd = getcwd_clean();++_test_re_eval();++exit 0;++# Test project directory+# ackrc in /tmp/x/project/.ackrc+sub _test_re_eval {+ local $Test::Builder::Level = $Test::Builder::Level + 1;++ return subtest subtest_name() => sub {+ plan tests => 5;++ # Code assertions are normally permitted in compiled Perl but not runtime JIT qr// or match// substitutions.+ # -M re=eval enables runtime eval-group (?{code}) (??{code}) (*{code}) in runtime qr//.+ # This is unsafe given $PROJECT/.ackrc --type, --ignore options, so it's blocked+ #+ # TODO Could add tests for --ignore --match --and REs, etc etc, as of several files prefaced by `no re 'eval';`+ # this actually monitors only one for regression.+ #+ $ENV{ACK_TEST_MODULE_OPTS}='re=eval'; ## Evil to be thwarted, ask Util.pm to prefix evil -Mre=eval for us++ my ( $stdout, $stderr ) = run_ack_with_stderr('-f',+ q(--type-add=badtype:firstlinematch:/(?{print "hello executing code \n"})oops/), ## code injection+ '--badtype', 't/swamp');++ is_empty_array( $stdout, 'No output with the errors' ); # If not fixed, will say 'hello executing code ' repeatedly.++ is(scalar(@$stderr), 3, 'number of errors as expected' );+ like($stderr->[0], qr/\QEval-group not allowed at runtime,\E/, "Eval-group blocked in type RE" );+ like($stderr->[1], qr/\QUnknown option: badtype\E/, "Unknown option in type RE" );+ like($stderr->[2], qr/\QInvalid option on command line\E/, "invalid option reported" );+ }+};