Доступность папки user¶
Панель управления Grav может предупредить, если безобидный тестовый файл в приватной папке хранилища доступен напрямую через веб:
Your web server is not applying Grav's access rules.
Отнеситесь к подтверждённой доступности серьёзно. В зависимости от того, какие папки и типы файлов открывает сервер, посетители могли бы скачать файлы учётных записей, конфигурацию, сохранённые отправки форм или резервные копии всего сайта. Проверка использует безобидные файлы-маркеры; она не скачивает ваши настоящие приватные данные.
Обновлённые версии Admin2 и API проверяют файлы .dat, .txt и .zip в user/data, backup и tmp и перечисляют найденные доступными комбинации папок и типов файлов. Старые версии проверяют только файл .dat в user/data; успешное прохождение этой одной проверки не доказывает, что другие типы файлов или папки защищены. Сетевые ошибки и неудачные проверки ничего не доказывают, а отсутствие предупреждения не является полным аудитом безопасности.
Почему так происходит¶
Grav полагается на веб-сервер в том, что тот запрещает прямые запросы к его приватным папкам. Блокировка перестаёт работать, когда:
- Вы используете Apache, но файлы
.htaccessигнорируются, потому что для корня сайта заданоAllowOverride None. Это самая частая причина. - Вы используете Nginx, Caddy или другой сервер, который не читает
.htaccessвообще, а эквивалентные правила так и не были добавлены в конфигурацию сайта. - Фронтенд-прокси сам отдаёт существующие статические файлы и передаёт PHP или Apache только остальные запросы. Фронтенд-сервер может открывать
.txtи.zip, даже когда запросы к.datдоходят до Apache и получают 403. - Пользовательская или предоставленная хостингом конфигурация сервера заменила поставляемые с Grav правила, не перенеся эти блокировки.
Как исправить¶
Цель одинакова на любом сервере: запретить прямой веб-доступ к backup, tmp, logs, user/accounts, user/config и user/data (каждой из них также внутри user/env/<name>/), но по-прежнему разрешать файлы, которые Grav считает публичными, а именно изображения аватаров в user/accounts и медиафайлы и ресурсы, загруженные в user/data. Эти исключения должны идти до соответствующего правила запрета, иначе они никогда не сработают.
Эти правила также срабатывают при необязательном префиксе env/<name>/, поэтому в мультисайте собственные папки config, accounts и data каждого подсайта в user/env/<name>/ защищены, а его темы, плагины и ресурсы остаются публичными. До Grav 2.2.4 правила блокировали всё в user/env, что ломало мультисайты. Обновление до 2.2.4 или новее обновляет корневой .htaccess существующего сайта и заменяет запрещающий всё user/env/.htaccess, который мог добавить предыдущий апгрейд, на файл, блокирующий только эти три папки. На Nginx, Caddy, lighttpd и IIS конфигурацию сервера нужно обновить самостоятельно.
Также запретите выполнение PHP и других скриптов в images/ и assets/. Grav пишет туда только кэшированные изображения и объединённые CSS и JavaScript, но обе папки публичны, поэтому скрипт, попавший в любую из них из-за ошибки плагина или хостинга, иначе выполнился бы. Grav 2.1.10 и новее поставляются с этим правилом, и обновление добавляет его в корневой .htaccess существующего сайта. На Nginx, Caddy и других серверах его нужно добавить самостоятельно.
Apache¶
Сначала убедитесь, что .htaccess учитывается. В виртуальном хосте (или соответствующем блоке <Directory>) задайте:
Перезагрузите Apache (sudo systemctl reload apache2 или sudo apachectl graceful). Сравните корневой .htaccess с текущей версией Grav и убедитесь, что в нём есть эти правила; старые версии могут не содержать tmp:
# Блокировать приватное хранилище независимо от расширения файлов
RewriteRule ^(backup|tmp|logs)/(.*) error [F,NC]
# Блокировать весь прямой доступ к этим чувствительным папкам user, независимо от типа файлов
RewriteRule ^(user)/(env/[^/]+/)?config/(.*) error [F,NC]
# Блокировать и user/accounts, но разрешить прямую отдачу изображений аватаров, где бы они ни
# хранились: в user/accounts/avatars/<file> или user/accounts/<username>/<file>
RewriteCond %{REQUEST_URI} !/user/(env/[^/]+/)?accounts/[^/]+/[^/]+\.(jpe?g|png|gif|webp|avif|bmp|ico)$ [NC]
RewriteRule ^(user)/(env/[^/]+/)?accounts/(.*) error [F,NC]
# Блокировать и user/data, но разрешить загрузку публичных ресурсов (например, изображений Flex-объектов)
RewriteCond %{REQUEST_URI} !\.(jpe?g|png|gif|webp|avif|bmp|ico|mp4|webm|ogg|ogv|mov|mp3|wav|m4a|flac|pdf|woff2|woff|ttf|otf|eot|css|js)$ [NC]
RewriteRule ^(user)/(env/[^/]+/)?data/(.*) error [F,NC]
# Блокировать выполнение скриптов в публичных папках кэша (производные изображения и объединённые ресурсы)
RewriteRule ^(images|assets)/(.*)\.(php|php2|php3|php4|php5|php7|php8|phar|phtml|pht|phtm|phps|pl|py|cgi|sh|bat)$ error [F,NC]
Если включить AllowOverride нельзя, скопируйте эти правила в конфигурацию виртуального хоста.
Примечание
Если вы заменяли или сильно редактировали поставляемый .htaccess, сравните его с текущим в репозитории Grav и убедитесь, что блок безопасности присутствует.
Nginx¶
Nginx не читает .htaccess. Сравните свою конфигурацию с поставляемым webserver-configs/nginx.conf и добавьте эти блоки location перед общими обработчиками статических файлов. Скорректируйте пути, если Grav установлен в подпапке или использует собственные пути хранилища:
# запретить приватное хранилище независимо от расширения файлов
location ~* ^/(backup|tmp|logs)/ { return 403; }
# запретить весь прямой доступ к этим чувствительным папкам user, независимо от типа файлов
location ~* ^/user/(env/[^/]+/)?config/.*$ { return 403; }
# разрешить прямую отдачу изображений аватаров в user/accounts, где бы они ни хранились:
# в user/accounts/avatars/<file> или user/accounts/<username>/<file>; это правило должно идти
# до запрета user/accounts, чтобы сработать первым при первом совпадении
location ~* ^/user/(env/[^/]+/)?accounts/[^/]+/[^/]+\.(jpe?g|png|gif|webp|avif|bmp|ico)$ { try_files $uri =404; }
# запретить всё остальное в user/accounts
location ~* ^/user/(env/[^/]+/)?accounts/.*$ { return 403; }
# разрешить прямую отдачу публичных медиафайлов в user/data;
# это правило должно идти до запрета user/data, чтобы сработать при совпадении
location ~* ^/user/(env/[^/]+/)?data/.*\.(jpe?g|png|gif|webp|avif|bmp|ico|mp4|webm|ogg|ogv|mov|mp3|wav|m4a|flac|pdf)$ { try_files $uri =404; }
# запретить всё остальное в user/data
location ~* ^/user/(env/[^/]+/)?data/.*$ { return 403; }
# запретить выполнение скриптов в публичных папках кэша (производные изображения и объединённые ресурсы);
# это правило должно идти до общего блока `location ~ \.php$`, который передаёт .php файлы в PHP-FPM
location ~* ^/(images|assets)/.*\.(php|php2|php3|php4|php5|php7|php8|phar|phtml|pht|phtm|phps|pl|py|cgi|sh|bat)$ { return 403; }
Затем перезагрузите Nginx (sudo nginx -t && sudo systemctl reload nginx). Полная рекомендуемая конфигурация описана в разделе Nginx.
Caddy¶
Caddy тоже игнорирует .htaccess. Сравните свою конфигурацию с поставляемым webserver-configs/Caddyfile. Здесь важны два поведения Caddy, и оба молча не срабатывают, если ошибиться:
- Средство сопоставления
pathв Caddy литеральное: оно понимает подстановочные знаки*, но не является регулярным выражением. Всё, что требует альтернации, классов символов или якорей, должно использоватьpath_regexp. - Вне блока
routeCaddy применяет собственный порядок директив, а не тот, в котором вы их написали, и глобальная перезаписьtry_filesвыполняется доrespond. Это отключает все правила запрета для любого пути, который Caddy не может сопоставить с файлом на диске. Поместите правила в блокroute, который выполняется сверху вниз в написанном порядке.
Средства сопоставления Caddy компилируются с Go RE2, где нет lookbehind, поэтому исключения для медиафайлов используют отрицающее средство сопоставления, а не встроенное:
@denied_dirs path_regexp (?i)^/(\.git|cache|bin|logs|backups?|tmp|tests)/
@denied_user_config path_regexp (?i)^/user/(env/[^/]+/)?config/
# блокировать user/accounts, но разрешить прямую отдачу изображений аватаров
@denied_user_accounts {
path_regexp (?i)^/user/(env/[^/]+/)?accounts/
not path_regexp (?i)^/user/(env/[^/]+/)?accounts/[^/]+/[^/]+\.(jpe?g|png|gif|webp|avif|bmp|ico)$
}
# блокировать user/data, но разрешить загрузку публичных медиафайлов (например, изображений Flex-объектов)
@denied_user_data {
path_regexp (?i)^/user/(env/[^/]+/)?data/
not path_regexp (?i)\.(jpe?g|png|gif|webp|avif|bmp|ico|mp4|webm|ogg|ogv|mov|mp3|wav|m4a|flac|pdf)$
}
# запретить выполнение скриптов в публичных папках кэша (производные изображения и объединённые ресурсы)
@denied_cache_scripts path_regexp (?i)^/(images|assets)/.*\.(php|php2|php3|php4|php5|php7|php8|phar|phtml|pht|phtm|phps|pl|py|cgi|sh|bat)$
route {
respond @denied_dirs 403
respond @denied_user_config 403
respond @denied_user_accounts 403
respond @denied_user_data 403
respond @denied_cache_scripts 403
# глобальная перезапись должна идти последней
try_files {path} {path}/ /index.php?_url={uri}&{query}
php_fastcgi 127.0.0.1:9000
file_server
}
LiteSpeed¶
LiteSpeed читает .htaccess и совместим с правилами Apache для Grav, поэтому достаточно включить .htaccess (эквивалент AllowOverride All). Убедитесь, что правила перезаписи включены для виртуального хоста.
За прокси, CDN или управляемым хостингом¶
Если ваш сайт находится за обратным прокси или CDN либо работает на управляемом или общем хостинге, правила должны быть применены на том уровне, который фактически отдаёт файлы. Если не уверены, какой сервер стоит спереди, уточните у хостинг-провайдера.
Управляемый хостинг и хранилище вне корня сайта¶
На таких хостингах, как Cloudways, выясните, какой стек отдаёт статические файлы. Правила Apache не могут защитить файл, который напрямую отдаёт nginx, даже если Apache стоит за ним. Попросите хостинг применить правила запрета на фронтенд-сервере перед его общими обработчиками статических файлов. Блокировка доступа в PHP не может перехватить такие запросы.
В версиях Grav с собственной поддержкой .env можно перенести резервные копии и временные загрузки, задав абсолютные пути в .env в корне Grav:
Выбирайте папки вне корня сайта и любых публичных псевдонимов сервера, доступные для записи процессу PHP. Эти настройки перенаправляют backup:// и tmp://; они не перемещают и не удаляют старые файлы. Переместите существующие резервные копии из старой публичной папки и удалите устаревшие временные файлы, когда не выполняется установка или обновление. Проверьте, что прежние URL больше не возвращают их содержимое. Перенос этих папок не защищает user/accounts, user/config или сохранённые отправки форм; для них по-прежнему нужны правила доступа.
Действие save плагина Form по умолчанию использует .txt. На хостинге, который отдаёт .txt напрямую, но блокирует .dat, настройте extension: dat как временную меру и убедитесь, что URL сохранённого файла запрещён. Одно расширение файла не является контролем доступа: стек только на nginx без правил запрета может отдавать и то, и другое. Расширения вроде yaml, yml, json и md могут отклоняться параметром security.uploads_dangerous_extensions; не отключайте эту защиту, чтобы изменить формат отправки.
Как убедиться, что проблема исправлена¶
Снова откройте панель управления после перезагрузки сервера. Предупреждение проверяется в реальном времени: оно пытается скачать тестовый файл так же, как это сделал бы посетитель, поэтому после блокировки прямого доступа баннер исчезнет при следующей загрузке.
Можно также проверить вручную, используя только безобидные файлы. Запишите короткий случайный маркер в новые файлы .dat, .txt и .zip в каждой проверяемой папке, запросите эти URL без аутентификации и затем удалите тестовые файлы. Ответ, содержащий маркер, доказывает доступность. Ответ 403 или 404 блокирует конкретную проверку; ошибка сервера, страница входа или неудачный запрос не доказывают защиту. Не используйте настоящие резервные копии, файлы учётных записей или отправки в качестве тестовых данных.
О публичных медиафайлах в user/data¶
Grav намеренно разрешает прямую отдачу обычных файлов изображений, аудио, видео и PDF в user/data, чтобы медиафайлы, загруженные через Flex-объекты и похожие поля, продолжали работать. Так задумано, и предупреждение не об этом. Предупреждение срабатывает только тогда, когда доступны файлы, не являющиеся медиафайлами, в ваших приватных папках. Чтобы безопасно отдавать приватные или произвольные загруженные файлы, направляйте их через прокси на уровне приложения, а не открывайте папку.