如何安全地将带空格的文件名传递给Perl中的外部命令?

时间:2009-08-12 17:49:02

标签: perl escaping

我有一个Perl脚本,它处理一堆文件名,并在反引号中使用这些文件名。但文件名包含空格,撇号和其他时髦字符。

我希望能够正确地逃脱它们(即不使用我头顶的随机正则表达式)。是否存在正确转义字符串以在bash命令中使用的CPAN模块?我知道我过去已经解决了这个问题,但这次我找不到任何东西。关于它的信息似乎很少。

3 个答案:

答案 0 :(得分:6)

如果你可以管理它(即如果你直接调用某个命令,没有任何shell脚本或高级重定向恶作剧),最安全的做法是避免完全通过shell传递数据。

在perl 5.8 +中:

my @output_lines = do {
    open my $fh, "-|", $command, @args or die "Failed spawning $command: $!";
    <$fh>;
};

如果有必要支持5.6:

my @output_lines = do {
    my $pid = open my $fh, "-|";
    die "Couldn't fork: $!" unless defined $pid;
    if (!$pid) {
        exec $command, @args or die "Eek, exec failed: $!";
    } else {
        <$fh>; # This is the value of the C<do>
    }
};

有关此类业务的详情,请参阅perldoc perlipc,另请参阅IPC::Open2IPC::Open3

答案 1 :(得分:3)

您在寻找quotemeta吗?

  

返回EXPR的值,并将所有非“word”字符反斜杠。

更新:正如hobbs在评论中指出的那样,quotemeta并非用于此目的,在考虑更多内容后,可能会遇到嵌入式nul的问题秒。另一方面String::ShellQuote在遇到嵌入式null时会发出嘶嘶声。

最安全的方法是完全避免使用shell。使用'system'的列表形式可以有很长的路要走(几个月前我发现cmd.exe可能仍然涉及Windows)我感到沮丧,我建议这样做。

如果您需要输出命令,最好(安全地)自行打开管道,如hobbs' answer

所示

答案 2 :(得分:2)

<强> TL;博士

以下子例程在类Unix和Windows系统上安全地引用(转义)文件名(路径)列表

#!/usr/bin/env perl

sub quoteforshell { 
  return join ' ', map { 
    $^O eq 'MSWin32' ?
      '"' . s/"/""/gr . '"'
      : 
      "'" . s/'/'\\''/gr . "'" 
  } @_;
}

#'# Sample invocation
my $shellcmd = ($^O eq 'MSWin32' ? 'echo ' : 'printf "%s\n" ') . 
  quoteforshell('\\foo/bar', 'I\'m here', '3" of snow', 'bar |&;()<>#!');

print `$shellcmd`;

在类Unix系统上输出示例命令,显示所有输入参数都是通过未修改的方式传递的:

\foo/bar
I'm here
3" of snow
bar |&;()<>#!
  • 在类Unix系统上,它应该适用于任何字符串(除了具有嵌入式NUL字符的字符串),而不仅仅是文件名 - 请参阅下面的详细信息。

  • 在Windows上,嵌入式"实例将转义为"",这是唯一的安全方式,但遗憾的是,可能不是目标程序所期望的 - 详见下文;但请注意,如果您仅在Windows上传递文件名,则不需要考虑此问题,因为"不是合法的文件名字符。

  • 请参阅本文底部的 无shell 命令调用替代,以绕过Windows上的"引用问题。

类Unix平台qx//`...`的通用形式)和systemexec的单参数形式通过将命令传递给/bin/sh -c 来调用shell。假设/bin/sh POSIX兼容(在给定系统上可能是也可能不是Bash)。

systemexec 的单参数形式可能会或可能不会涉及shell - 他们根据传递的特定命令决定是否参与需要shell。例如,如果命令具有嵌入(文字)单引号或双引号,则调用shell 。由于下面的解决方案基于在命令字符串中嵌入单引号令牌,因此它也适用于systemexec的单参数形式。

在与POSIX兼容的shell中,您可以利用单引号字符串,它不会以任何方式插入其内容。

唯一的挑战是逃避单引号(')本身,这需要欺骗,因为严格来说,不支持在单引号字符串中嵌入单引号由壳。

诀窍是'(原文如此)替换每个'\''实例,这可以通过有效地将输入字符串拆分为多个来解决问题单引号字符串,带有转义'个实例 - \' - 拼接在中 - 然后shell将字符串部分重新组合为单个字符串。

这里有一个子程序,它取一个字符串列表(文件名),并返回一个空格分隔的字符串引用版本字符串,保证 literal 使用shell:

sub quoteforsh { join ' ', map { "'" . s/'/'\\''/gr . "'" } @_ }

示例(使用大多数POSIX shell元字符):

my $shellcmd = 'printf "%s\n" ' . 
                  quoteforsh('\\foo/bar', 'I\'m here', '3" of snow', 'bar |&;()<>#!');
print `$shellcmd`;

这会将以下内容传递给/bin/sh -c(此处显示为纯文字,不带任何引号):

 printf "%s\n" '\foo/bar' 'I'\''m here' '3" of snow' 'bar |&;()<>#!'

请注意每个输入字符串是如何用单引号括起来的,以及所有输入字符串中唯一需要引用的字符是',如上所述,它被替换为'\''。< / p>

这应该输出输入字符串 as-is ,每行一个:

\foo/bar
I'm here
3" of snow
bar |&;()<>#!

Windows 上,类似的子程序如下所示:

sub quoteforcmdexe { join ' ', map { '"' . s/"/""/gr . '"' } @_ }

这与上面的quoteforsh()类似,除了

  • 双引号用于封装代币,因为cmd.exe不支持单引号。
  • 唯一需要转义的字符是",其转义为"" - 请注意,对于文件名,这不是必须的,因为这不是必需的,因为Windows不允许文件名中有"个实例。

但是,有限制和陷阱

  • 无法取消对现有环境变量的引用的解释,例如%USERNAME% ;相比之下,不存在的变量或孤立的%实例都可以。
    • 注意:您能够%实例转义%%,但是当它在批处理文件中有效时,它无法解释为什么不能使用Perl :
      • `perl "%%USERNAME%%.pl"`抱怨,例如,%jdoe%.pl未被发现,暗示%USERNAME%被插值,尽管%字符加倍。
      • (另一方面,双引号字符串中的孤立%个实例不需要转义它们在批处理文件中的方式。)
  • "转义嵌入式""实例是唯一安全的方法,但并非大多数目标程序所期望的
    • 在Windows上,令人难以置信的是,所需的转义最终取决于目标程序 - 对于完整背景,请参阅https://stackoverflow.com/a/31413730/45375
    • 简而言之,窘迫是:
      • 如果你为目标程序转义 - 大多数,包括Perl,期望\" - 那么部分参数 list may 永远不会传递给目标程序,其余部分会导致失败,不必要的重定向到文件,或者更糟糕的是,意外执行任意命令。
      • 如果您为cmd.exe转义,则可能会破坏目标程序的解析。
      • 你无法逃避这两个
      • 如果你的命令根本不需要涉及shell,你可以解决这个问题 - 见下文。

替代方案:无shell 命令调用

如果您的命令是单个可执行文件的调用,并且所有参数都按原样传递,则根本不需要涉及shell ,其中:

  • 不需要引用参数,这会明显绕过Windows上的"引用问题
  • 通常效率更高

以下子例程适用于类Unix系统和Windows ,并且是{strong>无shell替代qx//({{1} }}),它接受命令作为参数列表调用来解释为

`...`

<强>实施例

sub qxnoshell {
  use IPC::Cmd;
  return unless @_;
  my @cmdargs = @_;
  if ($^O eq 'MSWin32') { # Windows
    # Ensure that the executable name ends in '.exe'
    $cmdargs[0] .= '.exe' unless $cmdargs[0] =~ m/\.exe$/i;
    unless (IPC::Cmd::can_run $cmdargs[0]) { # executable not found
      # Issue warning, as qx// would and open '-|' below does.
      my $warnmsg = "Executable '$cmdargs[0]' not found";
      scalar(caller) eq 'main' ? warn($warnmsg . "\n") : warnings::warnif('exec', $warnmsg);
      return; 
    }
    for (@cmdargs[1..$#cmdargs]) {
      if (m'"') {
        s/"/\\"/; # \-escape embedded double-quotes
        $_ = '"' . $_ . '"'; # enclose as a whole in embedded double-quotes
      }
    }
  }
  open my $fh, '-|', @cmdargs or return;
  my @lines = <$fh>;
  close $fh;
  return wantarray ? @lines : join('', @lines);
}
  • 由于使用# Unix: $out should receive literal '$$', which demonstrates that # /bin/sh is not involved. my $out = qxnoshell 'printf', '%s', '$$' # Windows: $out should receive literal '%USERNAME%', which demonstrates # that cmd.exe is not involved. my $out = qxnoshell 'perl', '-e', 'print "%USERNAME%"' 而需要Perl v5.9.5 +。
  • 请注意,子程序很难在Windows上运行
    • 即使参数作为列表传递,如果初始调用尝试失败,Windows上的 IPC::Cmd仍然会回到open ..., '-|' - 这同样适用于cmd.exesystem(),顺便提一下。
    • 因此,为了防止这种回退到exec() - 这会产生意想不到的后果 - 子例程(a)确保第一个列表参数是cmd.exe可执行文件,(b)尝试定位它,以及(c)只有在可以找到可执行文件时才尝试调用该命令。
    • 在Windows上,遗憾的是,包含嵌入式双引号的任何参数未正确传递到目标程序 - 它需要通过以下方式转义:(a)添加嵌入双引号到括起该参数,以及(b)将原始嵌入双引号转义为*.exe