From ecc
테인트 모드, 입력 검증, 안전한 프로세스 실행, DBI 매개변수화 쿼리, 웹 보안(XSS/SQLi/CSRF) 및 perlcritic 보안 정책을 포함하는 포괄적인 Perl 보안 가이드입니다.
npx claudepluginhub sam42-lab/everything-claude-code-krThis skill uses the workspace's default tool permissions.
입력 검증, 인젝션 방지 및 안전한 코딩 관행을 포함하는 Perl 애플리케이션을 위한 포괄적인 보안 지침입니다.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
입력 검증, 인젝션 방지 및 안전한 코딩 관행을 포함하는 Perl 애플리케이션을 위한 포괄적인 보안 지침입니다.
테인트(taint)를 인식하는 입력 경계에서 시작하여 밖으로 확장하십시오. 입력을 검증하고 테인트를 제거(untaint)하며, 파일 시스템 및 프로세스 실행을 제한하고, 모든 곳에서 매개변수화된 DBI 쿼리를 사용하십시오. 아래 예제들은 사용자 입력, 셸 또는 네트워크를 건드리는 Perl 코드를 출시하기 전에 이 스킬이 적용하기를 기대하는 안전한 기본값들을 보여줍니다.
Perl의 테인트 모드(-T)는 외부 소스의 데이터를 추적하고 명시적인 검증 없이 안전하지 않은 작업에 사용되는 것을 방지합니다.
#!/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)};
use v5.36;
# 좋음: 특정 정규식으로 검증 및 테인트 제거
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";
}
# 좋음: 파일 경로 검증 및 테인트 제거
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 $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;
}
겹치는 패턴에 중첩된 수량자(quantifier)가 있으면 치명적인 역추적(backtracking)이 발생할 수 있습니다.
use v5.36;
# 나쁨: ReDoS에 취약 (지수적 역추적)
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]+$/; # 클래스에 단일 수량자
# 좋음: 소유적 수량자(possessive quantifiers) 또는 원자적 그룹(atomic groups)을 사용하여 역추적 방지
my $safe_re = qr/^[a-zA-Z]++$/; # 소유적 (5.10+)
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($timeout);
$matched = $string =~ $pattern;
alarm(0);
};
alarm(0);
die $@ if $@;
return $matched;
}
use v5.36;
# 좋음: 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;
}
# 나쁨: 사용자 데이터가 포함된 2개 인수 open (명령어 인젝션)
sub bad_read($path) {
open my $fh, $path; # $path가 "|rm -rf /"라면 명령 실행!
open my $fh, "< $path"; # 셸 메타문자 인젝션
}
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)를 사용하십시오.
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/');
# 좋음: 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;
}
# 나쁨: 문자열 형태 — 셸 인젝션!
sub bad_search($pattern) {
system("grep -r '$pattern' /var/log/app/"); # $pattern이 "'; rm -rf / #"인 경우
}
# 나쁨: 보간이 포함된 백틱 (Backticks)
my $output = `ls $user_dir`; # 셸 인젝션 위험
또한 외부 명령의 stdout/stderr를 안전하게 캡처하려면 Capture::Tiny를 사용하십시오.
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({});
}
# 나쁨: 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;
# 좋음: 컬럼 이름을 화이트리스트로 검증
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"); # SQLi!
}
use v5.36;
# DBIx::Class는 안전한 매개변수화된 쿼리를 생성함
my @users = $schema->resultset('User')->search({
status => 'active',
email => { -like => '%@example.com' },
}, {
order_by => { -asc => 'name' },
rows => 50,
});
use v5.36;
use HTML::Entities qw(encode_entities);
use URI::Escape qw(uri_escape_utf8);
# 좋음: HTML 컨텍스트를 위해 출력 인코딩
sub safe_html($user_input) {
return encode_entities($user_input);
}
# 좋음: URL 컨텍스트를 위해 인코딩
sub safe_url_param($value) {
return uri_escape_utf8($value);
}
# 좋음: 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 인코딩
# 나쁨: HTML 내의 원본 출력
sub bad_html($input) {
print "<div>$input</div>"; # $input에 <script>가 포함된 경우 XSS
}
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;
# Mojolicious 세션 + 헤더
$app->secrets(['정기적으로-교체되는-길고-무작위인-비밀키']);
$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().
requires 'DBI', '== 1.643';# .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
# Bare-word 파일핸들 금지
[InputOutput::ProhibitBarewordFileHandles]
severity = 5
# 파일 확인
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/웹 스크립트에서 -T 플래그 사용 여부 |
| 입력 검증 | 화이트리스트 패턴, 길이 제한 |
| 파일 작업 | 3개 인수 open, 경로 탐색 확인 |
| 프로세스 실행 | 리스트 형태의 system, 셸 보간 없음 |
| SQL 쿼리 | DBI 플레이스홀더, 절대 보간 금지 |
| HTML 출력 | encode_entities(), 템플릿 자동 이스케이프 |
| CSRF 토큰 | 생성 및 상태 변경 요청 시 검증 여부 |
| 세션 설정 | Secure, HttpOnly, SameSite 쿠키 |
| HTTP 헤더 | CSP, X-Frame-Options, HSTS |
| 의존성 | 고정된 버전, 감사된 모듈 |
| 정규식 안전성 | 중첩된 수량자 없음, 앵커 패턴 |
| 오류 메시지 | 사용자에게 스택 트레이스나 경로 노출 금지 |
# 1. 사용자 데이터가 포함된 2개 인수 open (명령어 인젝션)
open my $fh, $user_input; # 치명적인 취약점
# 2. 문자열 형태의 system (셸 인젝션)
system("convert $user_file output.png"); # 치명적인 취약점
# 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의 유연함은 강력하지만 규율이 필요합니다. 외부를 향하는 코드에는 테인트 모드를 사용하고, 화이트리스트로 모든 입력을 검증하며, 모든 쿼리에 DBI 플레이스홀더를 사용하고, 컨텍스트에 맞게 모든 출력을 인코딩하십시오. 심층 방어(Defense in depth)를 하십시오 — 절대 단일 레이어에 의존하지 마십시오.