跳转至

Perl 安全模式

涵盖输入验证、注入防护和安全编码实践的 Perl 应用综合安全指南。

何时激活

  • 在 Perl 应用中处理用户输入时
  • 构建 Perl Web 应用(CGI、Mojolicious、Dancer2、Catalyst)时
  • 审查 Perl 代码中的安全漏洞时
  • 使用用户指定路径执行文件操作时
  • 从 Perl 执行系统命令时
  • 编写 DBI 数据库查询时

工作原理

从支持 taint 的输入边界开始,然后向外扩展:验证并解除 taint 输入,将文件系统和进程执行保持在约束范围内,并在任何地方使用参数化 DBI 查询。以下示例展示了在发布接触用户输入、shell 或网络的 Perl 代码之前预期应用的默认做法。

taint 模式

Perl 的 taint 模式(-T)会跟踪来自外部源的数据,并防止其在未经显式验证的情况下用于不安全操作。

启用 taint 模式

#!/usr/bin/perl -T
use v5.36;

# テイントされた: プログラム外からのもの
my $input    = $ARGV[0];        # テイントされた
my $env_path = $ENV{PATH};      # テイントされた
my $form     = <STDIN>;         # テイントされた
my $query    = $ENV{QUERY_STRING}; # テイントされた

# PATHを早期にサニタイズ(テイントモードで必要)
$ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};

解除 taint 模式

use v5.36;

# Good: 特定の正規表現でバリデートしてアンテイント
sub untaint_username($input) {
    if ($input =~ /^([a-zA-Z0-9_]{3,30})$/) {
        return $1;  # $1はアンテイントされている
    }
    die "Invalid username: must be 3-30 alphanumeric characters\n";
}

# Good: ファイルパスをバリデートしてアンテイント
sub untaint_filename($input) {
    if ($input =~ m{^([a-zA-Z0-9._-]+)$}) {
        return $1;
    }
    die "Invalid filename: contains unsafe characters\n";
}

# Bad: 過度に許可的なアンテイント(目的を無効化する)
sub bad_untaint($input) {
    $input =~ /^(.*)$/s;
    return $1;  # 何でも受け入れる — 無意味
}

输入验证

允许列表优于阻止列表

use v5.36;

# Good: アローリスト — 許可されるものを正確に定義
sub validate_sort_field($field) {
    my %allowed = map { $_ => 1 } qw(name email created_at updated_at);
    die "Invalid sort field: $field\n" unless $allowed{$field};
    return $field;
}

# Good: 特定のパターンでバリデート
sub validate_email($email) {
    if ($email =~ /^([a-zA-Z0-9._%+-]+\@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/) {
        return $1;
    }
    die "Invalid email address\n";
}

sub validate_integer($input) {
    if ($input =~ /^(-?\d{1,10})$/) {
        return $1 + 0;  # 数値に強制変換
    }
    die "Invalid integer\n";
}

# Bad: ブロックリスト — 常に不完全
sub bad_validate($input) {
    die "Invalid" if $input =~ /[<>"';&|]/;  # エンコードされた攻撃を見逃す
    return $input;
}

长度约束

use v5.36;

sub validate_comment($text) {
    die "Comment is required\n"        unless length($text) > 0;
    die "Comment exceeds 10000 chars\n" if length($text) > 10_000;
    return $text;
}

安全正则表达式

ReDoS 防护

灾难性回溯会在重复模式中使用嵌套量词时发生。

use v5.36;

# Bad: ReDoSに脆弱(指数的バックトラッキング)
my $bad_re = qr/^(a+)+$/;           # ネストされた量詞
my $bad_re2 = qr/^([a-zA-Z]+)*$/;   # クラスにネストされた量詞
my $bad_re3 = qr/^(.*?,){10,}$/;    # 繰り返される貪欲/怠惰な組み合わせ

# Good: ネストなしで書き直す
my $good_re = qr/^a+$/;             # 単一の量詞
my $good_re2 = qr/^[a-zA-Z]+$/;     # クラスに単一の量詞

# Good: バックトラッキングを防ぐためにpossessive量詞またはアトミックグループを使用
my $safe_re = qr/^[a-zA-Z]++$/;             # Possessive (5.10+)
my $safe_re2 = qr/^(?>a+)$/;                # アトミックグループ

# Good: 信頼されていないパターンにタイムアウトを適用
use POSIX qw(alarm);
sub safe_match($string, $pattern, $timeout = 2) {
    my $matched;
    eval {
        local $SIG{ALRM} = sub { die "Regex timeout\n" };
        alarm($timeout);
        $matched = $string =~ $pattern;
        alarm(0);
    };
    alarm(0);
    die $@ if $@;
    return $matched;
}

安全文件操作

三参数 open

use v5.36;

# Good: 3引数open、レキシカルファイルハンドル、戻り値チェック
sub read_file($path) {
    open my $fh, '<:encoding(UTF-8)', $path
        or die "Cannot open '$path': $!\n";
    local $/;
    my $content = <$fh>;
    close $fh;
    return $content;
}

# Bad: ユーザーデータを使った2引数open(コマンドインジェクション)
sub bad_read($path) {
    open my $fh, $path;        # $pathが"|rm -rf /"なら、コマンドを実行!
    open my $fh, "< $path";   # シェルメタキャラクターインジェクション
}

TOCTOU 防护与路径遍历

use v5.36;
use Fcntl qw(:DEFAULT :flock);
use File::Spec;
use Cwd qw(realpath);

# アトミックファイル作成
sub create_file_safe($path) {
    sysopen(my $fh, $path, O_WRONLY | O_CREAT | O_EXCL, 0600)
        or die "Cannot create '$path': $!\n";
    return $fh;
}

# パスが許可されたディレクトリ内に留まることをバリデート
sub safe_path($base_dir, $user_path) {
    my $real = realpath(File::Spec->catfile($base_dir, $user_path))
        // die "Path does not exist\n";
    my $base_real = realpath($base_dir)
        // die "Base dir does not exist\n";
    die "Path traversal blocked\n" unless $real =~ /^\Q$base_real\E(?:\/|\z)/;
    return $real;
}

临时文件请使用 File::Temp(tempfile(UNLINK => 1)),并使用 flock(LOCK_EX) 防止竞态条件。

安全进程执行

列表形式的 system 和 exec

use v5.36;

# Good: リスト形式 — シェル補間なし
sub run_command(@cmd) {
    system(@cmd) == 0
        or die "Command failed: @cmd\n";
}

run_command('grep', '-r', $user_pattern, '/var/log/app/');

# Good: IPC::Run3で安全に出力をキャプチャ
use IPC::Run3;
sub capture_output(@cmd) {
    my ($stdout, $stderr);
    run3(\@cmd, \undef, \$stdout, \$stderr);
    if ($?) {
        die "Command failed (exit $?): $stderr\n";
    }
    return $stdout;
}

# Bad: 文字列形式 — シェルインジェクション!
sub bad_search($pattern) {
    system("grep -r '$pattern' /var/log/app/");  # $patternが"'; rm -rf / #"なら
}

# Bad: 補間のあるバッククォート
my $output = `ls $user_dir`;   # シェルインジェクションリスク

为了安全捕获外部命令的 stdout/stderr,也请使用 Capture::Tiny。

SQL 注入防护

DBI 占位符

use v5.36;
use DBI;

my $dbh = DBI->connect($dsn, $user, $pass, {
    RaiseError => 1,
    PrintError => 0,
    AutoCommit => 1,
});

# Good: パラメータ化クエリ — 常にプレースホルダーを使用
sub find_user($dbh, $email) {
    my $sth = $dbh->prepare('SELECT * FROM users WHERE email = ?');
    $sth->execute($email);
    return $sth->fetchrow_hashref;
}

sub search_users($dbh, $name, $status) {
    my $sth = $dbh->prepare(
        'SELECT * FROM users WHERE name LIKE ? AND status = ? ORDER BY name'
    );
    $sth->execute("%$name%", $status);
    return $sth->fetchall_arrayref({});
}

# Bad: SQLでの文字列補間(SQLi脆弱性!)
sub bad_find($dbh, $email) {
    my $sth = $dbh->prepare("SELECT * FROM users WHERE email = '$email'");
    # $emailが"' OR 1=1 --"なら、すべてのユーザーが返される
    $sth->execute;
    return $sth->fetchrow_hashref;
}

动态列白名单

use v5.36;

# Good: アローリストに対してカラム名をバリデート
sub order_by($dbh, $column, $direction) {
    my %allowed_cols = map { $_ => 1 } qw(name email created_at);
    my %allowed_dirs = map { $_ => 1 } qw(ASC DESC);

    die "Invalid column: $column\n"    unless $allowed_cols{$column};
    die "Invalid direction: $direction\n" unless $allowed_dirs{uc $direction};

    my $sth = $dbh->prepare("SELECT * FROM users ORDER BY $column $direction");
    $sth->execute;
    return $sth->fetchall_arrayref({});
}

# Bad: ユーザー選択カラムを直接補間
sub bad_order($dbh, $column) {
    $dbh->prepare("SELECT * FROM users ORDER BY $column");  # SQLi!
}

DBIx::Class(ORM 安全性)

use v5.36;

# DBIx::Classは安全なパラメータ化クエリを生成する
my @users = $schema->resultset('User')->search({
    status => 'active',
    email  => { -like => '%@example.com' },
}, {
    order_by => { -asc => 'name' },
    rows     => 50,
});

Web 安全

XSS 防护

use v5.36;
use HTML::Entities qw(encode_entities);
use URI::Escape qw(uri_escape_utf8);

# Good: HTMLコンテキスト用に出力をエンコード
sub safe_html($user_input) {
    return encode_entities($user_input);
}

# Good: URLコンテキスト用にエンコード
sub safe_url_param($value) {
    return uri_escape_utf8($value);
}

# Good: JSONコンテキスト用にエンコード
use JSON::MaybeXS qw(encode_json);
sub safe_json($data) {
    return encode_json($data);  # エスケープを処理
}

# テンプレートの自動エスケープ(Mojolicious)
# <%= $user_input %>   — 自動エスケープ(安全)
# <%== $raw_html %>    — 生の出力(危険、信頼されたコンテンツのみ)

# テンプレートの自動エスケープ(Template Toolkit)
# [% user_input | html %]  — 明示的なHTMLエンコード

# Bad: HTMLの生の出力
sub bad_html($input) {
    print "<div>$input</div>";  # $inputが<script>を含む場合XSS
}

CSRF 保护

use v5.36;
use Crypt::URandom qw(urandom);
use MIME::Base64 qw(encode_base64url);

sub generate_csrf_token() {
    return encode_base64url(urandom(32));
}

验证令牌时请使用常量时间比较。大多数 Web 框架(Mojolicious、Dancer2、Catalyst)都内置了 CSRF 保护——优先使用它们,而不是自制方案。

会话与头部安全

use v5.36;

# Mojolicousセッション + ヘッダー
$app->secrets(['long-random-secret-rotated-regularly']);
$app->sessions->secure(1);          # HTTPSのみ
$app->sessions->samesite('Lax');

$app->hook(after_dispatch => sub ($c) {
    $c->res->headers->header('X-Content-Type-Options' => 'nosniff');
    $c->res->headers->header('X-Frame-Options'        => 'DENY');
    $c->res->headers->header('Content-Security-Policy' => "default-src 'self'");
    $c->res->headers->header('Strict-Transport-Security' => 'max-age=31536000; includeSubDomains');
});

输出编码

始终将输出按其上下文进行编码:HTML 使用 HTML::Entities::encode_entities(),URL 使用 URI::Escape::uri_escape_utf8(),JSON 使用 JSON::MaybeXS::encode_json()。

CPAN 模块安全

  • 在 cpanfile 中固定版本:requires 'DBI', '== 1.643';
  • 优先选择受维护的模块:在 MetaCPAN 上查看最近发布
  • 最小化依赖:每个依赖都是攻击面

安全工具

perlcritic 安全策略

# .perlcriticrc — セキュリティ重視の設定
severity = 3
theme = security + core

# 3引数openを要求
[InputOutput::RequireThreeArgOpen]
severity = 5

# チェックされたシステムコールを要求
[InputOutput::RequireCheckedSyscalls]
functions = :builtins
severity = 4

# 文字列evalを禁止
[BuiltinFunctions::ProhibitStringyEval]
severity = 5

# バッククォート演算子を禁止
[InputOutput::ProhibitBacktickOperators]
severity = 4

# CGIでテイントチェックを要求
[Modules::RequireTaintChecking]
severity = 5

# 2引数openを禁止
[InputOutput::ProhibitTwoArgOpen]
severity = 5

# 裸のファイルハンドルを禁止
[InputOutput::ProhibitBarewordFileHandles]
severity = 5

运行 perlcritic

# ファイルをチェック
perlcritic --severity 3 --theme security lib/MyApp/Handler.pm

# プロジェクト全体をチェック
perlcritic --severity 3 --theme security lib/

# CI統合
perlcritic --severity 4 --theme security --quiet lib/ || exit 1

快速安全检查清单

检查项 确认事项
污点模式 CGI/Web 脚本的 -T 标志
输入验证 白名单模式、长度限制
文件操作 三参数 open、路径遍历检查
进程执行 列表形式的 system、无 shell 插值
SQL 查询 DBI 占位符、不要插值
HTML 输出 encode_entities()、模板自动转义
CSRF 令牌 已生成,并在状态变更请求中验证
会话设置 Secure、HttpOnly、SameSite Cookie
HTTP 头 CSP、X-Frame-Options、HSTS
依赖项 固定版本、已审计模块
正则表达式安全性 无嵌套量词、锚定模式
错误消息 不向用户泄露堆栈跟踪或路径

反模式

# 1. ユーザーデータを使った2引数open(コマンドインジェクション)
open my $fh, $user_input;               # CRITICAL脆弱性

# 2. 文字列形式のsystem(シェルインジェクション)
system("convert $user_file output.png"); # CRITICAL脆弱性

# 3. SQL文字列補間
$dbh->do("DELETE FROM users WHERE id = $id");  # SQLi

# 4. ユーザー入力でのeval(コードインジェクション)
eval $user_code;                         # リモートコード実行

# 5. サニタイズせずに$ENVを信頼する
my $path = $ENV{UPLOAD_DIR};             # 操作される可能性がある
system("ls $path");                      # 二重脆弱性

# 6. バリデーションなしにテイントを無効化
($input) = $input =~ /(.*)/s;           # 怠惰なアンテイント — 目的を無効化

# 7. HTMLでの生のユーザーデータ
print "<div>Welcome, $username!</div>";  # XSS

# 8. 未バリデートのリダイレクト
print $cgi->redirect($user_url);         # オープンリダイレクト

记住:Perl 的灵活性很强大,但需要纪律。对于面向 Web 的代码,使用污点模式,用白名单验证所有输入,在所有查询中使用 DBI 占位符,并将所有输出按其上下文进行编码。多层防御——不要依赖单一层。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 ev/ECC