If your GLPI DocumentRoot points at the install root instead of the public folder, the file holding your database password is one GET request away. Since GLPI 10 this is the single most important security decision of the deploy - and the one we most often find wrong in environments that reach us for maintenance, precisely because the system works perfectly even when it is misconfigured.
Why the wrong webroot goes unnoticed
GLPI 10 still opens and runs normally if you point the web server at the root (/var/www/glpi) instead of /var/www/glpi/public. Nothing breaks on screen, nobody files a ticket, and that is why the flaw survives for years. When onboarding client GLPI fleets for maintenance, this is the most common silent finding: the environment responds fine, but https://glpi.client.com/files/_log/php-errors.log hands back the application log as an ordinary download.
The detail that only shows up in operations: on Apache, the old install was partially saved by the .htaccess files scattered inside config/, files/ and similar folders, which denied access. That gives a false sense of protection. On Nginx those .htaccess files simply do not exist - Nginx never reads them. So migrating from Apache to Nginx while carrying the wrong root turns a partial exposure into a total one: files/, config/ and the entire source tree become static content served to anyone. Switching web servers is the classic moment when this bomb goes off.
What gets exposed when the DocumentRoot is wrong
The public folder exists to publish only the router (index.php), the .htaccess and the static files (CSS, JS, images). Everything else must stay out of the browser's reach. The table below is the verification matrix we use: with public as the webroot, every URL below must return 404.
| Path | Contents | Impact if exposed | Expected status |
|---|---|---|---|
/config/config_db.php | Database host, user and password | Full database leak | 404 |
/files/_log/ | php-errors.log, sql-errors.log | Application recon and PII leak | 404 |
/files/_dumps/ | SQL dumps produced by GLPI | Downloadable copy of the database | 404 |
/files/_sessions/ | Active session files | Session hijacking and CSRF token theft | 404 |
/src/ | Application PHP source code | Application map for exploitation | 404 |
/vendor/ | Composer dependencies | Exploiting known library CVEs | 404 |
/install/install.php | Installation wizard | Reconfiguration and environment takeover | 404 (remove after install) |
Beware the false "safe": with PHP active, /config/config_db.php may answer 200 with an empty body (the file only defines a class, it prints nothing). An empty 200 does not mean you are fine - it means the file sits inside the webroot. What actually leaks as a download are the non-PHP files: files/_log/*.log, files/_dumps/*.sql and any backup forgotten in the tree. That is why the ruler is 404, not "blank page".
Apache: hardened virtual host
The DocumentRoot points at public, directory listing is off, and plain HTTP is redirected to HTTPS. FallbackResource replaces the .htaccess routing without requiring AllowOverride All:
# /etc/apache2/sites-available/glpi.conf
<VirtualHost *:80>
ServerName glpi.yourcompany.com
# No GLPI over plain HTTP
Redirect permanent / https://glpi.yourcompany.com/
</VirtualHost>
<VirtualHost *:443>
ServerName glpi.yourcompany.com
DocumentRoot /var/www/glpi/public
<Directory /var/www/glpi/public>
Require all granted
Options -Indexes +FollowSymLinks
# Routes without depending on AllowOverride All
FallbackResource /index.php
</Directory>
# Deny any hidden file (.git, .env, .htaccess)
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# Security headers
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: without the .htaccess safety net
On Nginx there is no .htaccess to save you, so the root has to be public from the start. Note two lines that are almost always missing: the try_files $uri =404; inside the PHP block and the explicit hidden-file block.
# /etc/nginx/sites-available/glpi.conf
server {
listen 80;
server_name glpi.yourcompany.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name glpi.yourcompany.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 blocks execution via forged PATH_INFO
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 ignores .htaccess: deny hidden files by hand
location ~ /\. {
deny all;
}
}
The common mistake here is copying a generic PHP block from the internet without try_files $uri =404;. Without it, a request like /uploads/photo.jpg/x.php can be passed to FPM via PATH_INFO and executed, opening the door to RCE if the attacker manages to write a file into the tree. With =404, Nginx refuses before reaching PHP.
Post-deploy verification: the 30-second test
After reloading the web server, run this script against the real domain. Every line must say OK. Any FAIL is a served path that should not exist:
#!/usr/bin/env bash
BASE="https://glpi.yourcompany.com"
# Every URL below MUST return 404 on a correct webroot
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 "FAIL ${path} -> ${code} (EXPOSED)"
fi
done
In maintenance we pin this script to the onboarding checklist of every new client and run it again after each web-server swap, upgrade or reverse-proxy change - because a correct webroot is not a permanent state, it is something a careless migration reverts in seconds.
Production checklist
- DocumentRoot / root pointing at
.../public, never at the install root. - Remove
install/install.phpas soon as the install/upgrade finishes (GLPI itself shows a red warning while the file exists). - Force HTTPS with a 301 redirect from port 80 and
Strict-Transport-Securityenabled. - Optional hardening: move
config/andfiles/out of the web tree viaGLPI_CONFIG_DIRandGLPI_VAR_DIR, leaving the webroot with onlypublic. - Run the verification script and require 404 on every path.
- Check GLPI's own security panel (Setup) after startup - it flags pending items such as a present
install.php.
Fixing the webroot takes ten minutes; finding out that the database password was downloadable for three years costs far more. Whether you are standing up a fresh GLPI, migrating from Apache to Nginx, or inheriting an environment with no config history, the NexTool GLPI maintenance team validates the webroot, hardens the virtual host and runs the verification battery as part of onboarding. Talk to us about GLPI maintenance.
This content was produced with the help of artificial intelligence and reviewed by the Nextool Solutions team.