Repository navigation
v3.3.0 — Nav trees, URL import, extra tools, and a security audit
Upgrade promptly if any token or Control Panel user can upload assets. The first
entry below is a code-execution hole on a typical PHP host.
Security
-
Uploads go through Statamic's allowed-extensions check — The Control Panel runs every upload through Statamic's
AllowedFilerule. Thestatamic-assetsuploadandcreateactions did not, so a token withassets:write, or a Control Panel user over Basic Auth who may upload, could writeshell.phpinto a container. With a container underpublic/and a stock PHP host, that is code execution. Both actions now store through one path that applies the rule;statamic.assets.additional_uploadable_extensionswidens it as it does in the Control Panel.moveandcopycannot change a file's extension.This refuses uploads that previously succeeded — any file whose extension Statamic would not accept from the Control Panel either.
-
Outbound fetches can no longer be pointed at the server's own network. The OAuth Client ID Metadata Document fetch checked one IPv4 address with
gethostbyname()and then let the HTTP client resolve the name again, so a host could answer public to the check and private to the request (DNS rebinding). The blocked list also missed CGNAT, multicast, reserved and documentation ranges, IPv6 link-local, and IPv6 forms that embed an IPv4 address such as::ffff:127.0.0.1. A newOutboundFetcherchecks every A and AAAA answer, pins the connection to the checked address, re-checks each redirect, caps the body while reading, and never names the address it refused. CIMD and the new URL import both use it. -
An extra tool that names no permission is for super admins only — follow-up to #66 below. A router another package registers whose
getRequiredPermissions()returns nothing would otherwise be open to every Control Panel user over Basic Auth.
The rest of this block comes from an internal source-and-sandbox security audit of the whole addon (see Known follow-ups for what it surfaced but this release does not yet fix):
-
Id-addressed actions are bound to the resource the caller named. Authorization (the per-collection/taxonomy/global-set permission and the resource-policy allowlist) was keyed on the resource named in the arguments, while the handler acted on whatever the
idresolved to. A caller allowed on one collection could read, change or delete entries in another by pairing its own collection's handle with a foreign id; terms had the same gap on update and delete. Entry get/update/delete/publish/unpublish/localize and the revision actions now resolve the entry only within the named collection, and term update/delete apply the taxonomy checkgetalready had.This refuses cross-collection id access that previously succeeded.
-
The resource policy and the handler now name one resource.
global_set(a bare alias), and an unrelated allowedcollection/containerkey sent alongside a deniedhandle/data.handle, could satisfy the policy for a resource the action did not touch.GlobalsRouter,StructuresRouterandAssetsRouternow resolve the policy handle the same way their handlers do. -
statamic-usersstays within the caller's own authority. A non-super caller withedit rolescould addsuperto a role (its own included) and become super;create users/edit user groupsattacheddata.roleswithoutassign roles; andedit userscould set any user's password, including a super admin's. Grantingsupernow requires a super admin, attaching roles requiresassign roleson every path, and changing another user's password requireschange passwords— matching Statamic's own policies. Term create/delete likewise requirecreate/delete {taxonomy} termsrather thanedit. -
manage all mcp tokensis no longer impersonation. A non-super admin could widen a super admin's token to*and regenerate it to receive the plaintext secret. Regenerate is restricted to the caller's own tokens, widening another user's token is capped by the editor too, and a non-super admin cannot modify a super admin's token. -
denied_fieldsno longer leaks through list filters or revision snapshots. A denied field used as alistfilter key was an equality oracle;get_revisionreturned the field in full. Both are now filtered. -
Blueprint handles are validated.
create/generatepassed the handle and namespace straight into Statamic's path construction, which does not resolve.., so a handle like../evilwrote YAML outside its namespace. Handles must now match^[A-Za-z0-9_-]+$and the namespace must be a known one. -
file_pathuploads are CLI-only. The documented-CLI-onlyfile_pathupload source had no context gate, so a remote caller could copy files out ofstorage/appinto a container. It now requires CLI context. -
Deactivated users lose MCP access. The
deactivateaction setstatus=inactivebut nothing on the auth path read it. Deactivation now revokes the user's MCP tokens and both auth paths refuse an inactive user.
Known follow-ups
The audit surfaced issues this release does not yet address; they are either behavioural changes that need maintainer sign-off or depend on deployment facts:
- MCP entry/term/global writes do not apply Statamic's author-ownership and per-site policy (
EntryPolicy"other authors" / "access {site} site"). statamic-system-discoverandstatamic-content-facadesweeps list resources the per-domain resource policy would hide.- Revoking an OAuth access token does not revoke the matching refresh token, so the grant can be renewed after revocation.
- The OAuth consent screen does not show the redirect destination and sets no frame-protection header.
- CLI-vs-web trust derives from the process SAPI plus an optional header; under a console-SAPI HTTP runtime (e.g. Octane) authenticated web calls could be treated as the trusted local operator. Needs validation against the supported runtimes.
- The release workflow runs freshly resolved dependency code next to a write-capable token; CI hardening (pinned actions,
persist-credentials: false, narrowedpermissions).
Added
-
Import a file from a URL (#67) —
statamic-assetsuploadtakessource_url; the server downloads the file and stores it like any other upload. A remote client such as Claude can name an image on the web but cannot reasonably send megabytes of base64 through a tool call, so until now it could not add one at all. The filename defaults to the last segment of the URL. Configured undersecurity.url_import:enabled(on by default),timeout,allowed_hosts(exact hosts or wildcards, checked on every redirect) andblock_private_ipsfor local development. Thanks to @Bennovdp for the detailed request. -
Uploads can target a folder —
uploadtakesfolder, e.g.uploads/cases. Every upload used to land in the container root. -
Other packages can add their own tools (#66, @illia-sapryga) —
StatamicMcpServer::registerTool(Router::class)from a service provider, or thestatamic.mcp.extra_toolsconfig key. Extra tools must extendBaseRouter, so the web toggle, token scope, Statamic permissions, resource policy, confirmation gate and audit log all apply. They are reached throughsearch_toolsby default, and appear in the instructions,statamic-system-discoverandstatamic-system-schema. A router whose domain has no token scope in this package requires a full-access token. See Extending: tools.
Fixed
-
Navigation trees can be written (#65, @timluigjes) —
updatenever read a tree, andconfigureread aconfigargument the tool schema does not declare, so every attempt reported success and saved nothing.create,updateandconfigurenow share one write that takes the tree in the shapegetreturns (trees: [{site, items}]), as a map of site to items, or as a singletreewith an optionalsite. Items without an id get one, as in the Control Panel. The whole payload is validated before anything is saved: unknown keys, missing entries, duplicate ids, unknown sites and items deeper thanmax_depthare refused with the path that failed. A tree write now also clears the static cache, since it changes rendered menus.Navigation writes now refuse keys they do not understand, where they used to drop them silently.
-
copyworks — It called a method Statamic'sAssetdoes not have, so it failed on every call. It now copies the file and its data, and refuses to overwrite. -
movereadsdestinationas a path — The schema documentsdestinationas the new path, but it was handed to Statamic as a folder:archive/old-hero.jpgended up atarchive/old-hero.jpg/hero.jpg. A trailing slash, or a destination without an extension, still names a folder and keeps the filename. -
Discovery lists
localize, whichstatamic-entrieshas handled since 2.10.0.statamic-system-schemaalso caught up with the revision actions andstatamic-userssearch.
Dependencies
symfony/http-foundation8.1.9 — earlier releases are affected by CVE-2026-108082, a ReDoS inHeaderUtils::split(). It reaches your site through Laravel, so this package does not pin it; runcomposer update symfony/http-foundation. The SBOM now records 8.1.9.
Full changelog: v3.2.1...v3.3.0