From e5ab9821f9c7b3d80224222d9928526f5d3743bb Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 18 Nov 2022 13:25:02 +0100 Subject: [PATCH] Add snuffleupagus This seems to work in my limited testing. --- core/base/Dockerfile | 18 +++- webmails/Dockerfile | 6 +- webmails/php.ini | 2 + webmails/roundcube/config/config.inc.php | 2 +- webmails/snuffleupagus.rules | 126 +++++++++++++++++++++++ webmails/start.py | 6 +- 6 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 webmails/snuffleupagus.rules diff --git a/core/base/Dockerfile b/core/base/Dockerfile index d5be6a90..ca62e773 100644 --- a/core/base/Dockerfile +++ b/core/base/Dockerfile @@ -52,16 +52,32 @@ RUN set -euxo pipefail \ mkdir -p /root/.cargo/registry/index && \ git clone --bare https://github.com/rust-lang/crates.io-index.git /root/.cargo/registry/index/github.com-1285ae84e5963aae \ ; pip install -r requirements-${MAILU_DEPS}.txt \ - ; apk del -r .build-deps \ ; rm -rf /root/.cargo /tmp/*.pem \ ; } \ ; rm -rf /root/.cache +ARG SNUFFLEUPAGUS_VERSION=0.8.3 +ENV SNUFFLEUPAGUS_URL https://github.com/jvoisin/snuffleupagus/archive/refs/tags/v$SNUFFLEUPAGUS_VERSION.tar.gz + +RUN set -euxo pipefail \ + ; curl -sL ${SNUFFLEUPAGUS_URL} | tar xz \ + ; cd snuffleupagus-$SNUFFLEUPAGUS_VERSION \ + ; rm -rf src/tests/*php7*/ src/tests/*session*/ src/tests/broken_configuration/ src/tests/*cookie* src/tests/upload_validation/ \ + ; apk add --virtual .build-deps php81-dev php81-cgi php81-simplexml php81-xml pcre-dev build-base php81-pear php81-openssl re2c \ + ; ln -s /usr/bin/phpize81 /usr/bin/phpize \ + ; ln -s /usr/bin/pecl81 /usr/bin/pecl \ + ; ln -s /usr/bin/php-config81 /usr/bin/php-config \ + ; ln -s /usr/bin/php81 /usr/bin/php \ + ; pecl install vld-beta \ + ; make -j $(grep -c processor /proc/cpuinfo) release \ + ; cp src/.libs/snuffleupagus.so /app \ + ; apk del -r .build-deps # base mailu image FROM system COPY --from=build /app/venv/ /app/venv/ +COPY --chown=root:root --from=build /app/snuffleupagus.so /usr/lib/php81/modules/ ENV VIRTUAL_ENV=/app/venv ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" diff --git a/webmails/Dockerfile b/webmails/Dockerfile index 376399bf..0ec12384 100644 --- a/webmails/Dockerfile +++ b/webmails/Dockerfile @@ -21,6 +21,8 @@ RUN set -euxo pipefail \ ; ln -s /usr/bin/php81 /usr/bin/php \ ; gpg --import /tmp/snappymail.asc \ ; gpg --import /tmp/roundcube.asc \ + ; echo extension=snuffleupagus > /etc/php81/conf.d/snuffleupagus.ini \ + ; rm -f /tmp/*asc \ ; mkdir -p /run/nginx \ ; mkdir -p /conf @@ -41,7 +43,8 @@ RUN set -euxo pipefail \ ; cd roundcube \ ; rm -rf CHANGELOG.md SECURITY.md INSTALL LICENSE README.md UPGRADING composer.json-dist installer composer.* \ ; ln -sf index.php /var/www/roundcube/public_html/sso.php \ - ; rm -rf plugins/{autologon,example_addressbook,http_authentication,krb_authentication,new_user_identity,password,redundant_attachments,squirrelmail_usercopy,userinfo,virtuser_file,virtuser_query} + ; rm -rf plugins/{autologon,example_addressbook,http_authentication,krb_authentication,new_user_identity,password,redundant_attachments,squirrelmail_usercopy,userinfo,virtuser_file,virtuser_query} \ + ; sed -i '/suhosin.session.encrypt/d;/mbstring\.func_overload/d' program/lib/Roundcube/bootstrap.php COPY roundcube/config/config.inc.php /conf/ COPY roundcube/login/mailu.php /var/www/roundcube/plugins/mailu/ @@ -81,6 +84,7 @@ COPY start.py / COPY php.ini /defaults/ COPY php-webmail.conf /etc/php81/php-fpm.d/php-webmail.conf COPY nginx-webmail.conf /conf/ +COPY snuffleupagus.rules /etc/snuffleupagus.rules.tpl EXPOSE 80/tcp VOLUME /data diff --git a/webmails/php.ini b/webmails/php.ini index d9ba892c..87660288 100644 --- a/webmails/php.ini +++ b/webmails/php.ini @@ -11,3 +11,5 @@ log_errors=On zlib.output_compression=Off access.log = /dev/fd/2 error_log = /dev/fd/2 +module=snuffleupagus.so +sp.configuration_file=/etc/snuffleupagus.rules diff --git a/webmails/roundcube/config/config.inc.php b/webmails/roundcube/config/config.inc.php index 6e5ea0bd..e8aedeff 100644 --- a/webmails/roundcube/config/config.inc.php +++ b/webmails/roundcube/config/config.inc.php @@ -5,7 +5,7 @@ $config = array(); // Generals $config['db_dsnw'] = '{{ DB_DSNW }}'; $config['temp_dir'] = '/dev/shm/'; -$config['des_key'] = '{{ SECRET_KEY }}'; +$config['des_key'] = '{{ ROUNDCUBE_KEY }}'; $config['cipher_method'] = 'AES-256-CBC'; $config['identities_level'] = 0; $config['reply_all_mode'] = 1; diff --git a/webmails/snuffleupagus.rules b/webmails/snuffleupagus.rules new file mode 100644 index 00000000..35f0b7ce --- /dev/null +++ b/webmails/snuffleupagus.rules @@ -0,0 +1,126 @@ +# This is based on default configuration file for Snuffleupagus (https://snuffleupagus.rtfd.io), +# for php8. +# It contains "reasonable" defaults that won't break your websites, +# and a lot of commented directives that you can enable if you want to +# have a better protection. + +# Harden the PRNG +sp.harden_random.enable(); + +# Disabled XXE +sp.xxe_protection.enable(); + +# Global configuration variables +sp.global.secret_key("{{ SNUFFLEPAGUS_KEY }}"); + +# Globally activate strict mode +# https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.strict +sp.global_strict.enable(); + +# Prevent unserialize-related exploits +# sp.unserialize_hmac.enable(); + +# Only allow execution of read-only files. This is a low-hanging fruit that you should enable. +sp.readonly_exec.enable(); + +# PHP has a lot of wrappers, most of them aren't usually useful, you should +# only enable the ones you're using. +sp.wrappers_whitelist.list("file,php,phar,mailsosubstreams"); + +# Prevent sloppy comparisons. +sp.sloppy_comparison.enable(); + +# Use SameSite on session cookie +# https://snuffleupagus.readthedocs.io/features.html#protection-against-cross-site-request-forgery +sp.cookie.name("PHPSESSID").samesite("lax"); + +# Harden the `chmod` function (0777 (oct = 511, 0666 = 438) +sp.disable_function.function("chmod").param("permissions").value("438").drop(); +sp.disable_function.function("chmod").param("permissions").value("511").drop(); + +# Prevent various `mail`-related vulnerabilities +sp.disable_function.function("mail").param("additional_parameters").value_r("\\-").drop(); + +# Since it's now burned, me might as well mitigate it publicly +sp.disable_function.function("putenv").param("assignment").value_r("LD_").drop() + +# This one was burned in Nov 2019 - https://gist.github.com/LoadLow/90b60bd5535d6c3927bb24d5f9955b80 +sp.disable_function.function("putenv").param("assignment").value_r("GCONV_").drop() + +# Since people are stupid enough to use `extract` on things like $_GET or $_POST, we might as well mitigate this vector +sp.disable_function.function("extract").param("array").value_r("^_").drop() +sp.disable_function.function("extract").param("flags").value("0").drop() + +# This is also burned: +# ini_set('open_basedir','..');chdir('..');…;chdir('..');ini_set('open_basedir','/');echo(file_get_contents('/etc/passwd')); +# Since we have no way of matching on two parameters at the same time, we're +# blocking calls to open_basedir altogether: nobody is using it via ini_set anyway. +# Moreover, there are non-public bypasses that are also using this vector ;) +sp.disable_function.function("ini_set").param("option").value_r("open_basedir").drop() + +# Prevent various `include`-related vulnerabilities +sp.disable_function.function("require_once").value_r("\.(inc|phtml|php)$").allow(); +sp.disable_function.function("include_once").value_r("\.(inc|phtml|php)$").allow(); +sp.disable_function.function("require").value_r("\.(inc|phtml|php)$").allow(); +sp.disable_function.function("include").value_r("\.(inc|phtml|php)$").allow(); +sp.disable_function.function("require_once").drop() +sp.disable_function.function("include_once").drop() +sp.disable_function.function("require").drop() +sp.disable_function.function("include").drop() + +# Prevent `system`-related injections +sp.disable_function.function("system").param("command").value_r("[$|;&`\\n\\(\\)\\\\]").drop(); +sp.disable_function.function("shell_exec").param("command").value_r("[$|;&`\\n\\(\\)\\\\]").drop(); +sp.disable_function.function("exec").param("command").value_r("[$|;&`\\n\\(\\)\\\\]").drop(); +sp.disable_function.function("proc_open").param("command").value_r("^gpg ").allow(); +sp.disable_function.function("proc_open").param("command").value_r("[$|;&`\\n\\(\\)\\\\]").drop(); + +# Prevent runtime modification of interesting things +sp.disable_function.function("ini_set").param("option").value("assert.active").drop(); +sp.disable_function.function("ini_set").param("option").value("zend.assertions").drop(); +sp.disable_function.function("ini_set").param("option").value("memory_limit").drop(); +sp.disable_function.function("ini_set").param("option").value("include_path").drop(); +sp.disable_function.function("ini_set").param("option").value("open_basedir").drop(); + +# Detect some backdoors via environment recon +sp.disable_function.function("ini_get").param("option").value("allow_url_fopen").drop(); +sp.disable_function.function("ini_get").param("option").value("open_basedir").drop(); +sp.disable_function.function("ini_get").param("option").value_r("suhosin").drop(); +sp.disable_function.function("function_exists").param("function").value("eval").drop(); +sp.disable_function.function("function_exists").param("function").value("exec").drop(); +sp.disable_function.function("function_exists").param("function").value("system").drop(); +sp.disable_function.function("function_exists").param("function").value("shell_exec").drop(); +sp.disable_function.function("function_exists").param("function").value("proc_open").drop(); +sp.disable_function.function("function_exists").param("function").value("passthru").drop(); +sp.disable_function.function("is_callable").param("value").value("eval").drop(); +sp.disable_function.function("is_callable").param("value").value("exec").drop(); +sp.disable_function.function("is_callable").param("value").value("system").drop(); +sp.disable_function.function("is_callable").param("value").value("shell_exec").drop(); +sp.disable_function.function("is_callable").filename_r("/app/libraries/snappymail/pgp/gpg\.php$").param("value").value("proc_open").allow(); +sp.disable_function.function("is_callable").param("value").value("proc_open").drop(); +sp.disable_function.function("is_callable").param("value").value("passthru").drop(); + +# Ghetto error-based sqli detection +#sp.disable_function.function("mysql_query").ret("FALSE").drop(); +#sp.disable_function.function("mysqli_query").ret("FALSE").drop(); +#sp.disable_function.function("PDO::query").ret("FALSE").drop(); + +# Ensure that certificates are properly verified +sp.disable_function.function("curl_setopt").param("value").value("1").allow(); +sp.disable_function.function("curl_setopt").param("value").value("2").allow(); +# `81` is SSL_VERIFYHOST and `64` SSL_VERIFYPEER +sp.disable_function.function("curl_setopt").param("option").value("64").drop().alias("Please don't turn CURLOPT_SSL_VERIFYCLIENT off."); +sp.disable_function.function("curl_setopt").param("option").value("81").drop().alias("Please don't turn CURLOPT_SSL_VERIFYHOST off."); + +# File upload +sp.disable_function.function("move_uploaded_file").param("to").value_r("\\.ph").drop(); +sp.disable_function.function("move_uploaded_file").param("to").value_r("\\.ht").drop(); + +# Logging lockdown +sp.disable_function.function("ini_set").param("option").value_r("error_log").drop() +sp.disable_function.function("ini_set").param("option").value_r("display_errors").drop() + +sp.auto_cookie_secure.enable(); +sp.cookie.name("roundcube_sessauth").samesite("strict"); +sp.cookie.name("roundcube_sessid").samesite("strict"); +sp.ini_protection.policy_silent_fail(); diff --git a/webmails/start.py b/webmails/start.py index 06b90351..bf4472cd 100755 --- a/webmails/start.py +++ b/webmails/start.py @@ -51,7 +51,9 @@ if not secret_key: print(f"Can't read SECRET_KEY from file: {exc}", file=sys.stderr) exit(2) -context['SECRET_KEY'] = hmac.new(bytearray(secret_key, 'utf-8'), bytearray('ROUNDCUBE_KEY', 'utf-8'), 'sha256').hexdigest() +context['ROUNDCUBE_KEY'] = hmac.new(bytearray(secret_key, 'utf-8'), bytearray('ROUNDCUBE_KEY', 'utf-8'), 'sha256').hexdigest() +context['SNUFFLEPAGUS_KEY'] = hmac.new(bytearray(secret_key, 'utf-8'), bytearray('SNUFFLEPAGUS_KEY', 'utf-8'), 'sha256').hexdigest() +conf.jinja("/etc/snuffleupagus.rules.tpl", context, "/etc/snuffleupagus.rules") # roundcube plugins # (using "dict" because it is ordered and "set" is not) @@ -118,7 +120,7 @@ if os.path.exists("/var/run/nginx.pid"): os.system("nginx -s reload") # clean env -[env.pop(key, None) for key in env.keys() if key == "SECRET_KEY" or key.startswith("ROUNDCUBE_")] +[env.pop(key, None) for key in env.keys() if key == "SECRET_KEY" or key.endswith("_KEY")] # run nginx os.system("php-fpm81")