Статьи

Напишите безопасные сценарии с PHP 4.2!

В течение длительного времени одной из самых больших выгодных сторон PHP как языка сценариев на стороне сервера было то, что значения, представленные в форме, автоматически создавались для вас как глобальные переменные. Начиная с PHP 4.1 создатели PHP рекомендовали альтернативные способы доступа к представленным данным. В PHP 4.2 они отключили старый способ работы! Как я объясню в этой статье, эти изменения были сделаны во имя безопасности. Вместе мы рассмотрим новые возможности PHP для обработки отправки форм и других данных, а также то, как их можно использовать для написания более безопасных сценариев.

Что не так с этой картиной?

Рассмотрим следующий скрипт PHP, который предоставляет доступ к веб-странице, только если введены правильные имя пользователя и пароль:

<?php  // Check the username and password  if ($username == 'kevin' and $password == 'secret')  $authorized = true;  ?>  <?php if (!$authorized): ?>  <!-- Unauthorized users are prompted for their credentials -->  <p>Please enter your username and password:</p>  <form action="<?=$PHP_SELF?>" method="POST">  <p>Username: <input type="text" name="username" /><br />  Password: <input type="password" name="password" /><br />  <input type="submit" /></p>  </form>  <?php else: ?>  <!-- Super-Secret HTML content goes here -->  <?php endif; ?> 

Хорошо, я уверен, что около половины читателей в аудитории просто закатили глаза и сказали: «Это так глупо — я бы никогда не допустил такой ошибки!» Но я гарантирую, что многие из вас думают: «Эй, это не плохо. Я должен записать это!» И, конечно, всегда есть довольно запутанное меньшинство («Что такое PHP?»). PHP был разработан как «приятный и простой» язык сценариев, который начинающие могут начать использовать за считанные минуты; это также должно защитить тех новичков от страшных ошибок, подобных приведенной выше.

Для справки, проблема с вышеописанным скриптом в том, что вы можете легко получить к нему доступ без указания правильного имени пользователя и пароля. Просто введите адрес страницы в ваш браузер с « ?authorized=1 в конце. Так как PHP автоматически создает переменную для каждого отправляемого значения — из сообщения формы, строки запроса URL-адреса или файла cookie — это устанавливает $authorized в 1 в сценарии и отбрасывает неавторизованного пользователя прямо перед секретным рецептом полковника (извинения читателям, не употребляющим в пищу вредную пищу, которые не получат эту шутку

Так что, легко исправить, верно? Просто установите $authorized в false по умолчанию в верхней части скрипта. Проблема здесь в том, что исправление вообще не было необходимо! $authorized переменная, созданная и полностью используемая в скрипте; почему разработчик должен беспокоиться о защите каждой из его или ее переменных от переопределения значениями, представленными злоумышленниками?

Как PHP 4.2 меняет вещи?

Начиная с PHP 4.2, в новой установке PHP опция register_globals отключена по умолчанию, поэтому значения EGPCS (EGPCS — это сокращение для Environment, Get, Post, Cookies, Server — полный диапазон источников внешних переменных в PHP) не создаются как глобальные переменные. Да, эту опцию все еще можно включить вручную, но команда PHP предпочла бы ее, если вы этого не сделаете. Чтобы соответствовать их пожеланиям, вам нужно использовать альтернативный метод, чтобы получить эти значения.

Начиная с PHP 4.1, значения EGPCS теперь доступны в виде набора специальных массивов:

  • $_ENV — содержит системные переменные среды
  • $_GET — содержит переменные в строке запроса, в том числе из форм GET
  • $_POST — содержит переменные, отправленные из форм POST
  • $_COOKIE — содержит все переменные cookie
  • $_SERVER — содержит переменные сервера, такие как HTTP_USER_AGENT
  • $_REQUEST — Содержит все в $_GET , $_POST и $_COOKIE
  • $_SESSION — содержит все зарегистрированные переменные сеанса

До PHP 4.1 разработчики, которые работали с register_globals отключали (это также считалось хорошим способом немного повысить производительность PHP), чтобы получить доступ к этим значениям, используя громоздкие массивы, такие как $HTTP_GET_VARS . Эти новые имена переменных не только короче, но и имеют некоторые новые приятные функции.

Во-первых, давайте перепишем сломанный скрипт из предыдущего раздела для использования в PHP 4.2 (т.е. с отключенным register_globals ):

 <?php  $username = $_REQUEST['username'];  $password = $_REQUEST['password'];  $PHP_SELF = $_SERVER['PHP_SELF'];   // Check the username and password  if ($username == 'kevin' and $password == 'secret')    $authorized = true;  ?>  <?php if (!$authorized): ?>  <!-- Unauthorized users are prompted for their credentials -->  <p>Please enter your username and password:</p>  <form action="<?=$PHP_SELF?>" method="POST">    <p>Username: <input type="text" name="username" /><br />       Password: <input type="password" name="password" /><br />       <input type="submit" /></p>  </form>  <?php else: ?>  <!-- Super-Secret HTML content goes here -->  <?php endif; ?> 

Как видите, все, что мне нужно было сделать, это добавить три строки в начало скрипта:

   $username = $_REQUEST['username'];  $password = $_REQUEST['password'];  $PHP_SELF = $_SERVER'['PHP_SELF']; 

Поскольку мы ожидаем, что имя пользователя и пароль будут отправлены пользователем, мы извлекаем эти значения из массива $_REQUEST . Использование этого массива позволяет пользователям передавать значения любым доступным им способом: через строку запроса URL (например, чтобы позволить пользователям создать закладку, которая автоматически вводит для них свои учетные данные), путем отправки формы или в виде файла cookie. Если вы предпочитаете ограничивать методы, с помощью которых они могут отправлять свои учетные данные, только для отправки формы (или, точнее, HTTP POST-запросов, которые можно смоделировать без отправки формы, если push доходит до пуша), вы можете использовать массив $_POST вместо:

   $username = $_POST['username'];  $password = $_POST['password'];  $PHP_SELF = $_SERVER['PHP_SELF']; 

Мы также PHP_SELF обычно используемую переменную PHP_SELF из массива серверных переменных; как и переменные формы, он не создается автоматически с отключенным register_globals . Поскольку он используется только один раз в сценарии, вы, вероятно, предпочтете просто ссылаться на него прямо в коде формы:

   <form action="<?=$_SERVER['PHP_SELF']?>" method="POST"> 

За исключением «разрешения» этих трех переменных, скрипт не изменился вообще. Отключение register_globals просто заставляет разработчика быть осведомленным о данных, поступающих из внешних (ненадежных) источников.

Примечание для Nitpickers: значение по умолчанию для error_reporting в PHP по-прежнему равно E_ALL & ~E_NOTICE , поэтому, если значения ‘username’ и ‘password’ не были отправлены, попытка извлечь их из массива $_REQUEST или $_POST не будет выдать сообщение об ошибке. Если вы используете более строгий уровень проверки ошибок в настройках PHP, вам нужно добавить немного больше кода, чтобы проверить, установлены ли эти переменные первыми.

Но разве это не значит больше печатать?

Да, в простых сценариях, подобных приведенному выше, новый способ работы в PHP 4.2 часто требует большего набора текста. Но, эй, посмотрите на светлую сторону — вы можете начать зарядку нажатием клавиши!

Если серьезно, создатели PHP не совсем нечувствительны к вашей боли (у меня есть сводный брат, который страдает от повторяющихся стрессовых травм). Особенностью этих новых массивов является то, что, в отличие от всех других переменных PHP, они являются полностью глобальными. Как это поможет вам? Давайте немного расширим наш пример.

Чтобы на нескольких страницах сайта требовалось просматривать комбинацию имени пользователя и пароля, мы переместим наш код авторизации во включаемый файл ( protectme.php ) следующим образом:

 <?php /* protectme.php */   function authorize_user($authuser, $authpass)  {    $username = $_POST['username'];    $password = $_POST['password'];     // Check the username and password    if ($username != $authuser or $password != $authpass):      ?>      <!-- Unauthorized users are prompted for their credentials -->      <p>Please enter your username and password:</p>      <form action="<?=$_SERVER['PHP_SELF']?>" method="POST">        <p>Username: <input type="text" name="username" /><br />           Password: <input type="password" name="password" /><br />           <input type="submit" /></p>      </form>      <?php      exit();    endif;  }  ?> 

Теперь наша защищенная страница выглядит так:

 <?php  require('protectme.php');  authorize_user('kevin','secret');  ?>  <!-- Super-Secret HTML content goes here --> 

Красиво и просто, правда? Теперь вот задача для особо остроумных и опытных — чего не хватает в функции authorize_user ?

Чего не хватает, так это объявления $_POST внутри функции, чтобы вывести его из глобальной области видимости! В PHP 4.0 с register_globals вам пришлось бы добавить строку кода, чтобы получить доступ к переменным $username и $password внутри функции:

   function authorize_user($authuser, $authpass)  {    global $username, $password;     ... 

В PHP, в отличие от других языков с похожим синтаксисом, переменные вне функции автоматически не доступны внутри функции. Вы должны специально привести их в соответствие с global линией, показанной выше.

С отключенной функцией register_globals в PHP 4.0 для повышения безопасности вы должны использовать массив $HTTP_POST_VARS для получения значений, отправленных из вашей формы, но вам все равно придется импортировать этот массив из глобальной области видимости:

   function authorize_user($authuser, $authpass)  {    global $HTTP_POST_VARS;     $username = $HTTP_POST_VARS['username'];    $password = $HTTP_POST_VARS['password']; 

Но в PHP 4.1 или более поздней версии специальная переменная $_POST (и остальные специальные переменные, перечисленные в предыдущем разделе) всегда доступны во всех областях. Вот почему переменную $_POST не нужно объявлять global в верхней части функции:

   function authorize_user($authuser, $authpass)  {    $username = $_POST['username'];    $password = $_POST['password']; 

Как это влияет на сеансы?

Введение специального массива $_SESSION фактически помогает упростить сессионный код. Вместо того, чтобы регистрировать глобальные переменные в качестве переменных сеанса, а затем отслеживать, какие переменные были зарегистрированы, просто обращайтесь ко всем переменным сеанса как $_SESSION['varname'] .

Давайте рассмотрим другой пример авторизации. На этот раз он будет использовать сеансы, чтобы пометить пользователя как авторизованного на оставшуюся часть его пребывания на вашем сайте. Во-первых, версия PHP 4.0 (с включенным register_globals ):

 <?php  session_start();   if ($username == 'kevin' and $password == 'secret')  {    $authorized = true;    session_register('authorized');  }  ?>  <?php if (!$authorized): ?>  <!-- Display HTML Form prompting user to log in -->  <?php else: ?>  <!-- Super-secret HTML content goes here -->  <?php endif; ?> 

Теперь найдите дыру в безопасности. Как и раньше, добавление ?authorized=1 в конец URL-адреса обходит меры безопасности и предоставляет доступ к содержимому страницы. Разработчик, вероятно, думал о $authorized как сессионной переменной и упустил тот факт, что эту же переменную можно легко установить с помощью пользовательского ввода.

Вот как выглядит скрипт, когда мы добавляем наши специальные массивы (PHP 4.1) и отключаем register_globals (PHP 4.2):

 <?php  session_start();   if ($_POST['username'] == 'kevin' and      $_POST['password'] == 'secret')    $_SESSION['authorized'] = true;  ?>  <?php if (!$_SESSION['authorized']): ?>  <!-- Display HTML Form prompting user to log in -->  <?php else: ?>  <!-- Super-secret HTML content goes here -->  <?php endif; ?> 

Видеть? Гораздо проще! Вместо регистрации нормальной переменной в качестве переменной сеанса мы устанавливаем переменную сеанса (в $_SESSION ) напрямую, а затем используем ее таким же образом. Нет больше путаницы в отношении того, какие переменные являются переменными сеанса, и вы заметите, что код тоже немного короче!

Резюме

В этой статье я объяснил причины недавних изменений языка сценариев PHP. В PHP 4.1 к языку был добавлен набор специальных массивов для доступа к внешним значениям данных. Эти массивы доступны в любой области, чтобы сделать доступ к внешним данным более удобным. В PHP 4.2, register_globals был отключен по умолчанию, чтобы стимулировать миграцию на новые массивы и уменьшить склонность неопытных разработчиков писать небезопасные сценарии PHP.