Perl Security

by affaan-mef648e01899bNo license275K starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 3 days ago

テイントモード、入力バリデーション、安全なプロセス実行、DBIパラメータ化クエリ、Webセキュリティ(XSS/SQLi/CSRF)、perlcriticセキュリティポリシーを網羅する包括的なPerlセキュリティ。

AI-generated overview

Guidelines for writing secure Perl code, covering taint mode, input validation, injection prevention and perlcritic policy.

What it does
This skill provides a reference guide of Perl security patterns: taint mode and untainting, allowlist input validation, ReDoS-safe regular expressions, safe file and process handling, DBI parameterized queries, and web protections against XSS, CSRF and header issues. It also includes a perlcritic security configuration, a quick checklist and a list of anti-patterns. It produces guidance and code examples rather than running tools.
When to use it
Use it when handling user input in Perl, building or reviewing Perl web applications such as CGI, Mojolicious, Dancer2 or Catalyst, or writing DBI queries. It also fits reviews of file operations, system command execution and output encoding in Perl code.
Requirements
No scripts or runtime are required; it is an instructions-only reference. Applying the examples assumes a Perl environment with modules such as DBI, HTML::Entities, URI::Escape, JSON::MaybeXS, File::Temp and IPC::Run3, and optionally perlcritic for the policy checks.

Perlセキュリティパターン

入力バリデーション、インジェクション防止、セキュアコーディングプラクティスを網羅するPerlアプリケーションの包括的なセキュリティガイドライン。

アクティベートするタイミング

  • Perlアプリケーションでユーザー入力を処理するとき
  • PerlのWebアプリケーション(CGI、Mojolicious、Dancer2、Catalyst)を構築するとき
  • セキュリティ脆弱性についてPerlコードをレビューするとき
  • ユーザー指定パスでファイル操作を実行するとき
  • PerlからシステムコマンドをExecuteするとき
  • DBIデータベースクエリを書くとき

仕組み

テイント対応の入力境界から始め、次に外側に移動する: 入力をバリデートしてアンテイントし、ファイルシステムとプロセス実行を制約内に保ち、どこでもパラメータ化されたDBIクエリを使用する。以下の例は、ユーザー入力、シェル、またはネットワークに触れるPerlコードをリリースする前に適用することが期待されるデフォルトを示す。

テイントモード

Perlのテイントモード(-T)は外部ソースからのデータを追跡し、明示的なバリデーションなしに安全でない操作で使用されることを防ぐ。

テイントモードの有効化

perl
#!/usr/bin/perl -Tuse 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)};

アンテイントパターン

perl
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;  # 何でも受け入れる — 無意味}

入力バリデーション

ブロックリストよりアローリスト

perl
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;}

長さ制約

perl
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防止

壊滅的なバックトラッキングは重複するパターンにネストされた量詞が使用されるときに発生する。

perl
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;}

安全なファイル操作

3引数open

perl
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防止とパストラバーサル

perl
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

perl
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プレースホルダー

perl
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;}

動的カラムアローリスト

perl
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安全性)

perl
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防止

perl
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保護

perl
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保護がある — 手作りのソリューションよりそれらを優先する。

セッションとヘッダーセキュリティ

perl
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セキュリティポリシー

ini
# .perlcriticrc — セキュリティ重視の設定severity = 3theme = security + core
# 3引数openを要求[InputOutput::RequireThreeArgOpen]severity = 5
# チェックされたシステムコールを要求[InputOutput::RequireCheckedSyscalls]functions = :builtinsseverity = 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の実行

bash
# ファイルをチェック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フラグ
入力バリデーションアローリストパターン、長さ制限
ファイル操作3引数open、パストラバーサルチェック
プロセス実行リスト形式のsystem、シェル補間なし
SQLクエリDBIプレースホルダー、補間しない
HTML出力encode_entities()、テンプレート自動エスケープ
CSRFトークン生成され、状態変更リクエストで検証される
セッション設定Secure、HttpOnly、SameSiteクッキー
HTTPヘッダーCSP、X-Frame-Options、HSTS
依存関係ピン留めされたバージョン、監査されたモジュール
正規表現の安全性ネストされた量詞なし、アンカーされたパターン
エラーメッセージスタックトレースやパスがユーザーに漏れない

アンチパターン

perl
# 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プレースホルダーを使用し、すべての出力をそのコンテキスト用にエンコードする。多層防御 — 単一の層に依存しない。

Source and attribution

Source:affaan-m/eccindocs/ja-JP/skills/perl-securityat commitef648e0

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal