Папка public в GLPI: безопасная настройка для продуктива

Почему DocumentRoot в GLPI 10 должен указывать на папку public: что оказывается открытым, если это не так, усиленный виртуальный хост в Apache и Nginx, скрипт проверки и чек-лист для продуктива от службы сопровождения NexTool.

В этой статьеРазделов: 6

Если DocumentRoot вашего GLPI указывает на корень установки, а не на папку public, файл с паролем от вашей базы данных находится на расстоянии одного GET-запроса. Начиная с GLPI 10 это самое важное решение по безопасности при развёртывании — и именно его мы чаще всего находим неверным в средах, которые приходят к нам на сопровождение, потому что система отлично работает даже при неправильной настройке.

Почему неверный webroot остаётся незамеченным

GLPI 10 продолжает открываться и нормально работать, если направить веб-сервер на корень (/var/www/glpi) вместо /var/www/glpi/public. На экране ничего не ломается, никто не создаёт заявку, и поэтому уязвимость живёт годами. При сопровождении парков GLPI у клиентов это самая частая тихая находка при онбординге: среда отвечает нормально, но https://glpi.cliente.com/files/_log/php-errors.log отдаёт лог приложения как обычную загрузку.

Деталь, которая проявляется только в эксплуатации: в Apache старую установку частично спасали файлы .htaccess, разбросанные внутри config/, files/ и подобных каталогов и запрещавшие доступ. Это создаёт ложное ощущение защиты. В Nginx этих .htaccess просто не существует — Nginx их никогда не читает. Поэтому переход с Apache на Nginx с неверным root превращает частичное раскрытие в полное: files/, config/ и весь исходный код становятся статическим содержимым, доступным кому угодно. Смена веб-сервера — классический момент, когда эта бомба взрывается.

Что оказывается открытым при неверном DocumentRoot

Папка public существует для того, чтобы публиковать только маршрутизатор (index.php), .htaccess и статические файлы (CSS, JS, изображения). Всё остальное должно быть вне досягаемости браузера. Таблица ниже — матрица проверки, которую мы используем: при public в качестве webroot все перечисленные URL должны возвращать 404.

ПутьСодержимоеПоследствия раскрытияОжидаемый статус
/config/config_db.phpХост, пользователь и пароль базыПолная утечка базы данных404
/files/_log/php-errors.log, sql-errors.logРазведка приложения и утечка персональных данных404
/files/_dumps/SQL-дампы, созданные GLPIКопия базы, доступная для скачивания404
/files/_sessions/Файлы активных сессийПерехват сессии и кража CSRF-токена404
/src/Исходный код приложения на PHPКарта приложения для эксплуатации уязвимостей404
/vendor/Зависимости ComposerЭксплуатация CVE в библиотеках404
/install/install.phpМастер установкиПеренастройка и захват среды404 (удалить после установки)

Осторожно с ложной «безопасностью»: при работающем PHP /config/config_db.php может отвечать 200 с пустым телом (файл лишь определяет класс и ничего не выводит). Пустой ответ 200 не означает, что всё в порядке, — он означает, что файл находится внутри webroot. Как загрузка реально утекают не-PHP файлы: files/_log/*.log, files/_dumps/*.sql и любая забытая в дереве резервная копия. Поэтому критерий — 404, а не «пустая страница».

Apache: усиленный виртуальный хост

DocumentRoot указывает на public, листинг каталогов отключён, а чистый HTTP перенаправляется на HTTPS. FallbackResource заменяет маршрутизацию через .htaccess, не требуя AllowOverride All:

# /etc/apache2/sites-available/glpi.conf
<VirtualHost *:80>
    ServerName glpi.suaempresa.com
    # Nada de GLPI em HTTP puro
    Redirect permanent / https://glpi.suaempresa.com/
</VirtualHost>

<VirtualHost *:443>
    ServerName glpi.suaempresa.com
    DocumentRoot /var/www/glpi/public

    <Directory /var/www/glpi/public>
        Require all granted
        Options -Indexes +FollowSymLinks
        # Roteia sem depender de AllowOverride All
        FallbackResource /index.php
    </Directory>

    # Nega qualquer arquivo oculto (.git, .env, .htaccess)
    <FilesMatch "^\.">
        Require all denied
    </FilesMatch>

    # Cabeçalhos de segurança
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"

    SSLEngine On
    SSLCertificateFile /etc/ssl/certs/glpi.crt
    SSLCertificateKeyFile /etc/ssl/private/glpi.key
</VirtualHost>

Nginx: без страховки в виде .htaccess

В Nginx нет .htaccess, который бы вас спас, поэтому root должен быть public с самого начала. Обратите внимание на две строки, которых почти всегда не хватает: try_files $uri =404; внутри блока PHP и явная блокировка скрытых файлов.

# /etc/nginx/sites-available/glpi.conf
server {
    listen 80;
    server_name glpi.suaempresa.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name glpi.suaempresa.com;
    root /var/www/glpi/public;
    index index.php;

    ssl_certificate     /etc/ssl/certs/glpi.crt;
    ssl_certificate_key /etc/ssl/private/glpi.key;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ \.php$ {
        # =404 impede execução via PATH_INFO forjado
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Nginx não lê .htaccess: negue arquivos ocultos na mão
    location ~ /\. {
        deny all;
    }
}

Типичная ошибка здесь — скопировать из интернета универсальный блок PHP без try_files $uri =404;. Без него запрос вроде /uploads/foto.jpg/x.php может быть передан в FPM через PATH_INFO и выполнен, открывая путь к RCE, если злоумышленнику удастся записать файл в дерево. С =404 Nginx отклоняет запрос ещё до PHP.

Проверка после развёртывания: тест на 30 секунд

После перезагрузки веб-сервера запустите этот скрипт против реального домена. Каждая строка должна показывать OK. Любой FALHA (сбой) — это отдаваемый путь, которого не должно существовать:

#!/usr/bin/env bash
BASE="https://glpi.suaempresa.com"

# Todas as URLs abaixo DEVEM retornar 404 num webroot correto
for path in \
  /config/config_db.php \
  /src/ \
  /vendor/ \
  /files/_log/php-errors.log \
  /install/install.php ; do
  code=$(curl -sk -o /dev/null -w '%{http_code}' "${BASE}${path}")
  if [ "$code" = "404" ]; then
    echo "OK    ${path} -> ${code}"
  else
    echo "FALHA ${path} -> ${code} (EXPOSTO)"
  fi
done

При сопровождении мы закрепляем этот скрипт в чек-листе онбординга каждого нового клиента и запускаем его снова при каждой смене веб-сервера, обновлении или изменении обратного прокси — потому что правильный webroot не является постоянным состоянием: невнимательная миграция откатывает его за секунды.

Чек-лист для продуктива

  1. DocumentRoot / root указывает на .../public, никогда на корень установки.
  2. Удалите install/install.php, как только установка/обновление завершится (сам GLPI показывает красное предупреждение, пока файл существует).
  3. Принудительный HTTPS с редиректом 301 с порта 80 и включённым Strict-Transport-Security.
  4. Дополнительное усиление: вынесите config/ и files/ за пределы веб-дерева через GLPI_CONFIG_DIR и GLPI_VAR_DIR, оставив в webroot только public.
  5. Запустите скрипт проверки и добейтесь 404 на всех путях.
  6. Проверьте панель безопасности самого GLPI (Настройки) после запуска — она сигнализирует о незакрытых вопросах, таких как присутствующий install.php.

Исправить webroot — дело десяти минут; обнаружить, что пароль от базы три года был доступен для скачивания, обходится гораздо дороже. Если вы запускаете новый GLPI, переходите с Apache на Nginx или получаете в наследство среду без истории настроек, команда сопровождения GLPI от NexTool проверит webroot, усилит виртуальный хост и проведёт набор проверок в рамках онбординга. Обсудите с нами сопровождение GLPI.


Проверено командой NexTool Solutions.

Часто задаваемые вопросы

Потому что маршрутизация GLPI 10 не зависит от webroot при загрузке экранов: пути разрешаются внутри приложения. Приложение открывается и работает нормально с DocumentRoot в корне установки, поэтому визуально ничего не ломается. Проблема тихая: config/, files/ и исходный код становятся доступными из браузера. Поскольку симптомов нет, уязвимость живёт годами, пока кто-нибудь не проверит нужные URL.

Нет. .htaccess — возможность исключительно Apache. Nginx полностью игнорирует эти файлы, включая те, которые GLPI размещает внутри config/ и files/ для запрета доступа. Поэтому переход с Apache на Nginx с унаследованным неверным root опасен: частичная защита, которую давал Apache, исчезает, и раскрытие становится полным. В Nginx указание root на public обязательно, а не опционально.

Да. Пока install.php существует, сам GLPI показывает красное предупреждение о безопасности, а файл позволяет перенастроить среду. Удалите (rm public/install/install.php) или переименуйте его сразу после установки или обновления. Стоит помнить: если webroot правильно указывает на public, путь /install/install.php уже возвращает 404 — но удаление файла является рекомендуемой эшелонированной защитой.

Да, и это рекомендуемое усиление для чувствительных сред. GLPI поддерживает константы GLPI_CONFIG_DIR и GLPI_VAR_DIR, чтобы направить config/ и files/ в каталоги вне веб-дерева (например, /etc/glpi и /var/lib/glpi). Тогда, даже если в будущем кто-то ошибётся с webroot, в отдаваемом веб-сервером пути не будет ни учётных данных, ни логов.

Выполните curl к URL, которых не должно существовать при правильном webroot, например /config/config_db.php, /vendor/ и /files/_log/php-errors.log. Все они должны возвращать 404. Остерегайтесь ложноположительного результата: config_db.php может отвечать 200 с пустым телом, если PHP его выполняет, — это уже признак неверного webroot. Критерий — 404, а не пустая страница.

Нужна помощь?