ANT-2026-ZQ8AY22X · CraftCMS

privilege-escalation high

GHSA-cc7p-2j3x-x7xf

Severity Claude high · Security research firm - · Maintainer high

Discovered by Claude Mythos Preview

REPORT

The report below was sent to the maintainer and sealed at approval.

ANT-2026-ZQ8AY22X: Privilege Escalation/Bypass through UsersController->actionImpersonateWithToken()

Craft CMS's actionPreview() re-dispatches requests with $skipSpecialHandling=true and $checkToken=false, allowing an attacker-supplied action query parameter to redirect execution to UsersController::actionImpersonateWithToken(). That endpoint's only guard, requireToken(), merely checks the boolean _hadToken set when the preview token was resolved, without verifying the token was minted for impersonation. Because the action is also listed in $allowAnonymous, no prior authentication is enforced. An editor (or anyone holding a shared preview URL) can therefore append &action=users/impersonate-with-token&userId=1&prevUserId=1 to a preview URL and be logged in as user 1 (admin).

Target

Project: CraftCMS
Discovery: static analysis — not yet dynamically reproduced

Technical Details

Root cause is a confused-deputy between the preview dispatcher and the impersonation endpoint: actionPreview() passes $skipSpecialHandling=true to handleRequest() and $checkToken=false to checkIfActionRequest(), so security guards are skipped and the action parameter is attacker-controlled. requireToken() on actionImpersonateWithToken() only inspects _hadToken (set for any valid token) rather than validating that the token was issued for this route, and the action is in $allowAnonymous, so no further authorization occurs.

Reproduction

This finding was identified by static analysis and has not yet been dynamically reproduced. The Technical Details section above describes the code path; a trigger input is not included.

[No reproducer or sanitizer output attached — request from cvd@anthropic.com if needed.]

Acknowledgement

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-ZQ8AY22X.


Reference: ANT-2026-ZQ8AY22X
Anthropic CVD Policy: https://anthropic.com/security/cvd-policy

UPSTREAM FIX

https://github.com/craftcms/cms/security/advisories/GHSA-cc7p-2j3x-x7xf

TIMELINE

Dates from discovery through public reveal.

  1. 2026-03-16 Patch released
  2. 2026-03-29 Reported to tracker
  3. 2026-05-08 Sent to maintainer
  4. 2026-05-08 Maintainer acknowledged
  5. 2026-05-20 Publicly revealed
PROVENANCE

SHA-3-512 hash:

dc75439eef02f2f72ba1440db7ca5fbae9599c252caba20b38f6b5c634fae74db645c91470b08c00012cc38b7353a9a7f79f52cf703bb1a668c6df7682c8907f

Committed 2026-05-08 09:37 PT

Revealed 2026-05-20 00:40 PT

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-ZQ8AY22X",
  "bug_class": "privilege-escalation",
  "claude_severity": "high",
  "commit_sha": null,
  "created_at": "2026-03-29T20:43:35+00:00",
  "description": "Craft CMS's actionPreview() re-dispatches requests with $skipSpecialHandling=true and $checkToken=false, allowing an attacker-supplied action query parameter to redirect execution to UsersController::actionImpersonateWithToken(). That endpoint's only guard, requireToken(), merely checks the boolean _hadToken set when the preview token was resolved, without verifying the token was minted for impersonation. Because the action is also listed in $allowAnonymous, no prior authentication is enforced. An editor (or anyone holding a shared preview URL) can therefore append &action=users/impersonate-with-token&userId=1&prevUserId=1 to a preview URL and be logged in as user 1 (admin).",
  "discovered_at": null,
  "location": null,
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "CraftCMS",
  "reproduction": null,
  "technical_details": "Root cause is a confused-deputy between the preview dispatcher and the impersonation endpoint: actionPreview() passes $skipSpecialHandling=true to handleRequest() and $checkToken=false to checkIfActionRequest(), so security guards are skipped and the action parameter is attacker-controlled. requireToken() on actionImpersonateWithToken() only inspects _hadToken (set for any valid token) rather than validating that the token was issued for this route, and the action is in $allowAnonymous, so no further authorization occurs.",
  "title": "Privilege Escalation/Bypass through UsersController->actionImpersonateWithToken()",
  "vendor_severity": null
}