Lock the uploads folder.
Then check the public doors.
The nginx rule for uploads/omnithread, the Apache rules the module writes itself, the public addresses the module opens, and the settings worth a second look before you go live.
01Why uploads/omnithread must not be reachable
uploads/omnithread/ holds the encryption key file (omnithread-key.php) and, by default, the stored attachments (files/). The module already protects it three ways: the key file is PHP that prints nothing, attachments have random names without extensions and are only served through the module after a permission check, and each folder gets an Apache deny rule and an empty index.html. On nginx the .htaccess files have no effect, so add the rule below.
02nginx: deny uploads/omnithread
- Open the server block. Find your CRM's nginx configuration (for example in
/etc/nginx/sites-available/; your host's panel may offer "custom nginx directives" instead). - Add the rule. Put it inside the
server { … }block. The^~makes nginx use this block before any\.php$rule, so files in the folder are never run or served.
If Perfex is installed in a subfolder, include it:# Omnithread: no direct web access to the key file and stored attachments location ^~ /uploads/omnithread/ { deny all; }location ^~ /crm/uploads/omnithread/ { … }. - Test and reload.
sudo nginx -t sudo systemctl reload nginx - Check from outside. This must answer
403(or404), never200:curl -I https://your-crm.example.com/uploads/omnithread/omnithread-key.php
If you moved attachments to a Storage folder outside the web root, the rule still protects the key file.
03Apache: the rules the module writes
The module writes this .htaccess into uploads/omnithread/ when it creates the key, and into the attachment folder when it first stores a file:
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
Apache reads .htaccess files only where AllowOverride permits it. Run the same curl -I check as above. If it answers 200, add the rule to your virtual host instead and reload Apache:
<Directory "/path/to/your/crm/uploads/omnithread">
Require all denied
</Directory>
04The public addresses the module opens
| Address | For | Protected by |
|---|---|---|
/omnithread/hook/in/<id>-<key> | Messages and statuses from Meta, Telegram and Twilio | The secret key in the address, the provider's signature (Meta app secret, Twilio auth token) or secret header (Telegram), bodies over 1 MB refused, unknown addresses not logged |
/omnithread/widget/… | The website chat | Allowed websites, a random visitor token (only its hash is stored), rate limits, size limits |
/omnithread/rate/<token> | The satisfaction page | A single-use token that expires; only its hash is stored |
/omnithread/portal | Messages in the client portal | The Perfex client login; each contact sees only their own conversations |
These paths are exempt from Perfex's CSRF check because providers and visitors post to them without a CRM session; each one checks that the module is active and verifies its own key, signature or token. Every admin page needs a Perfex staff login and the matching Omnithread permission, and every change needs a POST with the module's token.
05Before you go live
- Your CRM runs on
https://only. - The
uploads/omnithread/check above answers 403 or 404. - The key file and the database are in your backups, and the backups are protected.
- Every website chat has its allowed websites filled in (why).
- Webhook addresses are pasted only at the providers. If one leaks, press Make a new address and paste the new one (for Telegram, run Test connection again).
- Each Meta channel has the App secret of the app that sends its webhooks; with a wrong secret every request is refused.
- Check the server certificate stays on for every mailbox.
- Allow mail servers on the local network stays off unless your mail server is on your private network.
- Staff have only the Omnithread permissions they need; new staff work under reply approval.
- You decided whether you need Keep the content of incoming webhooks for 14 days and an attachment retention period (Privacy).