Rex-LibSSH

 view release on metacpan or  search on metacpan

t/03-path-quoting.t  view on Meta::CPAN

    is( Rex::Interface::Fs::LibSSH::_q("back\\slash"), "'back\\slash'",    'backslash (literal in single quotes)' );
    is( Rex::Interface::Fs::LibSSH::_q("line1\nline2"),"'line1\nline2'",   'newline (literal in single quotes)' );

    # NUL cannot be represented in argv; libssh would silently truncate.
    eval { Rex::Interface::Fs::LibSSH::_q("foo\0bar") };
    like( $@, qr/NUL/, '_q dies on NUL byte' );
};

subtest 'File::_q matches Fs::_q contract' => sub {
    plan tests => 3;

    is( Rex::Interface::File::LibSSH::_q("o'clock"), q['o'"'"'clock'],
        'File _q: single quote' );
    is( Rex::Interface::File::LibSSH::_q('a$b'), "'a\$b'",
        'File _q: dollar literal' );
    eval { Rex::Interface::File::LibSSH::_q("foo\0bar") };
    like( $@, qr/NUL/, 'File _q dies on NUL byte' );
};

subtest 'glob rejects shell metacharacters beyond glob syntax' => sub {
    plan tests => 8;

    # Every entry below contains a character that would let the pattern
    # break out of "echo <pattern>" and execute an arbitrary command.
    # The hardened glob must die before _run() opens an ssh channel.
    for my $case (
        [ "/*; touch /tmp/rex-libssh-whatever", 'semicolon' ],
        [ "foo|bar",                           'pipe' ],
        [ "foo&bar",                           'ampersand' ],
        [ "foo>bar",                           'redirect out' ],
        [ "foo<bar",                           'redirect in' ],
        [ 'foo`bar`',                          'backtick' ],
        [ 'foo$(bar)',                         'dollar-paren' ],
        [ "foo;rm -rf /",                      'injection rm -rf' ],
    ) {
        my ( $pat, $label ) = @$case;
        eval { Rex::Interface::Fs::LibSSH->new->glob($pat) };
        like( $@, qr/(NUL|metachar)/,
            "glob rejects $label pattern: $pat" );
    }
};

# ============================================================
# Integration tests -- drive a real sshd
# ============================================================

my $srv = TestSSHD->start;
unless ($srv) {
    plan skip_all => 'sshd or ssh-keygen not available';
}

use Rex -feature => ['1.4'];
use Rex::Group::Entry::Server;
use Rex::Commands::Fs;
use Rex::Commands::File;
use Rex::Config;

set connection => 'LibSSH';

Rex::Config->set_user( scalar getpwuid($<) );
Rex::Config->set_private_key( $srv->client_key );
Rex::Config->set_public_key( $srv->client_key . '.pub' );

Rex::connect(
    server      => $srv->host,
    port        => $srv->port,
    user        => scalar( getpwuid($<) ),
    private_key => $srv->client_key,
    public_key  => $srv->client_key . '.pub',
    auth_type   => 'key',
    knownhosts  => $srv->known_hosts,
);

# Use a fresh tempdir on the remote side (the sshd is local, so a
# local /tmp path is visible to the remote user too).
my $dir = tempdir(CLEANUP => 1);

# Local source file for upload(). All "remote file creation" goes
# through upload (cat > $qpath) so the test is independent of
# Rex::Commands::File, which post-verifies with md5sum and uses a
# single-quoted path without escaping -- an upstream bug for paths
# containing '. Our _q() handles ' correctly; the test exercises
# the Fs::LibSSH layer that we control.
my $src = "$dir/local-src-$$";
open( my $sfh, '>', $src ) or die "cannot write $src: $!";
print $sfh "x\n";
close $sfh;

subtest 'is_file + stat + unlink with paths containing shell metacharacters' => sub {
    plan tests => 12;

    # Each block: create a file with an odd name (upload -> Fs::LibSSH
    # which goes through _q), verify is_file and stat, then unlink it.

    # --- space ---
    my $space = "$dir/has space.txt";
    upload $src, $space;
    ok  is_file($space),          'is_file: path with space';
    my %st = stat($space);
    ok  $st{size} > 0,            'stat: path with space returns size';
    ok  defined $st{mode},        'stat: path with space returns mode';
    unlink $space;
    ok !is_file($space),          'unlink: path with space removed';

    # --- single quote ---
    my $q = "$dir/o'clock.txt";
    upload $src, $q;
    ok  is_file($q),              'is_file: path with single quote';
    unlink $q;
    ok !is_file($q),              'unlink: path with single quote';

    # --- $ ---
    my $d = "$dir/price\$100.txt";
    upload $src, $d;
    ok  is_file($d),              'is_file: path with dollar';
    unlink $d;
    ok !is_file($d),              'unlink: path with dollar';

    # --- backtick ---
    my $b = "$dir/`echo injected`.txt";
    upload $src, $b;
    ok  is_file($b),              'is_file: path with backticks';
    unlink $b;
    ok !is_file($b),              'unlink: path with backticks';

    # --- newline ---
    my $n = "$dir/line1\nline2.txt";
    upload $src, $n;



( run in 0.408 second using v1.01-cache-2.11-cpan-036bef1c656 )