Join our Newsletter — 33% off our NHI Course
Home› Guides› Craft CMS Security: Patch, Protect the Security Key…
Guide Web & API Security

Craft CMS Security: Patch, Protect the Security Key and Harden Plugins

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 8 November 2026 14 min read
On this page

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 securityKey may 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_argv disabled on web servers; a correct php.ini closes some issues before a patch exists.
  • Several editor permissions are effectively admin. administrateUsers, assignUserPermissions, impersonateUsers and utility:system-messages all allow escalation; grant them like admin rights.
  • Production should be read-only for configuration. Set allowAdminChanges to false and turn off devMode, 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: maxInvalidLogins defaults to 5 attempts before lockout and cooldownDuration to 300 seconds. From 5.12.0, defaultRateLimit allows 10 requests per minute per IP on rate-limited actions before HTTP 429; behind a proxy set ipHeaders and trustedHosts or every user shares the proxy's address.
  • Sessions: userSessionDuration defaults to 3600 seconds of inactivity, and requireMatchingUserAgentForSession is 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: preventUserEnumeration is false by default. Set it to true so the forgotten password form always reports success.
  • The development switch: disable2fa (or CRAFT_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:

  1. 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 .env is exposed for good. See the Secrets Management Guide.
  2. Stop the web server from serving dot-files: Craft's guidance is to keep .env and other dot-files unreachable and to place config/, templates/, storage/ and vendor/ above the public web root.
  3. Use a unique key per environment: staging and production must not share a key, so a staging leak does not open production.
  4. 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.
  5. Rotate the neighbours too: a leaked .env usually 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 handleWhat it allowsWhy it is sensitive
administrateUsersChange emails, send password resets, set passwords, deactivate usersCraft 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 membershipSelf-escalation path if combined with edit rights on own account
impersonateUsersAct as another user temporarilyActions appear to come from the impersonated user, which weakens accountability
utility:system-messagesEdit system email templatesTwig in system messages can enable permission escalation; trusted admins and developers only
utility:db-backup, utility:find-replace, utility:migrationsDownload the database, rewrite content in bulk, run migrationsA database backup holds password hashes and every user's data
performUpdatesUpdate Craft and plugins from the control panelProduction 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 cpTrigger from admin is obscurity only.
  • Lock production: with allowAdminChanges false, the Settings section is hidden, project config and the Plugin Store become read-only, and allowUpdates is 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.

IdentityHow it authenticatesKey controls
Public GraphQL schemaNo token: requests without a token use it, if enabledDisabled by default. Enable only for content that is genuinely public; never include mutations or user data
Private schema tokenRandom 32-character string sent as Authorization: BearerOne 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 mutationsSame bearer tokenCraft 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:writePersonal access tokens expire after 7, 30 or 90 days or one year; creation needs an elevated session. Prefer the shortest period that works
Full schemaAdmins only, for GraphiQL testingNot for applications
  • Restrict CORS: GraphQL responses send access-control-allow-origin: * by default. Configure the cors filter in application config so only your front-end origins are allowed.
  • Disable risky directives: turn off @parseRefs on schemas that expose content from untrusted users, and set disableGraphqlTransformDirective if 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.

SignalWhere to see itWhy it matters
New admin user or new administrateUsers holderusers table admin flag, permission tables; periodic queryCommon persistence step after exploitation (ATT&CK T1098)
POST requests to unusual action routes from unknown IPsWeb server or WAF logsExploit attempts arrive as crafted requests (T1190)
New or changed .php files under web/ or storage/File integrity monitoringWeb shell drop (T1505.003)
Requests for /.env, /.git/ or backupsWeb server logsSecurity key theft attempt (T1552.001)
Bursts of HTTP 429 on loginWeb logsPassword spraying against control panel accounts
devMode, disable2fa or allowAdminChanges true in productionConfiguration drift check in CIRemoves 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 controlDefaultProduction value
devModefalsefalse, enforced by environment
allowAdminChangestruefalse
securityKeyemptyUnique per environment, from CRAFT_SECURITY_KEY
enableCsrfProtectiontruetrue; include CSRF tokens on every front-end form
preventUserEnumerationfalsetrue
sendPoweredByHeadertruefalse
enableTwigSandboxfalse in the reference (on in new projects from 4.17.0 and 5.9.0)true
sanitizeSvgUploadstruetrue
trustedHosts['any']Your proxy addresses only
elevatedSessionDuration300300 or lower, never 0
PHP register_argc_argvDepends on distributionOff for web SAPIs
  1. 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.
  2. Fix the file layout: only web/ is public; run craftcms/server-check; set least-privilege file permissions with nothing world-writable.
  3. Harden PHP: Craft 5 needs PHP 8.2 or later; use the newest your host supports and review php.ini for the web SAPI. See the PHP Configuration Hardening guide.
  4. Apply the settings table in a staging environment, then promote through project config.
  5. Enforce two-step verification for every group with control panel access, and register passkeys for admins.
  6. 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.
  7. Restrict uploads: trim allowedFileExtensions to what editors actually need and keep non-public asset volumes outside the web root. See the File Upload Security Guide.
  8. 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 .env files. On an unpatched site, that can lead to code execution. Scan history, rotate and patch.
  • Templates rendering user input as Twig: using the raw filter on user content or passing it to template rendering creates server-side template injection. Escape output and validate parameter types, for example with the numeric test 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: trustedHosts at any lets 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 areaNIST SP 800-53 Rev. 5MITRE ATT&CK
Rapid patching of Craft, plugins and YiiSI-2 Flaw Remediation, RA-5T1190 Exploit Public-Facing Application
Security key and secrets handlingSC-12, IA-5T1552.001 Credentials In Files
Permissions and admin limitsAC-6 Least PrivilegeT1098 Account Manipulation, T1078 Valid Accounts
Two-step verification and passkeysIA-2T1110 Brute Force
Production settings baselineCM-6, CM-7T1505.003 Web Shell
Plugin vettingSR-3T1195.002 Compromise Software Supply Chain
LoggingAU-2, AU-6Supports 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/cms advisories.
  • Check register_argc_argv is off for the web SAPI on every server.
  • Move securityKey to an environment variable, make it unique per environment, and rotate it if it has ever been in Git or a shared file.
  • Set allowAdminChanges false, devMode false, preventUserEnumeration true and sendPoweredByHeader false 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.lock and remove unused ones.
  • Stream logs to central storage and add file integrity monitoring on the web root and storage/.

Standards and references

Related NHI Mgmt Group resources: PHP Configuration Hardening · GraphQL Security Guide · Secrets Management Guide · Web Application Firewall Guide

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 8 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org