| name | perl-security |
| description | 테인트 모드, 입력 검증, 안전한 프로세스 실행, DBI 매개변수화 쿼리, 웹 보안(XSS/SQLi/CSRF) 및 perlcritic 보안 정책을 포함하는 포괄적인 Perl 보안 가이드입니다. |
| origin | ECC |
Perl 보안 패턴
입력 검증, 인젝션 방지 및 안전한 코딩 관행을 포함하는 Perl 애플리케이션을 위한 포괄적인 보안 지침입니다.
활성화 시점
- Perl 애플리케이션에서 사용자 입력을 처리할 때
- Perl 웹 애플리케이션(CGI, Mojolicious, Dancer2, Catalyst)을 구축할 때
- 보안 취약점을 위해 Perl 코드를 검토할 때
- 사용자 제공 경로로 파일 작업을 수행할 때
- Perl에서 시스템 명령을 실행할 때
- DBI 데이터베이스 쿼리를 작성할 때
작동 방식
테인트(taint)를 인식하는 입력 경계에서 시작하여 밖으로 확장하십시오. 입력을 검증하고 테인트를 제거(untaint)하며, 파일 시스템 및 프로세스 실행을 제한하고, 모든 곳에서 매개변수화된 DBI 쿼리를 사용하십시오. 아래 예제들은 사용자 입력, 셸 또는 네트워크를 건드리는 Perl 코드를 출시하기 전에 이 스킬이 적용하기를 기대하는 안전한 기본값들을 보여줍니다.
테인트 모드 (Taint Mode)
Perl의 테인트 모드(-T)는 외부 소스의 데이터를 추적하고 명시적인 검증 없이 안전하지 않은 작업에 사용되는 것을 방지합니다.
테인트 모드 활성화
use v5.36;
my $input = $ARGV[0];
my $env_path = $ENV{PATH};
my $form = <STDIN>;
my $query = $ENV{QUERY_STRING};
$ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
테인트 제거(Untainting) 패턴
use v5.36;
sub untaint_username($input) {
if ($input =~ /^([a-zA-Z0-9_]{3,30})$/) {
return $1;
}
die "Invalid username: must be 3-30 alphanumeric characters\n";
}
sub untaint_filename($input) {
if ($input =~ m{^([a-zA-Z0-9._-]+)$}) {
return $1;
}
die "Invalid filename: contains unsafe characters\n";
}
sub bad_untaint($input) {
$input =~ /^(.*)$/s;
return $1;
}
입력 검증
블랙리스트보다 화이트리스트
use v5.36;
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;
}
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";
}
sub bad_validate($input) {
die "Invalid" if =~ ;
;
}
길이 제한
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 방지
겹치는 패턴에 중첩된 수량자(quantifier)가 있으면 치명적인 역추적(backtracking)이 발생할 수 있습니다.
use v5.36;
my $bad_re = qr/^(a+)+$/;
my $bad_re2 = qr/^([a-zA-Z]+)*$/;
my $bad_re3 = qr/^(.*?,){10,}$/;
my $good_re = qr/^a+$/;
my $good_re2 = qr/^[a-zA-Z]+$/;
my $safe_re = qr/^[a-zA-Z]++$/;
my $safe_re2 = qr/^(?>a+)$/;
use POSIX qw(alarm);
sub safe_match($string, $pattern, $timeout = 2) {
my $matched;
eval {
local $SIG{ALRM} = sub { die "Regex timeout\n" };
alarm();
= =~ ;
();
};
();
;
;
}
안전한 파일 작업
3개 인수를 사용하는 Open
use v5.36;
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;
}
sub bad_read($path) {
open my $fh, $path;
open my $fh, "< $path";
}
TOCTOU 방지 및 경로 탐색(Path Traversal)
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;
sub run_command(@cmd) {
system(@cmd) == 0
or die "Command failed: @cmd\n";
}
run_command('grep', '-r', $user_pattern, '/var/log/app/');
use IPC::Run3;
sub capture_output(@cmd) {
my ($stdout, $stderr);
run3(\@cmd, \undef, \$stdout, \$stderr);
if ($?) {
die "Command failed (exit $?): $stderr\n";
}
return $stdout;
}
sub bad_search($pattern) {
system("grep -r '$pattern' /var/log/app/");
}
my $output = `ls $user_dir`;
또한 외부 명령의 stdout/stderr를 안전하게 캡처하려면 Capture::Tiny를 사용하십시오.
SQL 인젝션 방지
DBI 플레이스홀더 (Placeholders)
use v5.36;
use DBI;
my $dbh = DBI->connect($dsn, $user, $pass, {
RaiseError => 1,
PrintError => 0,
AutoCommit => 1,
});
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({});
}
sub bad_find($dbh, $email) {
my $sth = $dbh->prepare();
->execute;
->fetchrow_hashref;
}
동적 컬럼 화이트리스트
use v5.36;
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({});
}
sub bad_order($dbh, $column) {
$dbh->prepare("SELECT * FROM users ORDER BY $column");
}
DBIx::Class (ORM 안전성)
use v5.36;
my @users = $schema->resultset('User')->search({
status => 'active',
email => { -like => '%@example.com' },
}, {
order_by => { -asc => 'name' },
rows => 50,
});
웹 보안
XSS 방지
use v5.36;
use HTML::Entities qw(encode_entities);
use URI::Escape qw(uri_escape_utf8);
sub safe_html($user_input) {
return encode_entities($user_input);
}
sub safe_url_param($value) {
return uri_escape_utf8($value);
}
use JSON::MaybeXS qw(encode_json);
sub safe_json($data) {
return encode_json($data);
}
sub bad_html($input) {
print "<div>$input</div>";
}
CSRF 보호
use v5.36;
use Crypt::URandom qw(urandom);
use MIME::Base64 qw(encode_base64url);
sub generate_csrf_token() {
return encode_base64url(urandom(32));
}
토큰을 확인할 때는 상수 시간(constant-time) 비교를 사용하십시오. 대부분의 웹 프레임워크(Mojolicious, Dancer2, Catalyst)는 내장된 CSRF 보호 기능을 제공하므로 직접 만든 솔루션보다 이를 선호하십시오.
세션 및 헤더 보안
use v5.36;
$app->secrets(['정기적으로-교체되는-길고-무작위인-비밀키']);
$app->sessions->secure(1);
$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에서 최근 출시를 확인하십시오.
- 의존성을 최소화하십시오: 각 의존성은 공격 표면이 됩니다.
보안 도구 (Security Tooling)
perlcritic 보안 정책
severity = 3
theme = security + core
[InputOutput::RequireThreeArgOpen]
severity = 5
[InputOutput::RequireCheckedSyscalls]
functions = :builtins
severity = 4
[BuiltinFunctions::ProhibitStringyEval]
severity = 5
[InputOutput::ProhibitBacktickOperators]
severity = 4
[Modules::RequireTaintChecking]
severity = 5
[InputOutput::ProhibitTwoArgOpen]
severity = 5
[InputOutput::ProhibitBarewordFileHandles]
severity = 5
perlcritic 실행
perlcritic --severity 3 --theme security lib/MyApp/Handler.pm
perlcritic --severity 3 --theme security lib/
perlcritic --severity 4 --theme security --quiet lib/ || exit 1
빠른 보안 체크리스트
| 확인 항목 | 검증 내용 |
|---|
| 테인트 모드 | CGI/웹 스크립트에서 -T 플래그 사용 여부 |
| 입력 검증 | 화이트리스트 패턴, 길이 제한 |
| 파일 작업 | 3개 인수 open, 경로 탐색 확인 |
| 프로세스 실행 | 리스트 형태의 system, 셸 보간 없음 |
| SQL 쿼리 | DBI 플레이스홀더, 절대 보간 금지 |
| HTML 출력 | encode_entities(), 템플릿 자동 이스케이프 |
| CSRF 토큰 | 생성 및 상태 변경 요청 시 검증 여부 |
| 세션 설정 | Secure, HttpOnly, SameSite 쿠키 |
| HTTP 헤더 | CSP, X-Frame-Options, HSTS |
| 의존성 | 고정된 버전, 감사된 모듈 |
| 정규식 안전성 | 중첩된 수량자 없음, 앵커 패턴 |
| 오류 메시지 | 사용자에게 스택 트레이스나 경로 노출 금지 |
안티 패턴
open my $fh, $user_input;
system("convert $user_file output.png");
$dbh->do("DELETE FROM users WHERE id = $id");
eval $user_code;
my $path = $ENV{UPLOAD_DIR};
system("ls $path");
($input) = $input =~ /(.*)/s;
print "<div>Welcome, $username!</div>";
print $cgi->redirect($user_url);
기억하십시오: Perl의 유연함은 강력하지만 규율이 필요합니다. 외부를 향하는 코드에는 테인트 모드를 사용하고, 화이트리스트로 모든 입력을 검증하며, 모든 쿼리에 DBI 플레이스홀더를 사용하고, 컨텍스트에 맞게 모든 출력을 인코딩하십시오. 심층 방어(Defense in depth)를 하십시오 — 절대 단일 레이어에 의존하지 마십시오.