Perl 보안
Perl 보안 (perlsec)
Perl은 setuid나 setgid처럼 추가 권한을 가지고 돌아갈 때도 안전하게 프로그래밍하기 쉽도록 설계된 언어예요. 스크립트의 각 줄마다 여러 번 치환을 거치는 대부분의 명령행 셸과 달리, Perl은 더 일반적인 평가 방식을 써서 숨은 위험이 적어요. 게다가 언어 자체에 내장 기능이 많아서 목적을 달성할 때 외부(그리고 신뢰할 수 없을 수도 있는) 프로그램에 덜 의존할 수 있어요.
보안 취약점 신고 연락처 (SECURITY VULNERABILITY CONTACT INFORMATION)
Perl 인터프리터나 Perl 코어 코드베이스에서 관리되는 모듈에서 보안 취약점을 발견했다고 생각되면, 그 내용을 [email protected]로 이메일을 보내세요. 이 주소는 Perl 보안 팀이 모니터링하는 폐쇄 회원 메일링 리스트예요.
추가 정보는 perlsecpolicy를 참고하세요.
보안 메커니즘과 고려 사항 (SECURITY MECHANISMS AND CONCERNS)
Taint 모드 (Taint mode)
기본적으로 Perl은 프로그램이 실제(real)와 유효(effective) 사용자/그룹 ID가 다른 상태로 실행되고 있음을 감지하면, taint 모드라고 하는 일련의 특별한 보안 검사를 자동으로 켜요. Unix 권한에서 setuid 비트는 모드 04000, setgid 비트는 02000이에요. 둘 중 하나 또는 둘 다 설정될 수 있죠. taint 모드는 -T 명령행 플래그로 명시적으로 켤 수도 있어요. 이 플래그는 서버 프로그램이나 CGI 스크립트처럼 다른 사람을 대신해 실행되는 프로그램에 강력히 권장돼요. taint 모드가 켜지면 스크립트의 나머지 동안 계속 켜져 있어요.
이 모드에서 Perl은 **taint 검사(taint check)**라고 하는 특별한 예방 조치를 취해 분명한 함정과 은근한 함정을 모두 막아요. 어떤 검사는 상당히 단순해요 — 예를 들어 경로 디렉터리가 다른 사람이 쓸 수 없는지 확인하는 것처럼요. 신중한 프로그래머는 늘 이런 검사를 써 왔죠. 하지만 다른 검사는 언어 자체가 가장 잘 지원해요. 특히 그런 검사 덕분에 set-id Perl 프로그램은 대응하는 C 프로그램보다 더 안전해져요.
taint 검사 지원은 taint 기능을 쓰지 않더라도 모든 Perl 프로그램에 오버헤드를 더해요. Perl 5.18에서는 perl을 빌드할 때 taint 기능을 끌 수 있는 C 전처리기 심볼이 도입됐어요.
taint가 켜지면 프로그램 밖에서 온 데이터로 프로그램 밖의 다른 무언가에 영향을 줄 수 없어요 — 적어도 우연히는 불가능하죠. 모든 명령행 인자, 환경 변수, locale 정보(perllocale 참고), 특정 시스템 호출의 결과(readdir(), readlink(), shmread() 변수, msgrcv()가 반환한 메시지, getpwxxx() 호출이 반환한 password/gcos/shell 필드), 그리고 모든 파일 입력이 'tainted'로 표시돼요. tainted 데이터는 서브셸을 호출하는 어떤 명령에서도, 파일·디렉터리·프로세스를 수정하는 어떤 명령에서도 직간접적으로 쓸 수 없어요. 다만 다음 예외가 있어요:
print와syswrite의 인자는 tainted 여부를 검사하지 않아요.- 심볼릭 메서드
$obj->$method(@args);
와 심볼릭 서브 참조
&{$foo}(@args);
$foo->(@args);
는 tainted 여부를 검사하지 않아요. 이는 외부 데이터가 제어 흐름에 영향을 주지 않게 하려면 각별한 주의가 필요하다는 뜻이에요. 이 심볼릭 값들의 범위를 신중히 제한하지 않으면, 사람들이 POSIX::system처럼 Perl 코드 밖의 함수를 호출할 수 있고, 그러면 임의의 외부 코드를 실행할 수 있게 돼요.
- 해시 키는 절대 tainted가 아니에요.
효율성 때문에 Perl은 데이터가 tainted인지에 대해 보수적인 관점을 취해요. 표현식에 tainted 데이터가 포함되면, 하위 표현식의 값 자체가 tainted 데이터의 영향을 받지 않더라도 어떤 하위 표현식이든 tainted로 간주될 수 있어요.
tainted 여부는 각 스칼라 값에 연관되므로 배열이나 해시의 어떤 요소는 tainted이고 다른 요소는 아닐 수 있어요. 해시의 키는 절대 tainted가 아니에요.
예를 들어:
$arg = shift; # $arg is tainted
$hid = $arg . 'bar'; # $hid is also tainted
$line = <>; # Tainted
$line = <STDIN>; # Also tainted
open FOO, "/home/me/bar" or die $!;
$line = <FOO>; # Still tainted
$path = $ENV{'PATH'}; # Tainted, but see below
$data = 'abc'; # Not tainted
system "echo $arg"; # Insecure
system "/bin/echo", $arg; # Considered insecure
# (Perl doesn't know about /bin/echo)
system "echo $hid"; # Insecure
system "echo $data"; # Insecure until PATH set
$path = $ENV{'PATH'}; # $path now tainted
$ENV{'PATH'} = '/bin:/usr/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
$path = $ENV{'PATH'}; # $path now NOT tainted
system "echo $data"; # Is secure now!
open(FOO, "< $arg"); # OK - read-only file
open(FOO, "> $arg"); # Not OK - trying to write
open(FOO,"echo $arg|"); # Not OK
open(FOO,"-|")
or exec 'echo', $arg; # Also not OK
$shout = `echo $arg`; # Insecure, $shout now tainted
unlink $data, $arg; # Insecure
umask $arg; # Insecure
exec "echo $arg"; # Insecure
exec "echo", $arg; # Insecure
exec "sh", '-c', $arg; # Very insecure!
@files = <*.c>; # insecure (uses readdir() or similar)
@files = glob('*.c'); # insecure (uses readdir() or similar)
# In either case, the results of glob are tainted, since the list of
# filenames comes from outside of the program.
$bad = ($arg, 23); # $bad will be tainted
$arg, `true`; # Insecure (although it isn't really)
안전하지 않은 일을 시도하면 "Insecure dependency"나 "Insecure $ENV{PATH}" 같은 말을 담은 치명적 오류를 만나게 돼요.
**"한 개의 tainted 값이 표현식 전체를 taint시킨다"**는 원칙의 예외는 삼항 조건 연산자 ?:이에요. 삼항 조건이 있는 코드
$result = $tainted_value ? "Untainted" : "Also untainted";
는 사실상
if ( $tainted_value ) {
$result = "Untainted";
} else {
$result = "Also untainted";
}
와 같으므로 $result가 tainted일 이유가 없어요.
Tainted 데이터 세탁과 탐지 (Laundering and Detecting Tainted Data)
변수가 tainted 데이터를 담고 있는지(그래서 쓰면 "Insecure dependency" 메시지가 뜰지) 검사하려면 Scalar::Util 모듈의 tainted() 함수를 쓰면 돼요. 이 모듈은 가까운 CPAN 미러에서 구할 수 있고, 5.8.0 릴리스부터 Perl에 포함됐어요. 또는 다음 is_tainted() 함수를 쓸 수도 있어요:
sub is_tainted {
local $@; # Don't pollute caller's value.
return ! eval { eval("#" . substr(join("", @_), 0, 0)); 1 };
}
이 함수는 표현식 어디에든 tainted 데이터가 있으면 그 표현식 전체가 tainted가 된다는 사실을 이용해요. 모든 연산자가 모든 인자의 tainted 여부를 검사하는 건 비효율적이겠죠. 대신 조금 더 효율적이고 보수적인 접근, 즉 같은 표현식 안에서 tainted 값에 한 번이라도 접근했으면 전체 표현식을 tainted로 보는 방식을 써요.
하지만 tainted 여부 검사로는 할 수 있는 게 한계가 있어요. 때로는 데이터의 tainted 성질을 그냥 없애버려야 해요. 값을 해시의 키로 쓰면 untaint 될 수 있어요. 그 외에 taint 메커니즘을 우회하는 유일한 방법은 정규식 매치에서 하위 패턴(subpattern)을 참조하는 거예요. Perl은 non-tainting 패턴에서 $1, $2 등으로 부분 문자열을 참조한다면, 그 패턴을 쓸 때 뭘 하는지 알고 있었을 거라고 가정해요. 즉 약간의 생각이 필요하다는 뜻이에요 — 아무 것이나 무작정 untaint하면 메커니즘 전체를 무너뜨리는 거예요. 어떤 나쁜 문자가 있는지 검사하는 것보다 좋은 문자만 있는지("good"의 특정 의미에 대해) 검증하는 게 나아요. 생각지도 못한 나쁜 문자를 놓치기가 너무 쉽기 때문이에요.
다음은 데이터에 "단어" 문자(알파벳·숫자·밑줄), 하이픈, at 기호, 점 이외에는 아무것도 없다는 걸 확인하는 테스트예요:
if ($data =~ /^([-\@\w.]+)$/) {
$data = $1; # $data now untainted
} else {
die "Bad data in '$data'"; # log this somewhere
}
\w+는 보통 셸 메타문자와 매치하지 않고, 점·하이픈·at도 셸에서 특별한 의미를 갖지 않으므로 꽤 안전해요. /.+/를 쓰는 건 이론상 전부 통과시키므로 불안전하지만, Perl은 그걸 검사하지 않아요. 교훈은 — untaint 할 때 패턴에 각별히 주의해야 한다는 거예요. 정규식을 사용해 데이터를 세탁하는 것이, 아래에 설명할 더 낮은 권한의 자식 프로세스를 fork하는 전략을 쓰지 않는 한, 더러운 데이터를 untaint하는 유일한 메커니즘이에요.
use locale이 적용 중이면 위 예는 $data를 untaint하지 않아요. \w가 매치하는 문자가 locale에 의해 결정되거든요. Perl은 locale 정의가 프로그램 밖의 데이터를 담고 있으므로 신뢰할 수 없다고 봐요. locale을 인식하는 프로그램을 짜고 \w가 든 정규식으로 데이터를 세탁하고 싶다면, 같은 블록 안에서 그 표현식 앞에 no locale을 두세요. 자세한 논의와 예는 perllocale의 "SECURITY"를 참고하세요.
"#!" 줄의 스위치 (Switches On the "#!" Line)
스크립트를 실행 가능하게 만들어 명령으로 쓸 수 있게 하면, 시스템은 스크립트의 #! 줄에서 perl로 스위치를 넘겨줘요. Perl은 setuid(또는 setgid) 스크립트에 주어진 명령행 스위치가 실제로 #! 줄에 설정된 것과 일치하는지 검사해요. 일부 Unix 및 Unix 계열 환경은 #! 줄에 스위치 하나만 허용하는 제한을 두므로, 그런 시스템에서는 -w -U 대신 -wU처럼 써야 할 수 있어요. (이 문제는 #!과 setuid/setgid 스크립트를 지원하는 Unix/Unix 계열 환경에서만 생겨요.)
Taint 모드와 @INC (Taint mode and @INC)
taint 모드(-T)가 적용 중이면 환경 변수 PERL5LIB, PERLLIB, PERL_USE_UNSAFE_INC는 Perl이 무시해요. 프로그램 밖에서 -I 명령행 옵션으로 여전히 @INC를 조정할 수 있는데, 이는 perlrun에 설명돼 있어요. 두 환경 변수는 가려져 있어서 무시돼요 — 프로그램을 실행하는 사용자가 그 변수가 설정됐는지 모를 수 있기 때문이에요. 반면 -I 옵션은 명확히 보이므로 허용돼요.
프로그램을 수정하지 않고 @INC를 바꾸는 또 다른 방법은 lib pragma를 쓰는 거예요:
perl -Mlib=/foo program
-Mlib=/foo가 -I/foo보다 나은 점은, 전자가 중복된 디렉터리를 자동으로 제거하는 반면 후자는 그렇지 않다는 거예요.
참고로 tainted 문자열이 @INC에 추가되면 다음 문제가 보고돼요:
Insecure dependency in require while running with -T switch
5.26 이전의 Perl 버전에서는 taint 모드를 켜면 기본 @INC 값에서 현재 디렉터리(.)도 제거됐어요. 5.26부터는 현재 디렉터리가 기본적으로 @INC에 포함되지 않아요.
PATH 청소 (Cleaning Up Your Path)
"Insecure $ENV{PATH}" 메시지가 나면, $ENV{'PATH'}를 알려진 값으로 설정해야 해요. 그리고 경로의 각 디렉터리는 절대 경로여야 하고, 그 소유자와 그룹 외에는 쓸 수 없어야 해요. 유닉스 계열 환경에서는 PATH의 빈 구성 요소가 .(로컬 디렉터리)으로 해석될 수 있는데, 이것도 이 메시지를 유발할 수 있어요. 실행 파일의 경로명이 완전한데도 이 메시지가 나와서 놀랄 수 있어요. 이는 프로그램에 전체 경로를 안 줘서가 아니라, PATH 환경 변수를 설정하지 않았거나 안전한 값으로 설정하지 않아서 생겨요. Perl은 문제의 실행 파일이 자체적으로 다시 PATH에 의존하는 다른 프로그램을 실행할지 보장할 수 없으므로, PATH를 설정하도록 강제하는 거예요.
PATH만이 문제를 일으킬 수 있는 환경 변수는 아니에요. 일부 셸은 IFS, CDPATH, ENV, BASH_ENV 변수를 사용할 수 있으므로, Perl은 서브프로세스를 시작할 때 이들이 비어 있거나 untaint됐는지 검사해요. setid 및 taint 검사 스크립트에 다음과 같은 것을 추가하는 걸 고려해 보세요:
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; # Make %ENV safer
tainted 값을 쓰는지 신경 쓰지 않는 다른 연산으로도 곤란해질 수 있어요. 사용자가 제공한 파일 이름을 다룰 때는 파일 테스트를 분별 있게 쓰세요. 가능하면 제대로 특수 사용자(또는 그룹!) 권한을 내려놓은 뒤에 open 등을 수행하세요. Perl이 tainted 파일 이름을 읽기 위해 여는 것을 막지는 않으므로, 무엇을 출력하는지 조심하세요. taint 메커니즘은 어리석은 실수를 막기 위한 것이지, 생각할 필요를 없애주는 게 아니에요.
system과 exec에 셸 와일드카드가 든 문자열 대신 명시적 인자 목록을 넘기면 Perl은 와일드카드를 확장하려고 셸을 호출하지 않아요. 불행히도 open, glob, 백틱 함수는 그런 대체 호출 규약을 제공하지 않아서, 더 많은 속임수가 필요해요.
Perl은 setuid/setgid 프로그램에서 파일이나 파이프를 여는 합리적으로 안전한 방법을 제공해요. 더러운 일을 대신 해줄 권한을 낮춘 자식 프로세스를 만들면 돼요. 먼저 부모와 자식을 파이프로 연결하는 특수 open 문법으로 자식을 fork해요. 이제 자식은 자신의 ID 집합과 환경 변수·umask·현재 작업 디렉터리 같은 다른 프로세스별 속성들을 원래 또는 알려진 안전한 값으로 되돌려요. 그러면 특별한 권한이 더 이상 없는 자식 프로세스가 open이나 다른 시스템 호출을 수행해요. 마지막으로 자식은 접근에 성공한 데이터를 부모에게 돌려줘요. 파일·파이프가 부모보다 낮은 권한으로 도는 자식에서 열렸으므로, 하면 안 되는 일을 하도록 속임당할 가능성이 낮아요.
백틱을 합리적으로 안전하게 하는 방법이 하나 있어요. exec가 셸이 확장할 수 있는 문자열로 호출되지 않음을 주목하세요. 셸 이스케이프에 노출될 수 있는 무언가를 호출할 때, 아예 셸을 부르지 않는 것이 가장 좋은 방법이에요.
use English;
die "Can't fork: $!" unless defined($pid = open(KID, "-|"));
if ($pid) { # parent
while (<KID>) {
# do something
}
close KID;
} else {
my @temp = ($EUID, $EGID);
my $orig_uid = $UID;
my $orig_gid = $GID;
$EUID = $UID;
$EGID = $GID;
# Drop privileges
$UID = $orig_uid;
$GID = $orig_gid;
# Make sure privs are really gone
($EUID, $EGID) = @temp;
die "Can't drop privileges"
unless $UID == $EUID && $GID eq $EGID;
$ENV{PATH} = "/bin:/usr/bin"; # Minimal PATH.
# Consider sanitizing the environment even more.
exec 'myprog', 'arg1', 'arg2'
or die "can't exec myprog: $!";
}
비슷한 전략이 glob을 통한 와일드카드 확장에도 동작해요. 다만 readdir을 쓸 수도 있어요.
taint 검사는 자신이 '농장을 통째로 내줄' 프로그램을 쓰지 않았다고 믿어도, 결국 그 프로그램을 쓰는 사람들이 나쁜 짓을 하도록 속이려 들지 않는다고 믿을 수 없을 때 가장 유용해요. 이는 set-id 프로그램과 CGI 프로그램처럼 남의 대신 실행되는 프로그램에 유용한 보안 검사예요.
하지만 이는 코드 작성자조차 악한 짓을 하려 들지 않는다고 믿지 못하는 상황과는 상당히 달라요. 누군가 한 번도 본 적 없는 프로그램을 건네며 "자, 이거 실행해 봐"라고 하는 상황이 바로 그런 신뢰가 필요한 경우죠. 그런 안전을 위해 Perl 배포판에 표준으로 포함된 Safe 모듈을 확인해 볼 수 있어요. 이 모듈은 프로그래머가 모든 시스템 연산이 가둬지고 네임스페이스 접근이 신중히 제어되는 특별한 격실(compartment)을 설정할 수 있게 해줘요. 다만 Safe가 방탄이라고 생각하면 안 돼요 — 외부 코드가 무한 루프를 만들거나, 기가바이트의 메모리를 할당하거나, perl의 버그를 악용해 호스트 인터프리터를 죽이거나 예측 불가하게 행동하게 만드는 것을 막진 못해요. 어쨌든 보안이 정말 걱정된다면 피하는 게 나아요.
Shebang 경합 조건 (Shebang Race Condition)
스크립트처럼 유연한 시스템에 특별한 권한을 주는 데서 오는 명백한 문제들 말고도, 많은 Unix 버전에서 set-id 스크립트는 처음부터 본질적으로 불안전해요. 문제는 커널의 **경합 조건(race condition)**이에요. 커널이 어떤 인터프리터를 실행할지 보려고 파일을 여는 시점과, (이제 set-id가 된) 인터프리터가 그 파일을 다시 열어 해석하려 하는 시점 사이에 해당 파일이 바뀔 수 있어요. 특히 시스템에 심볼릭 링크가 있다면 더욱 그렇죠.
일부 Unix, 특히 최신 것들은 이 본질적 보안 버그가 없어요. 그런 시스템에서 커널은 set-id 스크립트의 이름을 인터프리터에 넘길 때, 간섭받기 쉬운 경로명 대신 /dev/fd/3을 넘겨요. 이는 이미 스크립트에 대해 열려 있는 특수 파일이라, 악성 스크립트가 악용할 경합 조건이 없어요. 그런 시스템에서는 Perl을 -DSETUID_SCRIPTS_ARE_SECURE_NOW로 컴파일해야 해요. Perl을 빌드하는 Configure 프로그램이 이를 스스로 알아내려 하므로 직접 지정할 일은 거의 없어요. 대부분의 최신 SysVr4와 BSD 4.4 릴리스는 커널 경합 조건을 피하려고 이 방식을 써요.
안전한 버전의 set-id 스크립트가 없어도 모든 건 잃은 게 아니에요. 때로 이 커널 "기능"을 비활성화할 수 있어요. 그러면 커널이 set-id 스크립트를 set-id로 실행하지 않거나, 아예 실행하지 않아요. 두 경우 모두 경합 조건의 악용 가능성을 피하지만, 실제로 set-id로 스크립트를 실행하는 데는 도움이 안 돼요.
커널 set-id 스크립트 기능이 비활성화되어 있지 않다면, 어떤 set-id 스크립트든 악용 가능한 취약점을 제공해요. Perl이 악용을 피할 수는 없지만, 가능한 곳에서 취약한 스크립트를 지적해 줄 순 있어요. Perl이 set-id 스크립트에 적용되고 있음을 감지하면 "당신의 set-id 스크립트는 불안전하다"고 크게 불평하며 실행하지 않아요. Perl이 불평하면 취약점을 없애기 위해 스크립트에서 set-id 비트를 제거해야 해요. 스크립트 실행 거부 자체로 취약점이 닫히는 건 아니에요. 그저 Perl이 그렇게 하도록 권장하는 것뿐이에요.
안전한 set-id 스크립트 버전이 없다면, 스크립트를 set-id로 실제 실행하려면 **C 래퍼(wrapper)**를 스크립트 주위에 두어야 해요. C 래퍼는 Perl 프로그램을 호출하는 것 외에는 아무것도 안 하는 컴파일된 프로그램이에요. 컴파일된 프로그램은 set-id 스크립트를 괴롭히는 커널 버그에 영향을 받지 않아요. 간단한 C 래퍼:
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#define REAL_PATH "/path/to/script"
int main(int argc, char **argv)
{
execv(REAL_PATH, argv);
fprintf(stderr, "%s: %s: %s\n",
argv[0], REAL_PATH, strerror(errno));
return 127;
}
이 래퍼를 바이너리 실행 파일로 컴파일한 다음, 스크립트 대신 그 바이너리를 setuid나 setgid로 만들어요. 주의할 점은 이 래퍼는 스크립트로 가는 안전한 경로를 보장하는 것 외에는 실행 환경을 정화하지 않아요. shebang 경합 조건만 피할 뿐이에요. 스크립트를 set-id로 안전하게 실행하는 일은 Perl 자체의 기능과 스크립트 스스로의 주의에 기대요.
프로그램 보호 (Protecting Your Programs)
Perl 프로그램의 소스를 숨기는 방법은 여러 가지가 있는데, 각각 "보안" 수준이 달라요.
우선, 읽기 권한을 빼앗을 수는 없어요. 소스 코드는 컴파일·해석되려면 읽을 수 있어야 하거든요. (그렇다고 웹 사용자들이 CGI 스크립트의 소스를 볼 수 있다는 뜻은 아니에요.) 그래서 권한을 친숙한 0755 수준으로 남겨둘 수밖에 없어요. 이러면 로컬 시스템의 사람들만 소스를 볼 수 있어요.
어떤 사람들은 이를 실수로 보안 문제로 여겨요. 프로그램이 불안전한 일을 하고, 사람들이 그 불안전성을 악용하는 법을 모르는 것에 의존한다면, 그건 안전한 게 아니에요. 소스를 보지 않고도 불안전한 부분을 알아내고 악용하는 것이 가능한 경우가 많아요. 버그를 고치는 대신 숨기는 것(security through obscurity)은 정말로 약한 보안이에요.
소스 필터를 통한 암호화를 시도할 수 있어요 (Filter::* CPAN, 또는 Perl 5.8부터 Filter::Util::Call과 Filter::Simple). 하지만 크래커가 복호화할 수도 있어요. 아래 설명하는 바이트 코드 컴파일러·인터프리터를 써 볼 수도 있지만, 크래커가 디컴파일할 수 있어요. 네이티브 코드 컴파일러를 써 볼 수도 있지만, 크래커가 디스어셈블할 수 있어요. 이들은 코드를 얻으려는 사람에게 각기 다른 난이도를 제공하지만, 어느 것도 확실히 숨길 순 없어요. (이건 Perl뿐 아니라 모든 언어에서 마찬가지예요.)
사람들이 코드로 이윤을 얻는 게 걱정된다면, 결론은 제한적인 라이선스만이 합법적 보안을 준다는 거예요. 소프트웨어에 라이선스를 붙이고 "이것은 XYZ Corp의 미공개 독점 소프트웨어입니다. 접근이 사용 권한을 주는 건 아닙니다 어쩌구저쩌구" 같은 위협적인 문구를 넣으세요. 라이선스 문구가 법정에서 유지될지 확실히 하려면 변호사를 만나야 해요.
Unicode
Unicode는 새롭고 복잡한 기술이라 특정 보안 함정을 쉽게 간과할 수 있어요. 개요는 perluniintro, 자세한 내용은 perlunicode, 특히 보안 함의는 perlunicode의 "Security Implications of Unicode"를 참고하세요.
알고리즘 복잡도 공격 (Algorithmic Complexity Attacks)
Perl 구현에 쓰이는 특정 내부 알고리즘은 입력을 신중히 골라서 시간이나 공간(또는 둘 다)을 크게 소모하게 만들 수 있어요. 이는 이른바 서비스 거부(DoS) 공격으로 이어질 수 있어요.
-
해시 알고리즘 — Perl이 쓰는 것 같은 해시 알고리즘은 해시 함수에 대한 **충돌 공격(collision attack)**에 취약한 것으로 잘 알려져 있어요. 이런 공격은 같은 버킷에 충돌하는 키 집합을 구성해 비효율적인 동작을 만들어내요. 이런 공격은 종종 키를 버킷에 매핑하는 해시 함수의 **시드(seed)**를 알아내는 데 의존해요. 그 시드로 서비스 거부 공격에 쓸 키 집합을 무차별 대입으로 구성해요. Perl 5.8.1에서 이런 공격에 Perl을 강화하는 변경이 도입됐고, 이후 Perl 5.18.0에서 이 기능들이 강화되고 추가 보호가 더해졌어요.
이 글을 쓰는 시점에 Perl 5.18.0은 해시 구현에 대한 알고리즘 복잡도 공격에 잘 강화된 것으로 여겨져요. 이는 대체로 다음 조치 덕분이에요:
- 해시 시드 무작위화 (Hash Seed Randomization) — 공격용 키 집합을 만들 시드를 알 수 없게 하려고, 이 시드는 프로세스 시작 시 무작위로 초기화돼요.
PERL_HASH_SEED환경 변수로 덮어쓸 수 있어요 (perlrun의 "PERL_HASH_SEED" 참고). 이 환경 변수는 항목이 실제로 어떻게 저장되는지를 제어하지,keys·values·each로 어떻게 제시되는지는 제어하지 않아요. - 해시 순회 무작위화 (Hash Traversal Randomization) — 해시 함수에 어떤 시드가 쓰이든,
keys·values·each는 해시마다 무작위화된 순서로 항목을 반환해요. 삽입으로 해시를 수정하면 그 해시의 반복 순서가 바뀌어요. 이 동작은Hash::Util의hash_traversal_mask()로, 또는PERL_PERTURB_KEYS환경 변수(perlrun의 "PERL_PERTURB_KEYS" 참고)로 덮어쓸 수 있어요. 이 기능은 키의 "보이는" 순서를 제어하지, 실제 저장 순서는 제어하지 않아요. - 버킷 순서 교란 (Bucket Order Perturbance) — 항목이 주어진 해시 버킷에 충돌하면 체인에 저장되는 순서가 Perl 5.18에서 더 이상 예측 불가능해요. 이는 충돌을 관찰하기 더 어렵게 하려는 의도예요.
PERL_PERTURB_KEYS환경 변수로 덮어쓸 수 있어요. - 새 기본 해시 함수 (New Default Hash Function) — 기본 해시 함수는 해시 시드를 추론하기 힘들게 하려는 의도로 수정됐어요.
- 대체 해시 함수 (Alternative Hash Functions) — 소스 코드에 선택할 수 있는 여러 해시 알고리즘이 포함돼 있어요. 기본 perl 해시가 공격에 견고하다고 믿지만, 폴백 옵션으로 Siphash 해시 함수를 포함했어요. Perl 5.18.0 릴리스 시점에 Siphash는 암호학적 강도를 가진 것으로 여겨져요. 기본 해시보다 훨씬 느려서 기본값은 아니에요.
특수한 Perl을 컴파일하지 않고는 5.18.0 이전 버전들과 정확히 같은 동작을 얻을 방법은 없어요. 가장 가까운 것은
PERL_PERTURB_KEYS를 0으로 설정하고PERL_HASH_SEED를 알려진 값으로 설정하는 거예요. 위 보안 고려 때문에 그런 설정을 프로덕션에 쓰는 것은 권장하지 않아요.Perl은 해시 키의 순서를 한 번도 보장한 적이 없고, 그 순서는 Perl 5의 수명 동안 이미 여러 번 바뀌었어요. 또한 해시 키의 순서는 항상, 그리고 계속해서 삽입 순서와 해시의 수명 동안 가해진 변경 이력의 영향을 받아요.
또, 해시 요소의 순서가 무작위화될 수는 있지만, 이 "의사 순서"는 목록을 무작위로 섞는 애플리케이션(그건
List::Util::shuffle()을 쓰세요.List::Util은 Perl 5.8.0부터 표준 코어 모듈이에요. 또는 CPAN의Algorithm::Numerical::Shuffle), 순열 생성(CPAN의Algorithm::Permute나Algorithm::FastPermute같은 것), 또는 암호화 애플리케이션에 쓰면 안 돼요.tied 해시는 자기만의 순서와 알고리즘 복잡도 공격을 가질 수 있어요.
- 해시 시드 무작위화 (Hash Seed Randomization) — 공격용 키 집합을 만들 시드를 알 수 없게 하려고, 이 시드는 프로세스 시작 시 무작위로 초기화돼요.
-
정규식 — Perl의 정규식 엔진은 이른바 NFA(비결정적 유한 오토마타) 라서, 정규식이 여러 방식으로 매치될 수 있다면 시간과 공간을 상당히 쉽게 크게 소모할 수 있어요. 정규식을 신중히 짜면 도움이 되지만, 실제로 할 수 있는 게 별로 없는 경우가 많아요 ("Mastering Regular Expressions" 책은 필독이에요, perlfaq2 참고). 공간이 바닥나는 것은 Perl이 메모리 부족으로 드러나요.
-
정렬 — 5.8.0 이전 Perl에서
sort()함수를 구현하는 quicksort 알고리즘은 시간을 많이 소모하도록 속이기 매우 쉬웠어요. Perl 5.8.0부터는 다른 정렬 알고리즘인 mergesort가 기본으로 쓰여요. Mergesort는 어떤 입력에서도 잘못 동작하지 않아요.
자세한 내용은 https://www.usenix.org/legacy/events/sec03/tech/full_papers/crosby/crosby.pdf 와, 알고리즘 복잡도에 관한 컴퓨터 과학 교과서를 참고하세요.
Sudo 사용 (Using Sudo)
널리 쓰이는 sudo 도구는 사용자가 다른 사용자로 프로그램을 실행할 수 있게 하는 통제된 방법을 제공해요. 실행 환경을 어느 정도 정화하고 shebang 경합 조건도 피해요. 안전한 set-id 스크립트 버전이 없다면, 스크립트를 다른 사용자로 실행하려고 C 래퍼를 쓰는 것보다 sudo가 더 편리할 수 있어요.
하지만 sudo는 set-id 비트처럼 유효(effective) ID만 바꾸는 게 아니라 실제(real) 사용자·그룹 ID를 대상 정체성으로 바꿔요. 결과적으로 Perl은 sudo 아래에서 실행 중임을 감지할 수 없어서, taint 모드 켜기 같은 자체 보안 예방 조치를 자동으로 취하지 않아요. sudo 설정이 정확히 어떤 명령을 실행할 수 있는지 정한다면, 승인된 명령에 taint 모드를 켜는 -T 옵션이 포함될 수 있어요.
일반적으로 스크립트가 sudo 아래에서 실행되기에 적합한지 평가할 때는 그런 실행 환경을 염두에 두고 따져야 해요. 같은 스크립트가 전통적인 set-id 환경에서 실행되기에 적합한 것과 필요충분조건이 같지는 않지만, 많은 이슈는 겹쳐요.
함께 보기 (SEE ALSO)
- perlrun의 "ENVIRONMENT" — 환경 변수 정리에 대한 설명
perlsecpolicy— Perl 보안 정책