Rozdział 5. Wspólne hosty

> Dodaj do ulubionych

Ujawnienie danych sesji

Na wspólnym hoście zabezpieczenia nigdy nie są tak szczelne, jak na hoście dedykowanym. Jest to kompromis, który trzeba zaakceptować w zamian za niewygórowaną cenę.

Jednym z największych niebezpieczeństw, jakie wiążą się z używaniem wspólnych hostów jest wspólny magazyn sesji. Domyślnie PHP przechowuje dane sesji w lokalizacji /tmp i dotyczy to każdego. Ludzie natomiast mają tendencję do pozostawania przy domyślnych ustawieniach we wszystkim, także jeśli chodzi o sesje. Na szczęście nie każdy może odczytywać pliki sesji, ponieważ dostęp do nich ma tylko serwer:

$ ls /tmp
total 12
-rw-------  1  nobody  nobody  123 May 21 12:34 sess_dc8417803c0f12c5b2e39477dc371462
-rw-------  1  nobody  nobody  123 May 21 12:34 sess_46c83b9ae5e506b8ceb6c37dc9a3f66e
-rw-------  1  nobody  nobody  123 May 21 12:34 sess_9c57839c6c7a6ebd1cb45f7569d1ccfc
$

Niestety napisanie skryptu odczytującego te dane jest banalnie proste, a ponieważ skrypt taki działa z uprawnieniami użytkownika nobody (lub o jakiejś innej nazwie), ma wszystkie potrzebne mu do tego uprawnienia.

Tę i kilka innych podobnych luk można załatać za pomocą dyrektywy safe_mode, ale ponieważ dyrektywa ta ma zastosowanie tylko do PHP, nie stanowi to rozwiązania przyczyny problemu. Atakujący może po prostu użyć innego języka programowania.

Lepsze rozwiązanie

Jakie rozwiązanie jest lepsze? Nie używać tego samego magazynu sesji, co wszyscy inni. Najlepiej dane te przechowywać w bazie danych, dostęp do której jest możliwy tylko po podaniu informacji uwierzytelniających. W tym celu należy użyć funkcji session_set_save_handler(), która pozwala zastąpić domyślny mechanizm obsługi sesji własnymi funkcjami.

Na poniższym listingu znajduje się uproszczony przykładowy skrypt zapisujący dane sesji w bazie danych:

<?php

session_set_save_handler('_open',
                         '_close',
                         '_read',
                         '_write',
                         '_destroy',
                         '_clean');

function _open()
{
  global $_sess_db;

  $db_user = $_SERVER['DB_USER'];
  $db_pass = $_SERVER['DB_PASS'];
  $db_host = 'localhost';

  if ($_sess_db = mysql_connect($db_host, $db_user, $db_pass))
  {
    return mysql_select_db('sessions', $_sess_db);
  }

  return FALSE;
}

function _close()
{
  global $_sess_db;

  return mysql_close($_sess_db);
}

function _read($id)
{
  global $_sess_db;

  $id = mysql_real_escape_string($id);

  $sql = "SELECT data
          FROM   sessions
          WHERE  id = '$id'";

  if ($result = mysql_query($sql, $_sess_db))
  {
    if (mysql_num_rows($result))
    {
      $record = mysql_fetch_assoc($result);

      return $record['data'];
    }
  }

  return '';
}

function _write($id, $data)
{
  global $_sess_db;

  $access = time();

  $id = mysql_real_escape_string($id);
  $access = mysql_real_escape_string($access);
  $data = mysql_real_escape_string($data);

  $sql = "REPLACE
          INTO    sessions
          VALUES  ('$id', '$access', '$data')";

  return mysql_query($sql, $_sess_db);
}

function _destroy($id)
{
  global $_sess_db;

  $id = mysql_real_escape_string($id);

  $sql = "DELETE
          FROM   sessions
          WHERE id = '$id'";

  return mysql_query($sql, $_sess_db);
}

function _clean($max)
{
  global $_sess_db;

  $old = time() - $max;
  $old = mysql_real_escape_string($old);

  $sql = "DELETE
          FROM   sessions
          WHERE  access < '$old'";

  return mysql_query($sql, $_sess_db);
}

?>

Skrypt ten do działania potrzebuje bazy danych o nazwie sessions o następującym schemacie:


mysql> DESCRIBE sessions;
+--------+------------------+------+-----+---------+-------+
| Field  | Type             | Null | Key | Default | Extra |
+--------+------------------+------+-----+---------+-------+
| id     | varchar(32)      |      | PRI |         |       |
| access | int(10) unsigned | YES  |     | NULL    |       |
| data   | text             | YES  |     | NULL    |       |
+--------+------------------+------+-----+---------+-------+

W MySQL bazę tę można utworzyć przy użyciu następującego polecenia:

CREATE TABLE sessions
(
    id varchar(32) NOT NULL,
    access int(10) unsigned,
    data text,
    PRIMARY KEY (id)
);

Zapisując dane sesji w bazie danych sprawiasz, że o ich bezpieczeństwie decydują zabezpieczenia tej bazy. Przypomnij sobie informacje o bazach danych z części o bazach danych i języku SQL, ponieważ mają one tu zastosowanie.

Przeglądanie systemu plików

Dla zabawy poniżej przedstawiony jest skrypt przeglądający system plików:

<?php

echo "<pre>n";

if (ini_get('safe_mode'))
{
    echo "[Tryb safe_mode włączony]nn";
}
else
{
    echo "[Tryb safe_mode wyłączony]nn";
}

if (isset($_GET['dir']))
{
    ls($_GET['dir']);
}
elseif (isset($_GET['file']))
{
    cat($_GET['file']);
}
else
{
    ls('/');
}

echo "</pre>n";

function ls($dir)
{
    $handle = dir($dir);

    while ($filename = $handle->read())
    {
        $size = filesize("$dir$filename");

        if (is_dir("$dir$filename"))
        {
            if (is_readable("$dir$filename"))
            {
                $line = str_pad($size, 15);
                $line .= "<a href="{$_SERVER['PHP_SE LF']}?dir=$dir$filename/">$filename/</a>";
            }
            else
            {
                $line = str_pad($size, 15);
                $line .= "$filename/";
            }
        }
        else
        {
            if (is_readable("$dir$filename"))
            {
                $line = str_pad($size, 15);
                $line .= "<a href="{$_SERVER['PHP_SELF']}?file=$dir$filename">$filename</a>";
            }
            else
            {
                $line = str_pad($size, 15);
                $line .= $filename;
            }
        }

        echo "$linen";
    }

    $handle->close();
}

function cat($file)
{
    ob_start();
    readfile($file);
    $contents = ob_get_contents();
    ob_clean();
    echo htmlentities($contents);

    return true;
}

?>

Dyrektywa safe_mode może uniemożliwić działanie tego skryptu, ale co jeśli podobny skrypt zostanie napisany w innym języku?

Dobrym rozwiązaniem jest zapisywanie poufnych informacji w bazie danych i chronienie danych uwierzytelniających do niej za pomocą technik, które zostały opisane wcześniej (gdzie dane te są zapisywane w zmiennych $_SERVER['DB_USER'] i $_SERVER['DB_PASS']).

Najlepszym rozwiązaniem jest korzystanie z dedykowanego hosta.

Autor: PHP Security Consortium

Źródło: http://phpsec.org/projects/guide/5.html

Tłumaczenie: Łukasz Piwko

Treść tej strony jest dostępna na zasadach licencji CC BY-NC-SA 2.0

Dodaj komentarz

17 + 3 =