Securing Craft

There are a number of things that you can do to harden the security of your Craft install.

Do not enable Dev Mode in a production environment #

Dev Mode is meant to help during local development and should not be enabled in a production environment, where it can cause a wide range of security issues.

Place your project’s source folder above your webroot #

The best way to ensure that Craft’s PHP files and other sensitive information are not directly accessible over HTTP traffic is to put your project’s source and runtime folders above your public webroot. Particularly vulnerable directories include:

  • config/ — PHP, JSON, and YAML configuration files can provide an attacker clues about your system’s version and settings.
  • templates/ — Twig files give insight into your content schema, accepted query parameters, permissions, and more.
  • storage/ — Extremely sensitive data, including database backups, live here.
  • vendor/ — Source files can reveal specific versions of installed software and give an attacker information about potential vulnerabilities.
  • Any project-specific source directories for CSS or Javascript
  • Asset filesystem or volume roots that contain uploads that aren’t configured to have URLs and ought not to be publicly accessible.

Additionally, a .env file within the webroot is not necessarily hidden from users—HTTP server misconfiguration can lead to this (and other files beginning with a dot) being served unintentionally.

Our starter project’s directory structure keeps only essential files within the webroot. As long as it is not substantially reconfigured on a public server, it should protect sensitive files reasonably well.

If you must to put your source folder below the webroot, ensure that users cannot directly access the contents of any files/folders within it. Remember that Craft’s source files (and other packages) are only part of the picture! Be mindful of other project files that might leak into the web root, like design files, notes, database backups, etc.

Use our server check tool to identify issues with your directory structure.

Set allowAdminChanges to false in production #

By setting allowAdminChanges to false in production, most critical system settings (including sweeping schema and permissions updates) are locked. The Settings and Plugin Store sections are hidden, Craft edition and Craft/plugin versions are locked, and project config becomes read-only.

Setting this to false makes production a more predictable and stable environment, preventing potential security concerns should an administrator’s account be compromised.

The @web alias and site URLs #

Avoid using the @web alias for any site’s Base URL. In multi-site installations (and sites that use a different domain for the control panel), this can prevent Craft from detecting the correct site or generating accurate URLs.

Each site should get its Base URL directly from an environment variable, such as $PRIMARY_SITE_URL or $SECONDARY_SITE_URL.

This also applies to the baseCpUrl config setting.

Install an SSL certificate and enforce HTTPS #

If you don’t have an SSL certificate, talk to your host about installing one. Once you have one in place, you should—at a minimum—enforce HTTPs for all control panel requests. Ideally, front-end requests should also use SSL—especially when sensitive information like usernames and passwords are transmitted to (or from) the server.

SSL certificates are freely available from LetsEncrypt, and many hosts (including Craft Cloud) automatically manage certificates and upgrade connections at the edge.

Don’t trust front-end user input #

As Content

If you accept front-end user input on your site, assume it is malicious.

  • Always escape and/or sanitize it before outputting it anywhere.
  • The same goes for user-supplied files that are exposed to other visitors (images or otherwise).
  • Twig has a set of escaping tools for different contexts.
  • Never output user-provided content with the raw filter, incorporate it into object templates or render it as Twig.

In Queries

Consider how user input is incorporated into element queries, as well: when allowing users to search or filter, it’s important to check value types before passing them off to a query. Element queries are parameterized to prevent SQL injection attacks, but it’s still possible to cleverly manipulate parameters with some knowledge of how they work:

{% set authorPosts = craft.entries()
  .authorId(craft.app.request.getParam('authorId'))
  .all() %}

In this case, passing ?authorId=123 would do exactly what you expect—show posts authored by user ID 123. As the query is being prepared, a number of valid authorId structures can produce very different results. Suppose the request was constructed with a query string like ?authorId=not%20123 or ?authorId[]=not&authorId[]=123—we would end up passing a string like not 123 or an array like ['not', 123] to the element query, and fetching posts authored by all users except the one with ID 123! Depending on the kind of data your site holds (including that of users or customers), misconfiguration can result in exfiltration of private information.

You can narrow the acceptable parameter types by using Craft’s built-in value-type tests:

{% set authorId = craft.app.request.getParam('authorId') %}
{% set authorPostsQuery = craft.entries() %}

{% if authorId is numeric %}
  {% do authorPosts.authorId(authorId) %}
{% endif %}

{% set authorPosts = authorPostsQuery.all() %}

Audit the allowedFileExtensions config setting #

You should review the default allowedFileExtensions config setting and remove any extensions you know will not be uploaded to your site—either via the control panel or front-end. Craft does its best to sanitize things like images and SVG uploads to strip out any maliciously embedded code by default. Some files—PDFs, for example—PHP can’t sanitize. If your site doesn’t need to allow PDF uploads, you can remove the extension from this config setting.

Purify HTML in custom fields #

If you’re using the Redactor or CKEditor plugins, leave the Purify HTML? field setting enabled. When saving data to one of these fields, Craft runs it through HTML Purifier to strip JavaScript and other potentially malicious attributes from the markup, preventing potential XSS vulnerabilities.

While not an alternative (you should purify HTML before storage, and escape it before output), you can purify HTML stored in other fields with the purify filter.

Enable CSRF protection #

Cross-Site Request Forgery (CSRF) protection is enabled by default, for all POST requests. See Enabling CSRF Protection on how to take advantage of it in your public-facing forms.

Make sure you’re running a recent version of PHP #

Craft 5 requires PHP 8.2 and is rigorously tested against each new version. You can see which PHP versions are under active development and are supported by the core team. Use the latest PHP version your hosting provider supports, and stay on top of security updates.

Keep Craft and all of your installed plugins updated #

All software has bugs, and older software has more bugs. Some bugs are security vulnerabilities. Keep your software updated to have fewer security vulnerabilities.

Check your permissions #

The required file permissions vary widely depending on your development and hosting environments. Always use the least permissive combination of settings to allow your application to function. For a LAMP system, this typically means granting user and group read-write access to project files and uploads. There is never a reason to enable world read/write permissions on any Craft or system files! This community-maintained shell script can help you configure and set permissions on a per-project basis on *nix based systems.

Within Craft itself, user and group permissions dictate what your front-end and control panel users can do. Ensure you trust anyone providing Twig anywhere in the system—whether that be system email messages or element URI settings. Craft’s entire API is available via Twig and can be used to do malicious things, like elevating one’s own permissions!

Review config settings on a per-site basis #

Many Craft configuration settings have security implications that should be reviewed per-site. Craft ships with reasonable defaults, but depending on the site requirements, they can be adjusted to be more secure.

Set security headers for the front-end site #

Your server should be configured to send these security headers:

As of Craft 5.3, you can provide additional headers and configure a CORS policy via application configuration.

OWASP maintains a comprehensive list of security-related HTTP headers. Run your domain through a tool like securityheaders.io to check for recommended header settings.

X-Frame-Options could be overly-restrictive for multi-domain installs using live preview. See Using Live Preview With An Alternate Control Panel Domain for recommendations.

Other Things to Consider #

While less about actual security and more about security-through-obscurity, consider some combination of the following if you’re particularly paranoid:

Change your cpTrigger #

The cpTrigger config setting determines the URI segment that points to the control panel. Changing it to a custom word will make it marginally more difficult for visitors to find your control panel’s login page.

Remove the X-Powered-By header #

If you don’t want tools like BuiltWith and Wappalyzer to know Craft powers your site, you can suppress the X-Powered-By: Craft CMS by setting sendPoweredByHeader to false.

More Resources #

Continue your learning with these official and community resources:

Applies to Craft CMS 5, Craft CMS 4, and Craft CMS 3.