Craft CMS is secured mostly by settings you control: prompt patching, a protected security key, tight PHP configuration and careful plugin and permission choices. Craft is a PHP content management system built on the Yii framework, popular with agencies for its flexible content modelling, its GraphQL API and its plugin ecosystem. That flexibility moves risk into places teams rarely review: the securityKey in .env, the permissions granted to editors, GraphQL tokens handed to front-end builds, and third-party plugins that run with the full rights of the application. This guide covers security updates, the security key, admin and editor permissions, API tokens, logging and a production hardening baseline with real setting names. It is for developers, agencies and security teams running Craft 4 and 5, checked against Craft's own documentation and GitHub advisories in November 2026.
This is an independent guide from NHI Mgmt Group, based on Craft CMS's public documentation as of November 2026. It is not sponsored or reviewed by Craft CMS. Product features change, so confirm settings against Craft CMS's current documentation.
Key takeaways
- Patch speed is the main control. Apply Craft's security updates promptly on every release line you run.
- The security key is a credential, not a setting. On an unpatched site, anyone holding your
securityKeymay be able to run code. Keep it in an environment variable, never in Git, and rotate it after any exposure. - PHP configuration matters to Craft. Keep
register_argc_argvdisabled on web servers; a correctphp.inicloses some issues before a patch exists. - Several editor permissions are effectively admin.
administrateUsers,assignUserPermissions,impersonateUsersandutility:system-messagesall allow escalation; grant them like admin rights. - Production should be read-only for configuration. Set
allowAdminChangesto false and turn offdevMode, so schema, plugins and permissions change only through deployments of project config.
Security updates and advisories
Craft publishes security advisories on its GitHub security advisories page. Apply Craft's security updates promptly on every release line you run, and include the Yii framework and Composer dependencies in your patch scope.
NHI Mgmt Group recommendation: subscribe to the advisories feed rather than waiting for the control panel's update badge, and keep register_argc_argv disabled in the web server's php.ini.
Two practical points follow. First, internet-facing Craft sites need an emergency patch path measured in hours, with a staging site that mirrors production so a Composer update can be tested quickly. Second, after a security update is found missing on a public site, treat the server as possibly compromised: look for unexpected PHP files under the web root and in storage/, new admin users and changed templates. The PHP web shells guide covers the hunt.
How identity and access work in Craft CMS
Craft has one user model for the control panel and, optionally, the front end. Access is decided by three layers: the admin flag, user group permissions (Craft Pro; Craft Team has a single group) and per-user permissions. Permissions from several groups are additive, so removing a user from one group does not remove a right granted by another. Review effective permissions, not group membership alone.
- Authentication options: passwords, time-based one-time passwords (TOTP) with recovery codes, and passkeys added by each user from the account screen. Requiring two-step verification is set per user group under Settings, Users, Security; from Craft 5.6 the requirement applies to all matching users, not only control panel users. See the Passkey Rollout Guide.
- Lockout and throttling:
maxInvalidLoginsdefaults to 5 attempts before lockout andcooldownDurationto 300 seconds. From 5.12.0,defaultRateLimitallows 10 requests per minute per IP on rate-limited actions before HTTP 429; behind a proxy setipHeadersandtrustedHostsor every user shares the proxy's address. - Sessions:
userSessionDurationdefaults to 3600 seconds of inactivity, andrequireMatchingUserAgentForSessionis on by default. Session cookies are protected with the security key, so rotating it signs everyone out. - Elevated sessions: sensitive actions such as changing permissions, removing authentication methods or editing GraphQL schemas ask for the password again within
elevatedSessionDuration(300 seconds by default). Setting it to 0 disables the protection; do not. - Enumeration:
preventUserEnumerationis false by default. Set it to true so the forgotten password form always reports success. - The development switch:
disable2fa(orCRAFT_DISABLE_2FA=true) turns off two-step checks globally. It belongs in local development only; alert if it appears in a production environment file.
The security key
The securityKey general setting is a private key Craft uses to hash and encrypt data, including session cookies and other signed values. Its default is empty, and it is generated with php craft setup/security-key, which writes it to .env as CRAFT_SECURITY_KEY in standard installs. Because a known key can lead to code execution on an unpatched site, treat it like a database password:
- Keep it out of version control: read it from the environment only. Scan repositories and old commits for it; a key in a public theme repository or a leaked
.envis exposed for good. See the Secrets Management Guide. - Stop the web server from serving dot-files: Craft's guidance is to keep
.envand other dot-files unreachable and to placeconfig/,templates/,storage/andvendor/above the public web root. - Use a unique key per environment: staging and production must not share a key, so a staging leak does not open production.
- Rotate on exposure: generate a new key, deploy it, and accept that users are signed out and any data encrypted with the old key becomes unreadable. Test the effect on plugins that store encrypted settings before you need to do it in an incident.
- Rotate the neighbours too: a leaked
.envusually also holds database credentials, mail credentials and API keys. Rotate them all.
Admin roles and privileged access
Admins can do everything, including actions with no explicit permission, and only an existing admin can grant admin. Craft's own documentation recommends limiting admin to a few key people and using granular permissions for everyone else. The table lists permissions that deserve the same care as admin.
| Permission handle | What it allows | Why it is sensitive |
|---|---|---|
administrateUsers | Change emails, send password resets, set passwords, deactivate users | Craft warns it can be used to take over other admins' accounts and that holders can elevate their own permissions |
assignUserPermissions, assignUserGroup:[UID] | Grant permissions or group membership | Self-escalation path if combined with edit rights on own account |
impersonateUsers | Act as another user temporarily | Actions appear to come from the impersonated user, which weakens accountability |
utility:system-messages | Edit system email templates | Twig in system messages can enable permission escalation; trusted admins and developers only |
utility:db-backup, utility:find-replace, utility:migrations | Download the database, rewrite content in bulk, run migrations | A database backup holds password hashes and every user's data |
performUpdates | Update Craft and plugins from the control panel | Production should not change code outside the deployment pipeline |
- Separate control panel access: Craft supports serving the control panel from an alternate domain or subdomain. That lets you restrict it by IP allowlist, VPN or an identity-aware proxy, which is a real control; changing
cpTriggerfromadminis obscurity only. - Lock production: with
allowAdminChangesfalse, the Settings section is hidden, project config and the Plugin Store become read-only, andallowUpdatesis disabled automatically. Schema, plugin and permission changes then arrive only through reviewed project config in Git. - Keep agency access temporary: agencies often leave named admin accounts behind after launch. Review admins quarterly and deactivate leavers the same day.
Integrations, API tokens and service accounts
Craft's machine identities are GraphQL tokens, user-scoped OAuth and personal access tokens, plugin credentials and the queue runner. Each is a non-human identity with an owner, a scope and a lifetime to manage. See the GraphQL Security Guide for API-level controls.
| Identity | How it authenticates | Key controls |
|---|---|---|
| Public GraphQL schema | No token: requests without a token use it, if enabled | Disabled by default. Enable only for content that is genuinely public; never include mutations or user data |
| Private schema token | Random 32-character string sent as Authorization: Bearer | One token per consumer; no stated expiry, so rotate on a schedule by regenerating (which revokes old clients). Tokens live in the database, not project config |
| Schema with mutations | Same bearer token | Craft warns such a token is about as dangerous as a user with equivalent permissions; keep it server side, never in browser code |
| OAuth and personal access tokens (5.12.0+) | Token tied to a user, scopes graphql:read or graphql:write | Personal access tokens expire after 7, 30 or 90 days or one year; creation needs an elevated session. Prefer the shortest period that works |
| Full schema | Admins only, for GraphiQL testing | Not for applications |
- Restrict CORS: GraphQL responses send
access-control-allow-origin: *by default. Configure thecorsfilter in application config so only your front-end origins are allowed. - Disable risky directives: turn off
@parseRefson schemas that expose content from untrusted users, and setdisableGraphqlTransformDirectiveif you do not need on-demand image transforms, which can be abused to consume server resources. - Plugins are code with full rights: a Craft plugin is a Composer package that runs inside the application with the same privileges as Craft itself, including database access and the security key. Install only plugins you have reviewed for maintenance activity and advisories, pin versions in
composer.lock, and check them in your dependency scanning. See the Dependency Update Automation Guide. - Plugin credentials: commerce gateways, search services and mail transports store API keys. Reference them as environment variables in plugin settings rather than typing values into the control panel, so they stay out of project config and backups.
Logging and detection
Craft writes daily files to storage/logs/: web-[date].log, console-[date].log and queue-[date].log, rotated with a default of five files kept. With CRAFT_STREAM_LOG set to true, logs go to stdout and stderr, which is the right choice for containers feeding a central platform. Outside dev mode, messages below warning are dropped unless you set level in monologTargetConfig. Craft's documentation does not describe a built-in audit log of logins or permission changes, so capture them from web server logs and the database, or add a logging target.
| Signal | Where to see it | Why it matters |
|---|---|---|
New admin user or new administrateUsers holder | users table admin flag, permission tables; periodic query | Common persistence step after exploitation (ATT&CK T1098) |
| POST requests to unusual action routes from unknown IPs | Web server or WAF logs | Exploit attempts arrive as crafted requests (T1190) |
New or changed .php files under web/ or storage/ | File integrity monitoring | Web shell drop (T1505.003) |
Requests for /.env, /.git/ or backups | Web server logs | Security key theft attempt (T1552.001) |
| Bursts of HTTP 429 on login | Web logs | Password spraying against control panel accounts |
devMode, disable2fa or allowAdminChanges true in production | Configuration drift check in CI | Removes core protections silently |
Track a small set of measures: days from a Craft or plugin advisory to production patch (target under 3 days for critical issues), the number of admin and escalation-capable accounts, the share of control panel users with two-step verification or passkeys (target 100%), the age of the oldest GraphQL token, and the count of plugins without a release in the last 12 months. The sensitiveKeywords property of the security component adds names to the redaction list for logged environment variables; add your own secret names so they never reach log storage.
Hardening baseline
| Setting or control | Default | Production value |
|---|---|---|
devMode | false | false, enforced by environment |
allowAdminChanges | true | false |
securityKey | empty | Unique per environment, from CRAFT_SECURITY_KEY |
enableCsrfProtection | true | true; include CSRF tokens on every front-end form |
preventUserEnumeration | false | true |
sendPoweredByHeader | true | false |
enableTwigSandbox | false in the reference (on in new projects from 4.17.0 and 5.9.0) | true |
sanitizeSvgUploads | true | true |
trustedHosts | ['any'] | Your proxy addresses only |
elevatedSessionDuration | 300 | 300 or lower, never 0 |
PHP register_argc_argv | Depends on distribution | Off for web SAPIs |
- Inventory first: list every Craft site, its version and release line, its plugins and who holds admin. Retire sites on unsupported release lines. See the Patch Management Guide.
- Fix the file layout: only
web/is public; runcraftcms/server-check; set least-privilege file permissions with nothing world-writable. - Harden PHP: Craft 5 needs PHP 8.2 or later; use the newest your host supports and review
php.inifor the web SAPI. See the PHP Configuration Hardening guide. - Apply the settings table in a staging environment, then promote through project config.
- Enforce two-step verification for every group with control panel access, and register passkeys for admins.
- Add headers: Craft 5.3 and later can set headers and CORS in application config. Set HSTS,
X-Content-Type-Options, a frame policy that allows live preview where needed, and a Content Security Policy. See the Content Security Policy Guide. - Restrict uploads: trim
allowedFileExtensionsto what editors actually need and keep non-public asset volumes outside the web root. See the File Upload Security Guide. - Put a WAF in front with rules for published Craft advisories, as a stopgap not a replacement for patching. Roll back any setting by reverting the project config commit; keep a database backup taken before each change.
Common misconfigurations
- Security key committed to a repository: agency starter kits and client handovers often ship
.envfiles. On an unpatched site, that can lead to code execution. Scan history, rotate and patch. - Templates rendering user input as Twig: using the
rawfilter on user content or passing it to template rendering creates server-side template injection. Escape output and validate parameter types, for example with thenumerictest before passing an ID to an element query. - Dev mode left on: it exposes detailed errors and stack traces. Production should fail closed with generic error pages.
- Proxy trust left open:
trustedHostsatanylets clients spoof forwarded IP headers, defeating rate limiting and IP-based control panel restrictions. - Abandoned plugins kept for one feature: unmaintained plugins carry unpatched flaws with the application's full rights. Replace or remove them.
- Public GraphQL schema with broad scope: enabling the public schema for convenience can expose drafts, user fields or mutations to anyone.
Framework mapping
| Control area | NIST SP 800-53 Rev. 5 | MITRE ATT&CK |
|---|---|---|
| Rapid patching of Craft, plugins and Yii | SI-2 Flaw Remediation, RA-5 | T1190 Exploit Public-Facing Application |
| Security key and secrets handling | SC-12, IA-5 | T1552.001 Credentials In Files |
| Permissions and admin limits | AC-6 Least Privilege | T1098 Account Manipulation, T1078 Valid Accounts |
| Two-step verification and passkeys | IA-2 | T1110 Brute Force |
| Production settings baseline | CM-6, CM-7 | T1505.003 Web Shell |
| Plugin vetting | SR-3 | T1195.002 Compromise Software Supply Chain |
| Logging | AU-2, AU-6 | Supports detection of all of the above |
Practitioner checklist
- Confirm every Craft site is at or above 3.9.15, 4.14.15 or 5.6.17 on its release line, and subscribe to the
craftcms/cmsadvisories. - Check
register_argc_argvis off for the web SAPI on every server. - Move
securityKeyto an environment variable, make it unique per environment, and rotate it if it has ever been in Git or a shared file. - Set
allowAdminChangesfalse,devModefalse,preventUserEnumerationtrue andsendPoweredByHeaderfalse in production. - Limit admin and the escalation-capable permissions to named people, reviewed quarterly.
- Require two-step verification for all control panel groups and register passkeys for admins.
- Give each GraphQL consumer its own token with the narrowest schema, restrict CORS, and keep mutation tokens server side.
- Review plugins for maintenance and advisories, pin them in
composer.lockand remove unused ones. - Stream logs to central storage and add file integrity monitoring on the web root and
storage/.
Standards and references
- Craft CMS: Securing Craft
- Craft CMS: General Config Settings
- Craft CMS: User Management
- Craft CMS: GraphQL API
- Craft CMS: Security Advisories
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: PHP Configuration Hardening · GraphQL Security Guide · Secrets Management Guide · Web Application Firewall Guide