Безопасное хранение учётных данных в PHP

В веб-приложениях, как и в любом другом виде приложений, одной из самых сложных вещей является защита «секретных» данных. Этими данными могут быть ключи авторизации API, пароли БД и др. В идеале они не должны определяться в самом приложении, а загружаться из другого источника.

Хоть и множество проблем, связанных с защитой секретных данных, может быть устранено с помощью более менее «секретной обработки”, но, тем не менее, кажется, что всё ещё имеет место быть потребность в сохранении секретных данных прямо в коде. Использование такого рода паттерна, очевидно, не рекомендуется. В базе данных Common Weakness Enumeration есть даже запись об этом: CWE-798. Жёстко закодированные учётные данные могут представлять огромный риск, если злоумышленник каким-то образом смог получить доступ к коду и прочесть их.

Так что насчёт PHP?

В PHP приложениях существует общий паттерн для хранения конфигураций и доступов в .env файлах, располагаемый в директории, доступной для чтения из PHP. Поскольку это обычная практика, то я не хочу отходить слишком далеко от неё. Я предложу нечто такое, что сможет легко заменить это, но по-прежнему будет как можно проще.

Мы пройдёмся по нескольким хорошим методам хранения учётных данных, обсудим, что в них хорошего и плохого. Все они будут использовать простые методы хранения, основанные на коде или в соответствующем простом файле (например, в .env). Мы начнём с худшего варианта — сохранение учётных данных в текстовом виде в коде.

Размещение учётных данных в коде

Разработчик может подумать, что это очень просто, поскольку если нужны данные для, скажем, соединения HTTP клиента с API, то самым простым и лучшим решением будет разместить их как можно ближе к этому коду. Однако есть два нюанса:

  1. Эти жёстко запрограммированные учётные данные будут существовать в вашей системе контроля версий. Если она находится в публичном репозитории GitHub, вы просто напросто раскроете пароли любому, кто сможет склонировать этот репозиторий;
  2. Если злоумышленник когда-либо сможет получить доступ к исходникам приложения, то у него появится прямой доступ к учётке, даже без необходимости что-либо дешифровывать или выполнять реверсивную работу.

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

За последние несколько лет было несколько случаев сливания паролей, где-то в веб-приложениях, а где-то у других приложений или аппаратных средствах, у которых учётные данные были заданы по умолчанию или жёстко запрограммированы. Поэтому ИЗБЕГАЙТЕ такого варианта. Существует очень, очень редкие случаи, когда такой вариант размещения учётных данных допустим. Но даже тогда присутствуют, как правило, другие методы защиты, созданные специально для этого.

.evn файл, находящийся в корне сайта

Итак, теперь, когда мы выяснили, что нам нужно избегать жёстко запрограммированных простых текстовых учётных данных в коде, нам нужно изучить другой способ их хранения. И именно .env файл сейчас является наиболее популярным. С тех пор как настал более совершенный век PHP-разработки (благодаря таким инструментам как Composer), множество фреймворков и библиотек приняли паттерн использования .env файла для хранения настроек, специфичных для приложения. Само собой это означало, что, в конечном счете, конфиденциальные данные начали храниться там.

Наличие всего этого в отдельном файле конечно лучше, чем жёсткое кодирование, но, всё же, есть некоторые проблемы. Если вы перечитаете заголовок этого раздела, вы сможете найти одну из таких. Вы же помните, что всё, что находится в корне вашего рабочего каталога, напрямую читается внешним миром? В этом случае, при размещении .env файла в корне, кто-то сможет получить к нему доступ через браузер по http://mycoolsite/.env

Таким образом, мы можем исключить этот вариант из списка методов хранения. Однако есть ещё один подобный вариант, который может воспрепятствовать прямому доступу: перемещение .env из корня сайта.

.env файл, находящийся за пределами корня сайта

В этом случае мы просто перемещаем файл на один уровень ниже, что делает его недоступным через интернеты. Т.е. если файл был в /var/www/mycoolsite/public/.env, то теперь его нужно переместить в /var/www/mycoolsite/.env.

Для примера, мы могли бы использовать популярный пакет vlucas/phpdotenv для импортирования файла в текущую среду:

<?php 

require_once __DIR__.'/../vendor/autoload.php'; 

$dotenv = new Dotenv\Dotenv(__DIR__.'/../'); 
$dotenv->load();
?>

В этом скрипте загружается .env файл из каталога на один уровень выше, а значения присваиваются в суперглобальный массив $_ENV. Это лучше, чем просто доступ к файлу, но есть и свои недостатки:

  1. Если злоумышленник сможет загрузить PHP файл и выполнить код, то он может просто распечатать значения $_ENV массива и получить полный доступ;
  2. Значения всё ещё существуют на диске в текстовом формате, поэтому если обнаружить локальный файл, то можно его прочитать.

Таким образом, это шаг в правильном направлении, но у нас ещё есть и более надёжные способы защиты учётных данных.

Зашифрованные учётные данные

Следующий шаг заключается в шифровании секретных данных. С помощью библиотеки defuse/php-encryption мы упростим себе эту задачу.

Для начала нам нужно установить библиотеку и сгенерировать ключ:

composer require defuse/php-encryption

vendor/bin/generate-defuse-key

Это сгенерирует надёжный ключ, который лучше хранить не где-то в каталогах сайта, а, например, в /usr/local. Только обязательно необходимо будет изменить chmod и владельца/группу, чтобы PHP мог его прочитать.

После того, как вы перенесёте ключ в нужный каталог и дадите ему нужные разрешения, мы сможем читать и расшифровывать .env файл. Воспользуемся всё той же библиотекой vlucas/phpdotenv для чтения, а php-encryption для расшифровки.

Пример .env файла:

test=def502003cbef858698bc40b2b8d0ffb6f365f2cef00009047650910941da72372313c7ce3f9d4ce8ba2cd64f6a5a5a330da47151c5c90124fd4e8ea792d40810d8906b8a888b12db78f1cbb0819825447ce685b1c608dfb1f30

test1=def502005c647492189c68d7f5fec781a0e10bdee8865b23f729b080c7bbadd2204005367ea6464d75609ea48be235886cd2f398bf60eaa0a0bb32e2906ab9b9b1f66c58fdd24f054b5311460fdf8770c5d729b3c296cb5d

И PHP-скрипт для расшифровки его:

<?php 

require_once __DIR__.'/../vendor/autoload.php'; 

$dotenv = new Dotenv\Dotenv(__DIR__.'/../'); 
$dotenv->load();

$keyContents = file_get_contents('/usr/local/keyfile`);
$key = \Defuse\Crypto\Key::loadFromAsciiSafeString($keyContents);

$secret = \Defuse\Crypto\Crypto::decrypt($ciphertext, $key);
?>

Дешифрованное секретное значение попадёт в $secret переменную. Очевидно, вы не захотите каждый раз копировать и вставлять этот код, поэтому лучше обернуть код во вспомогательную функцию или класс, чтобы сделать его автономным.

Хоть это ещё один шаг в правильном направлении защиты, тем не менее, это всё ещё локальный файл, который злоумышленник может прочитать.

У нас заканчиваются варианты, но позвольте предложить ещё один. Этот метод по-прежнему позволяет хранить значения в простом файле, но защищает их от локального внедрения в PHP, поскольку PHP не сможет получить доступ к нему. Давайте начнём.

Метод «Apache Pull»

В этом методе мы будем использовать некоторые их тех же методов, что и раньше (сохраняя секретные данные в зашифрованном простом файле), но будут использоваться переменные окружения Apache для передачи этих значений в PHP.

Примечание: в данном руководстве показано как настроить веб-сервер Apache, но этот же подход, вероятно, может быть выполнен и в nginx.

Если вам нужна быстрая и лёгкая версия, я уже настроил этот Docker репозиторий с конфигурированной средой.

Итак. Что нужно для реализации этого метода:

  1. Файл создаётся с зашифрованными учётными данными где-то в файловой системе (в данном случае мы просто помещаем его в /tmp);
  2. Затем этот файл используется в /etc/apache2/envvars в качестве дополнительного источника для установки переменных локальной среды;
  3. Когда Apache запускается, он извлекает все значения из envvars и переопределяет их внутри, включая и наши особые значения;
  4. И уже эти значения получает PHP через SetEnv инструкцию Apache.

Чтобы нельзя было прочитать зашифрованный файл через PHP, поместим его в таком месте, в который PHP не имеет доступа. Ограничить доступ в каталоги можно с помощью open_basedir конфигурации. Об этом будет ниже.

Прежде, чем мы продолжим, я хочу предупредить, что это решение — не идеально. Если злоумышленник сможет выполнить PHP-код и прочитать из $_ENV суперглобального массива, ключевые значения всё равно будут получены.

Секретный ключ шифрования

Сначала мы создадим файл с секретным ключом шифрования. В нашем /tmp/addl-settings файле мы определили ключ:

export ENC_KEY=1234567890 // This is just a sample key, obviously

Теперь мы добавим ссылку на этот файл в конец файла envvars:

. /tmp/addl-settings

Когда с этим будет закончено, Apache загрузит ENC_KEY значение в свою внутреннюю среду и сделает его доступным.

Конфигурация Apache

Пример виртуального хоста:

<VirtualHost *:80>
    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

    SetEnv ENC_KEY ${ENC_KEY}
</VirtualHost>

Здесь вы можете увидеть такую конструкцию как ${}, это используется для вставки переменных окружения Apache.

open_basedir

Последним шагом является защита с помощью open_basedir. Мы создадим файл open_basedir.ini и скопируем его в нужное место Apache, чтобы включить его в файл конфигурации php.ini:

open_basedir=/var/www/html

Теперь секретный ключ шифрования ENC_KEY доступен для PHP через Apache, но не может быть прочитан в виде файла напрямую.

Подождите, это ещё не всё!

Это отличная настройка, но вы можете спросить «Как мне прочесть зашифрованную конфигурацию теперь?» Ну, с помощью удобной библиотеки psecio/secure_dotenv . У нас уже есть ключ, который нам нужен для шифрования и дешифрования, поэтому мы просто используем его для этих примеров. Сначала установить пакет с помощью Composer:

composer require psecio/secure_dotenv

Затем в вашем приложении создайте экземпляр Parser, в качестве параметров которого передайте ключ шифрования и путь к env файлу:

<?php 

$envFile = __DIR__.'/.env'; 
$parser = new \Psecio\SecureDotenv\Parser($_ENV['ENC_KEY'], $envFile); 

?>

Повторное использование env файла из приведённых выше примеров даёт нам значения test и test1 переменных, которые могут быть извлечены из getContent:

<?php 
echo 'test1 is: '.$parser->getContent()['test1'];
?>

Библиотека дешифрует значения в фоне (с использованием той же  defuse/php-encryption библиотеки), и вы получаете чистый результат.

Резюме

Защита учётных данных в PHP-приложениях — интересная проблема. В исследовании, проведённом для этой статьи, я обнаружил, что как и любая другая задача, связанная с безопасностью, всегда есть более одного способа выполнить её. Но PHP делает решение ещё более сложным, поскольку взаимодействует с веб-серверами. PHP-скрипты и обработка должны иметь, по крайней мере, доступ на чтение каждого файла, с которым им нужно работать. Это делает ОЧЕНЬ трудным возможность предотвратить проблемы с локальным файлом, если вы не очень осторожны.

Нет никаких 100% защит для хранения учётных данных. Но вышеописанный метод Apache pull, является одним из тех, что проще в реализации, чем сама технология, используемая для этого. Конечно, если у вас более сложная среда, развёрнутая с использованием Chef, Vagrant или других инструментов, у них есть некоторые дополнительные функции (например, зашифрованные базы данных Chef), которые также могут быть использованы для обработки учётных данных.

Не забывайте, что для этой задачи нет решения, которое подошло бы всем. Всё зависит от вашей среды и требованиям к рискам для вашего приложения. Обязательно сядьте и создайте точную модель угрозы своего приложения, прежде чем принимать решение о том, как вы собираетесь защищать свои учётные данные. Это даст вам лучшее представление о ваших потребностях и о том, какой тип (или виды) защиты вам понадобятся.

Примечание

Это авторский перевод статьи Keeping Credentials Secure in PHP.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *