Block PHP execution in modules/ with .htaccess (PrestaShop 1.7 and 8)
In PrestaShop 8 and earlier, any .php file inside modules/ can be requested by URL. PrestaShop 9 denies it out of the box. We explain how to apply that protection to your store, how to allow exceptions and which other folders are worth locking down.
One of the least talked-about security improvements in PrestaShop 9 lives in a file a dozen lines long: the .htaccess that the modules/ folder ships with. With it, Apache returns a 403 to any direct request for a module's .php, before PHP even starts.
In PrestaShop 1.7 and 8 that file does not exist. Any .php inside modules/, whether it belongs to an installed module, an uninstalled one or a backdoor, can be requested by URL and is executed. This article explains how to bring that protection to your store without breaking anything.
Why it matters
Almost everything a modern module does goes through the PrestaShop dispatcher: front controllers are requested as index.php?fc=module&module=nombre&controller=accion (or their friendly URL), and admin controllers go through the back office. None of those routes needs to request a .php in modules/ directly.
What does get requested directly is:
- Legacy files from old modules:
ajax.php,validation.php, cron or import scripts that bootstrap PrestaShop on their own. Some have known vulnerabilities. - Files from orphaned modules, which stay on disk even though the module has been uninstalled.
- Backdoors left in a module's folder, often with a name that looks perfectly ordinary.
Denying direct .php requests closes all three at once. What stays open is only what goes through the dispatcher, which is much easier to review.
What exactly PrestaShop 9 does
The stock file uses a FilesMatch that denies by extension, with both the Apache 2.2 and Apache 2.4 directives. It denies .php and also other files that should never be served from there: .log, .txt, .lock, .json, .yml, .yaml, .tpl, .twig, each module's config.xml and its per-language variants. Module CSS, JavaScript and images are still served as normal.
How to apply it in PrestaShop 1.7 and 8
- List what is requested directly. Before locking down, go through the server access logs for the last few weeks looking for requests to
/modules/that end in.phpand return 200. That is the list of what you could break. - Classify each one. Payment gateway returns, cron jobs scheduled in the panel, Ajax calls from some old module. Each one is either a justified exception or something worth replacing.
- Create the file
modules/.htaccessdenying.phpand the configuration extensions, modelled on the PrestaShop 9 one. On Apache 2.4 the essential rule is<FilesMatch "\.(php|phtml|phar|php[0-9])$"> Require all denied </FilesMatch>. Includingphtml,pharand the numbered variants stops a file with another extension the server also executes from slipping through. - Add the exceptions in each module's folder, not in the general one. For example, in
modules/mipasarela/.htaccess:<Files "notificacion.php"> Require all granted </Files>. That way the exception only applies to that file of that module, and not to any file with the same name in another folder. - Test with real requests. Request any module
.php(it should return 403), request the excepted one (it should respond) and place a test order with each payment method. - Check the cron jobs. If a cron in the panel called
modules/x/cron.php, it will now fail silently. Switch it to the module's controller URL if it has one, or leave it as an exception.
Limitations worth knowing
- It does not protect front controllers. A
controllers/front/is served throughindex.php, so the.htaccessinmodules/does not cover it. If an installed module has a vulnerable controller, it is still vulnerable. There is no shortcut for that: update or remove the module. - Nginx ignores
.htaccessfiles. If your server is pure Nginx, the denial is done with alocationblock in the server configuration, which is normally your hosting provider's job. - It depends on the server reading
.htaccessfiles (theAllowOverridedirective). Check it by requesting a.phpafter creating the file; do not take it for granted. - An attacker with write access can delete it. If someone can write to
modules/, they can remove the.htaccess. There is no legitimate reason for it to disappear, so monitoring its presence is a useful alarm in itself.
The other folders: upload/, img/, var/
upload/ and download/
This is where the files customers upload for customisable products and the downloadable products go. PrestaShop serves them through a controller, not by direct access. In PrestaShop 9, download/ denies everything and upload/ denies files with the internal name PrestaShop gives them. Neither should ever contain a .php, so adding a PHP denial to them is safe. If, on top of that, your store serves nothing from upload/ by direct URL, you can deny everything, but check first.
img/
In PrestaShop 9 it denies everything except image, video, PDF and icon extensions. In earlier versions, check what your img/.htaccess contains: it may have its own rules, so add the PHP denial at the end, without replacing what is there. And when you regenerate the root .htaccess from the back office, check afterwards that your blocks are still where you left them.
var/ and cache/
Since 1.7, var/ comes with a full denial. Check that the file is still there: an accessible var/logs leaks paths, errors and sometimes customer data. In 1.6 stores, the equivalent folder is cache/, and it must not serve PHP.
The root
Also check that there are no backups (.zip, .sql, .tar.gz) or hosting error logs accessible in the root. A downloadable database dump is worse than any shell.
How to solve it in PrestaShop
RKR Security Alert has a data-folder hardening panel that checks that the .htaccess in upload/, download/, img/ and other folders not only exists but actually blocks execution, and alerts you if something executable appears where there should only be data. It can write the denial block for you, appending it to the end of the existing file and keeping a copy. For modules/, it checks which extensions the .htaccess really denies and cross-references that with the entry points it finds, and every scanner finding says whether it can be reached by URL or is closed off. If you would rather not touch it yourself, with a Rekire support hours pack we review the logs, define the exceptions needed and test it with real orders.