Skill v1.0.0
currentAutomated scan96/100version: "1.0.0" 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)는 외부 소스의 데이터를 추적하고 명시적인 검증 없이 안전하지 않은 작업에 사용되는 것을 방지합니다.
테인트 모드 활성화
#!/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)};
테인트 제거(Untainting) 패턴
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;}
안전한 정규 표현식
ReDoS 방지
겹치는 패턴에 중첩된 수량자(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;}
안전한 파일 작업
3개 인수를 사용하는 Open
use v5.36;# 좋음: 3개 인수 open, 렉시컬 파일핸들, 반환값 확인sub read_file($path) {open my $fh, '<:encoding(UTF-8)', $pathor 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"; # 셸 메타문자 인젝션}
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) == 0or 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를 사용하십시오.
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({});}# 나쁨: 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!}
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,});
웹 보안
XSS 방지
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}
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;# 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().
CPAN 모듈 보안
- cpanfile에서 버전을 고정하십시오:
requires 'DBI', '== 1.643'; - 유지보수되는 모듈을 선호하십시오: MetaCPAN에서 최근 출시를 확인하십시오.
- 의존성을 최소화하십시오: 각 의존성은 공격 표면이 됩니다.
보안 도구 (Security Tooling)
perlcritic 보안 정책
# .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# Bare-word 파일핸들 금지[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/웹 스크립트에서 -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)를 하십시오 — 절대 단일 레이어에 의존하지 마십시오.