In a previous article, I discussed how to set up ModSecurity with the Core Rule Set on Debian. This can be considered as a first line of defense against malicious HTTP traffic. In a defense in depth strategy of course we want to add additional layers of protection to your web servers. One such layer is Snuffleupagus. Snuffleupagus is a PHP module which protects your web applications against various attacks. Some of the hardening features it offers are encryption of cookies, disabling XML External Entity (XXE) processing, a white or blacklist for the functions which can be used in the eval()
function and the possibility to selectively disable PHP functions with specific arguments (virtual-patching).
Installing Snuffleupagus on Debian
Unfortunately there is no package for Snuffleupagus included in Debian, but it is not too difficult to build one yourself:
$ apt install php-dev
$ mkdir snuffleupagus
$ cd snuffleupagus
$ git clone https://github.com/jvoisin/snuffleupagus
$ cd snuffleupagus
$ make debian
This will build the latest development code from the master branch. If you want to build the latest stable release, before running make debian
, use these commands to view all tags and to checkout the latest table tag, which in this case was v0.8.2:
$ git tag
$ git checkout v0.8.2
If all went well, you should now have a file snuffleupagus_0.8.2_amd64.deb
in the above directory, which you can install:
$ cd .. $ apt install ./snuffleupagus_0.8.2_amd64.deb
Configuring Snuffleupagus
First we take the example configuration file and put it in PHP’s configuration directory. For example for PHP 7.4:
# zcat /usr/share/doc/snuffleupagus/examples/default.rules.gz > /etc/php/7.4/snuffleupagus.rules
Also take a look at the config subdirectory in the source tree for more example rules.
Edit the file /etc/php/7.4/fpm/conf.d/20-snuffleupagus.ini
so that it looks like this:
extension=snuffleupagus.so
sp.configuration_file=/etc/php/7.4/snuffleupagus.rules
Now we will edit the file /etc/php/7.4/snuffleupagus.rules
.
We need to set a secret key, which will be used for various cryptographic features:
sp.global.secret_key("YOU _DO_ NEED TO CHANGE THIS WITH SOME RANDOM CHARACTERS.");
You can generate a random key with this shell command:
$ echo $(head -c 512 /dev/urandom | tr -dc 'a-zA-Z0-9')
Simulation mode
Snuffleupagus can run rules in simulation mode. In this mode, the rule will not block further execution of the PHP file, but will just output a warning message in your log. Unfortunately there is no global simulation mode, but it has to be set per rule. You can run a rule in simulation mode by appending .simulation()
to it. For example to run INI protection in simulation mode:
sp.ini_protection.simulation();
INI protection
To prevent PHP applications from modifying php.ini settings, you can set this in snuffleupagus.rules:
sp.ini_protection.enable();
sp.ini_protection.policy_readonly();
Cookie protection
The following configuration options sets the SameSite attribute to Lax on session cookies, which offers protection against CSFR on this cookie. We enforce setting the secure
option on cookies, which instructs the web browser to only send them over an encrypted HTTPS connection and also enable encryption of the content of the session on the server. The encryption key being used is derived of the value of the global secret key you have set, the client’s user agent and the environment variable SSL_SESSION_ID
.
sp.cookie.name("PHPSESSID").samesite("lax");
sp.auto_cookie_secure.enable();
sp.global.cookie_env_var("SSL_SESSION_ID");
sp.session.encrypt();
Note that the definition of cookie_env_var
needs to happen before sp.session.encrypt();
which enables the encryption.
You have to make sure the variable SSL_SESSION_ID
is passed to PHP. In Apache you can do so by having this in your virtualhost:
<FilesMatch "\.(cgi|shtml|phtml|php)$">
SSLOptions +StdEnvVars
</FilesMatch>
eval white- or blacklist
eval() is used to evaluate PHP content, for example in a variable. This is very dangerous if the PHP code to be evaluated can contain user provided data. Therefore it is strongly recommended that you create a whitelist of functions which can be called by code evaluated by eval().
Start by putting this in snuffleupagus.rules and restart PHP:
sp.eval_whitelist.list().simulation();
Then test your websites and see which errors you get in the logs, and add them separated by commas to the eval_whitelist.list()
. After that you need to remove .simulation() and restart PHP in order to activate this protection. For example
sp.eval_whitelist.list("array_pop,array_push");
You can also use a blacklist, which only blocks certain functions. For example:
sp.eval_blacklist.list("system,exec,shell_exec,proc_open");
Limit execution to read-only PHP files
The read_only_exec()
feature of Snuffleupagus will prevent PHP from execution of PHP files on which the PHP process has write permissions. This will block any attacks where an attacker manages to upload a malicious PHP file via a bug in your website, and then attempts to execute this malicious PHP script.
It is a good practice to let your PHP scripts be owned by a different user than the PHP user, and give PHP only read-only permissions on your PHP files.
To test this feature, add this to snuffleupagus.rules:
sp.readonly_exec.simulation();
If you are sure all goes well, enable it:
sp.readonly_exec.enable();
Virtual patching
One of the main features of Snuffleupagus is virtual patching. Thjs feature will disable functions, depending on the parameters or and values they are given. The example rules file contains a good set of generic rules which blocks all kinds of dangerous behaviour. You might need to fine-tune the rules if your PHP applications hits certain rules.
Some examples of virtual-patching rules:
sp.disable_function.function("chmod").param("mode").value("438").drop();
sp.disable_function.function("chmod").param("mode").value("511").drop();
These rules will drop calls to the chmod
function with octal values 438 and 511, which correspond to the dangerous 0666 and 0777 decimal permissions.
sp.disable_function.function("include_once").value_r(".(inc|phtml|php)$").allow();
sp.disable_function.function("include_once").drop();
These two rules will only allow the include_once
function to include files which file name are ending with inc, phtml or php. All other include_once
calls will be dropped.
Using generate-rules.php to automatically site-specific hardening rules
In the scripts subdirectoy of the Snuffleupagus source tree, there is a file named generate_rules.php
. You can run this script from the command line, giving it a path to a directory with PHP files, and it will automatically generate rules which specifically allow all needed dangerous function calls, and then disable them globally. For example to generate rules for the /usr/share/tt-rss/www
and /var/www
directories:
# php generate_rules.php /usr/share/tt-rss/www/ /var/www/
This will generate rules:
sp.disable_function.function("function_exists").filename("/usr/share/tt-rss/www/api/index.php").hash("fa02a93e2724d7e818c5c13f4ba8b110c47bbe7fb65b74c0aad9cff2ed39cf7d").allow();
sp.disable_function.function("function_exists").filename("/usr/share/tt-rss/www/classes/pref/prefs.php").hash("43926a95303bc4e7adefe9d2f290dd8b66c9292be836908081e3f2bd8a198642").allow();
sp.disable_function.function("function_exists").drop();
The first two rules allow these two files to call function_exists
and the last rule drops all requests to function_exists
from any other files. Note that the first two rules limit the rules not only to the specified file name, but also define the SHA256 of the file. This way, if the file is changed, the function call will be dropped. This is the safest way, but it can be annoying if the files are often or automatically updated because it will break the site. In this case, you can call generate_rules.php
with the --without-hash
option:
# php generate_rules.php --without-hash /usr/share/tt-rss/www/ /var/www/
After you have generated the rules, you will have to add them to your snuffleupagus.rules
file and restart PHP-FPM.
File Upload protection
The default Snuffleupagus rule file contains 2 rule which will block any attempts uploading a html or PHP file. However, I noticed that they were not working with PHP 7.4 and these rules would cause this error message:
PHP Warning: [snuffleupagus][0.0.0.0][config][log] It seems that you are filtering on a parameter 'destination' of the function 'move_uploaded_file', but the parameter does not exists. in /var/www/html/foobar.php on line 15PHP message: PHP Warning: [snuffleupagus][0.0.0.0][config][log] - 0 parameter's name: 'path' in /var/www/html/foobar.php on line 15PHP message: PHP Warning: [snuffleupagus][0.0.0.0][config][log] - 1 parameter's name: 'new_path' in /var/www/html/foobar.php on line 15'
The snuffleupagus rules use the parameter destination for the move_uploaded_file instead of the parameter new_path. You will have to change the rules like this:
sp.disable_function.function("move_uploaded_file").param("new_path").value_r("\.ph").drop();<br />sp.disable_function.function("move_uploaded_file").param("new_path").value_r("\.ht").drop();
Note that on PHP 8, the parameter name is to
instead of new_path
.
Enabling Snuffleupagus
To enable Snuffleupagus in PHP 7.4, link the configuration file to /etc/php/7.4/fpm/conf.d
:
# cd /etc/php/7.4/fpm/conf.d
# ln -s ../../mods-available/snuffleupagus.ini 20-snuffleupagus.ini
# systemctl restart php7.4-fpm
After restarting PHP-FPM, always check the logs to see whether snuffleupagus does not give any warning or messages for example because of a syntax error in your configuration:
# journalctl -u php7.4-fpm -n 50
Snuffleupagus logs
By default Snuffleupagus logs via PHP. Then if you are using Apache with PHP-FPM, you will find Snuffleupagus logs, just like any PHP warnings and errors in the Apache error_log, for example /var/log/apache/error.log
. If you encounter any problems with your website, go check this log to see what is wrong.
Snuffleupagus can also be configured to log via syslog, and actually even recommends this, because PHP’s logging system can be manipulated at runtime by malicious scripts. To log via syslog, add this to snuffleupagus.rules:
sp.log_media("syslog");
I give a few examples of errors you can encounter in the logs and how to fix them:
[snuffleupagus][0.0.0.0][xxe][log] A call to libxml_disable_entity_loader was tried and nopped in /usr/share/tt-rss/www/include/functions.php on line 22
tt-rss calls the function libxml_disable_entity_loader
but this is blocked by the XXE protection. Commenting this in snuffleupagus.rules should fix this:
sp.xxe_protection.enable();
Another example:
[snuffleupagus][0.0.0.0][disabled_function][drop] Aborted execution on call of the function 'ini_set', because its argument '$varname' content (display_errors) matched a rule in /usr/share/tt-rss/www/include/functions.php on line 37'
Modifying the PHP INI option display_errors is not allowed by this rule:
sp.disable_function.function("ini_set").param("varname").value_r("display_errors").drop();
You can completely remove (or comment) this rule in order to disable it. But a better way is to add a rule before this rule which allows it for specially that PHP file. So add this rule before:
sp.disable_function.function("ini_set").filename("/usr/share/tt-rss/www/include/functions.php").param("varname").value_r("display_errors").allow();
If you get something like this:
[snuffleupagus][0.0.0.0][disabled_function][drop] Aborted execution on call of the function 'function_exists', because its argument '$function_name' content (exec) matched a rule in /var/www/wordpress/wp-content/plugins/webp-express/vendor/rosell-dk/exec-with-fallback/src/ExecWithFallback.php on line 35', referer: wp-admin/media-new.php
It’s caused by this rule:
sp.disable_function.function("function_exists").param("function_name").value("exec").drop();
You can add this rule before to allow this:
sp.disable_function.function("function_exists").filename("/var/www/wordpress/wp-admin/media-new.php").param("function_name").value("exec").allow();