diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index 4d10e4ca..a7eb4e85 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -394,7 +394,7 @@ jobs: strategy: fail-fast: false matrix: - target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "postfix", "dovecot", "unbound", "nginx"] + target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "oletools", "postfix", "dovecot", "unbound", "nginx"] steps: - uses: actions/checkout@v3 - name: Retrieve global variables @@ -439,7 +439,7 @@ jobs: strategy: fail-fast: false matrix: - target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "postfix", "dovecot", "unbound", "nginx"] + target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "oletools", "postfix", "dovecot", "unbound", "nginx"] steps: - uses: actions/checkout@v3 - name: Retrieve global variables diff --git a/README.md b/README.md index b6ed040b..54045d28 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Main features include: - **Web access**, multiple Webmails and administration interface - **User features**, aliases, auto-reply, auto-forward, fetched accounts - **Admin features**, global admins, announcements, per-domain delegation, quotas -- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner, [Snuffleupagus](https://github.com/jvoisin/snuffleupagus/) +- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner, [Snuffleupagus](https://github.com/jvoisin/snuffleupagus/), block malicious attachments - **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing - **Freedom**, all FOSS components, no tracker included diff --git a/core/base/Dockerfile b/core/base/Dockerfile index bb18cb65..82c8cb9b 100644 --- a/core/base/Dockerfile +++ b/core/base/Dockerfile @@ -27,6 +27,7 @@ ENV \ FRONT_ADDRESS="front" \ SMTP_ADDRESS="smtp" \ IMAP_ADDRESS="imap" \ + OLETOOLS_ADDRESS="oletools" \ REDIS_ADDRESS="redis" \ ANTIVIRUS_ADDRESS="antivirus" \ ANTISPAM_ADDRESS="antispam" \ diff --git a/core/base/requirements-dev.txt b/core/base/requirements-dev.txt index 52874a86..5a9df4b1 100644 --- a/core/base/requirements-dev.txt +++ b/core/base/requirements-dev.txt @@ -45,6 +45,10 @@ watchdog # core/postfix postfix-mta-sts-resolver +# core/oletools +python-magic +oletools + # optional/fetchmail requests diff --git a/core/base/requirements-prod.txt b/core/base/requirements-prod.txt index 8b861cd5..0da5219b 100644 --- a/core/base/requirements-prod.txt +++ b/core/base/requirements-prod.txt @@ -41,6 +41,7 @@ MarkupSafe==2.1.1 marshmallow==3.18.0 marshmallow-sqlalchemy==0.28.1 multidict==6.0.2 +oletools==0.60.1 mysql-connector-python==8.0.29 packaging==21.3 passlib==1.7.4 @@ -51,7 +52,10 @@ psycopg2-binary==2.9.5 pycares==4.2.2 pycparser==2.21 Pygments==2.13.0 -pyparsing==3.0.9 +pyOpenSSL==22.1.0 +pyparsing==2.4.7 +python-dateutil==2.8.2 +python-magic==0.4.27 python-dateutil==2.8.2 pytz==2022.6 PyYAML==6.0 diff --git a/core/oletools/Dockerfile b/core/oletools/Dockerfile new file mode 100644 index 00000000..8bb98cd9 --- /dev/null +++ b/core/oletools/Dockerfile @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile-upstream:1.4.3 + +# oletools image +FROM base + +ARG VERSION=local +LABEL version=$VERSION + +RUN set -euxo pipefail \ + ; apk add --no-cache netcat-openbsd libmagic libffi \ + ; curl -sLo olefy.py https://raw.githubusercontent.com/HeinleinSupport/olefy/f8aac6cc55283886d153e89c8f27fae66b1c24e2/olefy.py \ + ; chmod 755 olefy.py + +RUN echo $VERSION >/version + +HEALTHCHECK --start-period=60s CMD echo PING|nc -q1 127.0.0.1 11343|grep "PONG" +EXPOSE 11343/tcp + +USER nobody:nobody + +ENV \ + OLEFY_BINDADDRESS="0.0.0.0" \ + OLEFY_BINDPORT="11343" \ + OLEFY_OLEVBA_PATH="/app/venv/bin/olevba" \ + OLEFY_PYTHON_PATH="/app/venv/bin/python3" \ + OLEFY_TMPDIR="/dev/shm/" \ + OLEFY_MINLENGTH="300" \ + OLEFY_DEL_TMP="1" \ + OLEFY_DEL_TMP_FAILED="1" + +CMD /app/olefy.py diff --git a/core/rspamd/conf/composites.conf b/core/rspamd/conf/composites.conf new file mode 100644 index 00000000..68e03073 --- /dev/null +++ b/core/rspamd/conf/composites.conf @@ -0,0 +1,14 @@ +{% if SCAN_MACROS == 'True' %} +OLETOOLS_MACRO_MRAPTOR { + expression = "(OLETOOLS_A & OLETOOLS_W) | (OLETOOLS_A & OLETOOLS_X) | (OLETOOLS_W & OLETOOLS_X)"; + message = "Rejected (malicious macro - mraptor)"; + policy = "leave"; + score = 20.0; +} +OLETOOLS_MACRO_SUSPICIOUS { + expression = "OLETOOLS_FLAG | OLETOOLS_VBASTOMP | OLETOOLS_A"; + message = "Rejected (malicious macro)"; + policy = "leave"; + score = 20.0; +} +{% endif %} diff --git a/core/rspamd/conf/external_services.conf b/core/rspamd/conf/external_services.conf new file mode 100644 index 00000000..d299ed89 --- /dev/null +++ b/core/rspamd/conf/external_services.conf @@ -0,0 +1,64 @@ +{% if SCAN_MACROS == 'True' %} +oletools { + # default olefy settings + servers = "{{ OLETOOLS_ADDRESS }}:11343" + + # needs to be set explicitly for Rspamd < 1.9.5 + scan_mime_parts = true; + extended = true; + max_size = 3145728; + timeout = 20.0; + retransmits = 1; + + patterns { + OLETOOLS_MACRO_FOUND= '^.....M..$'; + OLETOOLS_AUTOEXEC = '^A....M..$'; + OLETOOLS_FLAG = '^.....MS.$'; + OLETOOLS_VBASTOMP = '^VBA Stomping$'; +# see https://github.com/decalage2/oletools/blob/master/oletools/mraptor.py + OLETOOLS_A = '(?i)\b(?:Auto(?:Exec|_?Open|_?Close|Exit|New)|Document(?:_?Open|_Close|_?BeforeClose|Change|_New)|NewDocument|Workbook(?:_Open|_Activate|_Close|_BeforeClose)|\w+_(?:Painted|Painting|GotFocus|LostFocus|MouseHover|Layout|Click|Change|Resize|BeforeNavigate2|BeforeScriptExecute|DocumentComplete|DownloadBegin|DownloadComplete|FileDownload|NavigateComplete2|NavigateError|ProgressChange|PropertyChange|SetSecureLockIcon|StatusTextChange|TitleChange|MouseMove|MouseEnter|MouseLeave|OnConnecting))\b|Auto_Ope\b'; + OLETOOLS_W = '(?i)\b(?:FileCopy|CopyFile|Kill|CreateTextFile|VirtualAlloc|RtlMoveMemory|URLDownloadToFileA?|AltStartupPath|WriteProcessMemory|ADODB\.Stream|WriteText|SaveToFile|SaveAs|SaveAsRTF|FileSaveAs|MkDir|RmDir|SaveSetting|SetAttr)\b|(?:\bOpen\b[^\n]+\b(?:Write|Append|Binary|Output|Random)\b)'; + OLETOOLS_X = '(?i)\b(?:Shell|CreateObject|GetObject|SendKeys|RUN|CALL|MacScript|FollowHyperlink|CreateThread|ShellExecuteA?|ExecuteExcel4Macro|EXEC|REGISTER|SetTimer)\b|(?:\bDeclare\b[^\n]+\bLib\b)'; + } + + # mime-part regex matching in content-type or filename + mime_parts_filter_regex { + #UNKNOWN = "application\/octet-stream"; + DOC2 = "application\/msword"; + DOC3 = "application\/vnd\.ms-word.*"; + XLS = "application\/vnd\.ms-excel.*"; + PPT = "application\/vnd\.ms-powerpoint.*"; + GENERIC = "application\/vnd\.openxmlformats-officedocument.*"; + } + # mime-part filename extension matching (no regex) + mime_parts_filter_ext { + doc = "doc"; + dot = "dot"; + docx = "docx"; + dotx = "dotx"; + docm = "docm"; + dotm = "dotm"; + xls = "xls"; + xlt = "xlt"; + xla = "xla"; + xlsx = "xlsx"; + xltx = "xltx"; + xlsm = "xlsm"; + xltm = "xltm"; + xlam = "xlam"; + xlsb = "xlsb"; + ppt = "ppt"; + pot = "pot"; + pps = "pps"; + ppa = "ppa"; + pptx = "pptx"; + potx = "potx"; + ppsx = "ppsx"; + ppam = "ppam"; + pptm = "pptm"; + potm = "potm"; + ppsm = "ppsm"; + slk = "slk"; + } +} +{% endif %} diff --git a/core/rspamd/conf/external_services_group.conf b/core/rspamd/conf/external_services_group.conf new file mode 100644 index 00000000..0b44b229 --- /dev/null +++ b/core/rspamd/conf/external_services_group.conf @@ -0,0 +1,40 @@ +{% if SCAN_MACROS == 'True' %} +# local.d/external_services_group.conf + +description = "Oletools content rules"; +symbols = { + "OLETOOLS" { + weight = 1.0; + description = "OLETOOLS found a Macro"; + one_shot = true; + }, + "OLETOOLS_MACRO_FOUND" { + weight = 0.0; + one_shot = true; + }, + "OLETOOLS_AUTOEXEC" { + weight = 0.0; + one_shot = true; + }, + "OLETOOLS_FLAG" { + weight = 0.0; + one_shot = true; + }, + "OLETOOLS_VBASTOMP" { + weight = 0.0; + one_shot = true; + }, + "OLETOOLS_A" { + weight = 0.0; + one_shot = true; + }, + "OLETOOLS_W" { + weight = 0.0; + one_shot = true; + }, + "OLETOOLS_X" { + weight = 0.0; + one_shot = true; + }, +} +{% endif %} diff --git a/core/rspamd/conf/forbidden_file_extension.map b/core/rspamd/conf/forbidden_file_extension.map new file mode 100644 index 00000000..bc584455 --- /dev/null +++ b/core/rspamd/conf/forbidden_file_extension.map @@ -0,0 +1,68 @@ +ace +ade +adp +apk +appx +appxbundle +arj +bat +bin +cab +chm +class +cmd +com +cpl +diagcab +diagcfg +diagpack +dll +ex +ex_ +exe +hlp +hta +img +ins +iso +isp +jar +jnlp +js +jse +lib +lnk +lzh +mde +msc +msi +msix +msixbundle +msp +mst +msu +nsh +ocx +ovl +pif +ps1 +r01 +r14 +r18 +r25 +scr +sct +shb +shs +sys +vb +vbe +vbs +vbscript +vdl +vhd +vxd +wsc +wsf +wsh +xll diff --git a/core/rspamd/conf/multimap.conf b/core/rspamd/conf/multimap.conf index dd25c08e..7ee6669e 100644 --- a/core/rspamd/conf/multimap.conf +++ b/core/rspamd/conf/multimap.conf @@ -9,3 +9,16 @@ IS_LOCAL_DOMAIN_E { selector = "from('smtp'):domain"; map = "http://{{ ADMIN_ADDRESS }}/internal/rspamd/local_domains"; } + +FORBIDDEN_FILE_EXTENSION { + type = "filename"; + filter = "extension"; + map = [ + "/etc/rspamd/local.d/forbidden_file_extension.map", + ]; + prefilter = true; + action = "reject"; + symbol = "FORBIDDEN_FILE_EXTENSION"; + description = "List of forbidden file extensions"; + message = "Forbidden attachment extension"; +} diff --git a/docs/antispam.rst b/docs/antispam.rst index 3873be64..fb97e66a 100644 --- a/docs/antispam.rst +++ b/docs/antispam.rst @@ -155,3 +155,31 @@ For more information on using the multimap filter see the official `multimap doc .. _`1438`: https://github.com/Mailu/Mailu/issues/1438 .. _`1167`: https://github.com/Mailu/Mailu/issues/1167 .. _`1566`: https://github.com/Mailu/Mailu/issues/1566 + +Can I change the list of authorized file attachments? +----------------------------------------------------- + +Mailu rejects emails with file attachements it deems to be "executable" or otherwise dangerous. If you would like to tweak the block list, you can do so using the following commands: + + .. code-block:: bash + + docker-compose exec antispam cat /etc/rspamd/local.d/forbidden_file_extension.map > overrides/rspamd/forbidden_file_extension.map + docker-compose restart antispam + +Now the file `overrides/rspamd/forbidden_file_extension.map` can be edited, to make changes to the forbidden file extensions list. +For the changes to take effect, rspamd must be restarted. + +Mailu rejects emails with documents attached containing some macros. How can I fix it? +-------------------------------------------------------------------------------------- + +If configured to do so, Mailu uses a lightweight tool called `mraptor from oletools`_ to scan documents containing macros. By default only macros deemed potentially harmful are blocked, but there may be false positives. If you want to change the default behaviour, you may need to override the ``/etc/rspamd/local.d/composites.conf`` file in the antispam container. The following commands may be useful: + + .. code-block:: bash + + docker-compose exec antispam cat /etc/rspamd/local.d/composites.conf > overrides/rspamd/composites.conf + docker-compose restart antispam + +Now the file `overrides/rspamd/composites.conf` can be edited, to override the mraptor configuration in rspamd. +For the changes to take effect, rspamd must be restarted. + +.. _`mraptor from oletools`: https://github.com/decalage2/oletools/wiki/mraptor diff --git a/docs/configuration.rst b/docs/configuration.rst index f4510626..5ecfa347 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -100,6 +100,12 @@ by setting ``INBOUND_TLS_ENFORCE`` to ``True``. Please note that this is forbidd internet facing hosts according to e.g. `RFC 3207`_ , because this prevents MTAs without STARTTLS support or e.g. mismatching TLS versions to deliver emails to Mailu. +The ``SCAN_MACROS`` (default: True) setting controls whether Mailu will endavour +to reject emails containing documents with malicious macros. Under the hood, it uses +`mraptor from oletools`_ to determine whether a macro is malicious or not. + +.. _`mraptor from oletools`: https://github.com/decalage2/oletools/wiki/mraptor + .. _`RFC 3207`: https://tools.ietf.org/html/rfc3207 .. _fetchmail: diff --git a/docs/index.rst b/docs/index.rst index 0b37cf43..b73bf529 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,7 +28,7 @@ Main features include: - **Web access**, multiple Webmails and administration interface - **User features**, aliases, auto-reply, auto-forward, fetched accounts - **Admin features**, global admins, announcements, per-domain delegation, quotas -- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner, Snuffleupagus +- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner, [Snuffleupagus](https://github.com/jvoisin/snuffleupagus/), block malicious attachments - **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing - **Freedom**, all FOSS components, no tracker included diff --git a/setup/flavors/compose/docker-compose.yml b/setup/flavors/compose/docker-compose.yml index cd470098..6eb4c409 100644 --- a/setup/flavors/compose/docker-compose.yml +++ b/setup/flavors/compose/docker-compose.yml @@ -103,16 +103,40 @@ services: - {{ dns }} {% endif %} +{% if oletools_enabled %} + oletools: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}oletools:${MAILU_VERSION:-{{ version }}} + hostname: oletools + restart: always + networks: + - noinet + depends_on: + {% if resolver_enabled %} + - resolver + dns: + - {{ dns }} + {% endif %} +{% endif %} + antispam: image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-{{ version }}} hostname: antispam restart: always env_file: {{ env }} +{% if oletools_enabled %} + networks: + - default + - noinet +{% endif %} volumes: - "{{ root }}/filter:/var/lib/rspamd" - "{{ root }}/overrides/rspamd:/etc/rspamd/override.d:ro" depends_on: - front + - redis + {% if oletools_enabled %} + - oletools + {% endif %} {% if antivirus_enabled %} - antivirus {% endif %} @@ -202,3 +226,8 @@ networks: {% if ipv6_enabled %} - subnet: {{ subnet6 }} {% endif %} +{% if oletools_enabled %} + noinet: + driver: bridge + internal: true +{% endif %} diff --git a/setup/flavors/compose/mailu.env b/setup/flavors/compose/mailu.env index cc99912e..980788ce 100644 --- a/setup/flavors/compose/mailu.env +++ b/setup/flavors/compose/mailu.env @@ -58,6 +58,9 @@ WEBDAV={{ webdav_enabled or 'none' }} # Antivirus solution (value: clamav, none) ANTIVIRUS={{ antivirus_enabled or 'none' }} +# Scan Macros solution (value: true, false) +SCAN_MACROS={{ oletools_enabled or 'false' }} + ################################### # Mail settings ################################### diff --git a/setup/templates/steps/compose/02_services.html b/setup/templates/steps/compose/02_services.html index b6964bdf..2311e4a3 100644 --- a/setup/templates/steps/compose/02_services.html +++ b/setup/templates/steps/compose/02_services.html @@ -55,6 +55,15 @@ the security implications caused by such an increase of attack surface.
Fetchmail allows users to retrieve mail from an external mail-server via IMAP/POP3 and puts it in their inbox. +