Program: CWE-20
Project: CVE-2018-20683
Buggy source:
#!/usr/bin/perl
use strict;
use warnings;

use lib $ENV{GL_LIBDIR};
use Gitolite::Easy;

=for admins

BUNDLE SUPPORT

    (1) For each repo in gitolite.conf for which you want bundle support (or
        '@all', if you wish), add the following line:

            option bundle       =   1

        Or you can say:

            option bundle.ttl   =   <number>

        A bundle file that is more than <number> seconds old (default value
        86400, i.e., 1 day) is recreated on the next bundle request.  Increase
        this if your repo is not terribly active.

        Note: a bundle file is also deleted and recreated if it contains a ref
        that was then either deleted or rewound in the repo.  This is checked
        on every invocation.

    (2) Add 'rsync' to the ENABLE list in the rc file


GENERIC RSYNC SUPPORT

    TBD

=cut

=for usage
rsync helper for gitolite

BUNDLE SUPPORT

    Admins: see src/commands/rsync for setup instructions

    Users:
        rsync -P git@host:repo.bundle .
            # downloads a file called "<basename of repo>.bundle"; repeat as
            # needed till the whole thing is downloaded
        git clone repo.bundle repo
        cd repo
        git remote set-url origin git@host:repo
        git fetch origin    # and maybe git pull, etc. to freshen the clone

GENERIC RSYNC SUPPORT

    TBD

=cut

usage() if not @ARGV or $ARGV[0] eq '-h';

# rsync driver program.  Several things can be done later, but for now it
# drives just the 'bundle' transfer.

if ( $ENV{SSH_ORIGINAL_COMMAND} =~ /^rsync --server --sender (-[-\w=.]+ )+\. (\S+)\.bundle$/ ) {

    my $repo = $2;
    $repo =~ s/\.git$//;

    # all errors have the same message to avoid leaking info
    can_read($repo) or _die "you are not authorised";
    my %config = config( $repo, "gitolite-options.bundle" ) or _die "you are not authorised";

    my $ttl = $config{'gitolite-options.bundle.ttl'} || 86400;    # in seconds (default 1 day)

    my $bundle = bundle_create( $repo, $ttl );

    $ENV{SSH_ORIGINAL_COMMAND} =~ s( \S+\.bundle)( $bundle);
    trace( 1, "rsync bundle", $ENV{SSH_ORIGINAL_COMMAND} );
    Gitolite::Common::_system( split ' ', $ENV{SSH_ORIGINAL_COMMAND} );
    exit 0;
}

_warn "invalid rsync command '$ENV{SSH_ORIGINAL_COMMAND}'";
usage();

# ----------------------------------------------------------------------
# helpers
# ----------------------------------------------------------------------

sub bundle_create {
    my ( $repo, $ttl ) = @_;
    my $bundle = "$repo.bundle";
    $bundle =~ s(.*/)();
    my $recreate = 0;

    my ( %b, %r );
    if ( -f $bundle ) {
        %b = map { chomp; reverse split; } `git ls-remote --heads --tags $bundle`;
        %r = map { chomp; reverse split; } `git ls-remote --heads --tags .`;

        for my $ref ( sort keys %b ) {

            my $mtime = ( stat $bundle )[9];
            if ( time() - $mtime > $ttl ) {
                trace( 1, "bundle too old" );
                $recreate++;
                last;
            }

            if ( not $r{$ref} ) {
                trace( 1, "ref '$ref' deleted in repo" );
                $recreate++;
                last;
            }

            if ( $r{$ref} eq $b{$ref} ) {
                # same on both sides; ignore
                delete $r{$ref};
                delete $b{$ref};
                next;
            }

            `git rev-list --count --left-right $b{$ref}...$r{$ref}` =~ /^(\d+)\s+(\d+)$/ or _die "git too old";
            if ($1) {
                trace( 1, "ref '$ref' rewound in repo" );
                $recreate++;
                last;
            }

        }

    } else {
        trace( 1, "no bundle found" );
        $recreate++;
    }

    return $bundle if not $recreate;

    trace( 1, "creating bundle for '$repo'" );
    -f $bundle and ( unlink $bundle or die "a horrible death" );
    system("git bundle create $bundle --branches --tags >&2");

    return $bundle;
}

sub trace {
    Gitolite::Common::trace(@_);
}


Fixed version (ground truth):
#!/usr/bin/perl
use strict;
use warnings;

use lib $ENV{GL_LIBDIR};
use Gitolite::Easy;

=for admins

BUNDLE SUPPORT

    (1) For each repo in gitolite.conf for which you want bundle support (or
        '@all', if you wish), add the following line:

            option bundle       =   1

        Or you can say:

            option bundle.ttl   =   <number>

        A bundle file that is more than <number> seconds old (default value
        86400, i.e., 1 day) is recreated on the next bundle request.  Increase
        this if your repo is not terribly active.

        Note: a bundle file is also deleted and recreated if it contains a ref
        that was then either deleted or rewound in the repo.  This is checked
        on every invocation.

    (2) Add 'rsync' to the ENABLE list in the rc file

=cut

=for usage
rsync helper for gitolite

BUNDLE SUPPORT

    Admins: see src/commands/rsync for setup instructions

    Users:
        rsync git@host:repo.bundle .
            # downloads a file called "<basename of repo>.bundle"; repeat as
            # needed till the whole thing is downloaded
        git clone repo.bundle repo
        cd repo
        git remote set-url origin git@host:repo
        git fetch origin    # and maybe git pull, etc. to freshen the clone

    NOTE on options to the rsync command: you are only allowed to use the
    "-v", "-n", "-q", and "-P" options.

=cut

usage() if not @ARGV or $ARGV[0] eq '-h';

# rsync driver program.  Several things can be done later, but for now it
# drives just the 'bundle' transfer.

if ( $ENV{SSH_ORIGINAL_COMMAND} =~ /^rsync --server --sender (?:-[vn]*(?:e\d*\.\w*)? )?\. (\S+)\.bundle$/ ) {

    my $repo = $1;
    $repo =~ s/\.git$//;

    # all errors have the same message to avoid leaking info
    can_read($repo) or _die "you are not authorised";
    my %config = config( $repo, "gitolite-options.bundle" ) or _die "you are not authorised";

    my $ttl = $config{'gitolite-options.bundle.ttl'} || 86400;    # in seconds (default 1 day)

    my $bundle = bundle_create( $repo, $ttl );

    $ENV{SSH_ORIGINAL_COMMAND} =~ s( \S+\.bundle)( $bundle);
    trace( 1, "rsync bundle", $ENV{SSH_ORIGINAL_COMMAND} );
    Gitolite::Common::_system( split ' ', $ENV{SSH_ORIGINAL_COMMAND} );
    exit 0;
}

_warn "Sorry, you are only allowed to use the '-v', '-n', '-q', and '-P' options.";
usage();

# ----------------------------------------------------------------------
# helpers
# ----------------------------------------------------------------------

sub bundle_create {
    my ( $repo, $ttl ) = @_;
    my $bundle = "$repo.bundle";
    $bundle =~ s(.*/)();
    my $recreate = 0;

    my ( %b, %r );
    if ( -f $bundle ) {
        %b = map { chomp; reverse split; } `git ls-remote --heads --tags $bundle`;
        %r = map { chomp; reverse split; } `git ls-remote --heads --tags .`;

        for my $ref ( sort keys %b ) {

            my $mtime = ( stat $bundle )[9];
            if ( time() - $mtime > $ttl ) {
                trace( 1, "bundle too old" );
                $recreate++;
                last;
            }

            if ( not $r{$ref} ) {
                trace( 1, "ref '$ref' deleted in repo" );
                $recreate++;
                last;
            }

            if ( $r{$ref} eq $b{$ref} ) {
                # same on both sides; ignore
                delete $r{$ref};
                delete $b{$ref};
                next;
            }

            `git rev-list --count --left-right $b{$ref}...$r{$ref}` =~ /^(\d+)\s+(\d+)$/ or _die "git too old";
            if ($1) {
                trace( 1, "ref '$ref' rewound in repo" );
                $recreate++;
                last;
            }

        }

    } else {
        trace( 1, "no bundle found" );
        $recreate++;
    }

    return $bundle if not $recreate;

    trace( 1, "creating bundle for '$repo'" );
    -f $bundle and ( unlink $bundle or die "a horrible death" );
    system("git bundle create $bundle --branches --tags >&2");

    return $bundle;
}

sub trace {
    Gitolite::Common::trace(@_);
}