@ 11d698f

260607.4 · 45ff5fe77dbd96f7332f31943102291f878f30b8

Make CVE changes

reading declarations

Changes

@@ -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)

MANIFEST

@@ -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

ack

@@ -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 ) {

lib/App/Ack.pm

@@ -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 {

lib/App/Ack/ConfigLoader.pm

@@ -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 {

lib/App/Ack/Filter/Match.pm

@@ -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 {

t/Util.pm

@@ -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 );     }

t/context.yaml

@@ -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: ""

t/file-iterator.t

@@ -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

t/forbidden-options.t

@@ -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 {

t/invalid-context.t

new file mode 100644@@ -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;

t/naughty-filenames.t

new file mode 100644@@ -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 );+    };+}++

t/no-re-eval.t

new file mode 100644@@ -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" );+    }+};

t/swamp/bad-ascii.tar.GZ

new file mode 100644Binary file changed