Repository navigation
Release v3.4.0
This release closes every item from the Known follow-ups of 3.3.0 — the
remainder of the internal source-and-sandbox security audit, plus the CI
hardening that depended on it.
Security
-
MCP entry actions apply Statamic's own
EntryPolicy(#68) —statamic-entriesresolved an entry by collection + id and then acted on it with only a token scope and the site-wide resource policy; the per-entry policy was never consulted. A delegated, non-super caller withedit {collection} entriesbut not theedit other authors {collection} entriesvariant could edit, publish or delete entries authored by someone else, and a caller barred from a site (access {site} site) could still write that site's entries.get,update,delete,publish,unpublishandlocalizenow run the matching ability through the caller'sEntryPolicyonce the entry is resolved. It applies in web context only — CLI is the trusted operator and super admins pass via the policy'sbefore().This refuses entry writes that previously succeeded — a caller acting on another author's entry, or on a site it may not access, where the collection-level permission alone used to let it through.
-
Discovery and the content facade are scoped to the resource policy (#69) —
statamic-system-discoverandstatamic-content-facade's sweeps listed collections, taxonomies and globals the per-domain resource-policy allowlist would hide from a direct call, disclosing the existence and handles of resources the caller cannot read. Both now filter every section through the sameResourcePolicycheck the read tools apply. -
Revoking an OAuth access token ends the whole grant (#70) — revoking an access token left its refresh token intact, so a client could renew access immediately afterwards, defeating revocation as an incident-response control.
TokenService::revokeToken()now also revokes the refresh tokens for that grant (user + OAuth client), via a newOAuthDriver::revokeRefreshTokensFor(); a registered client that has since been deleted or pruned can no longer refresh either. -
The OAuth consent screen shows where approval sends the user (#71) — the screen named the client but not the redirect destination, and set no frame-protection header, so a user could approve without seeing the callback host and the page could be framed. It now shows the parsed redirect host, labels a dynamically-registered (unverified) client as such, and sends
X-Frame-Options: DENYandContent-Security-Policy: frame-ancestors 'none'. -
Web-vs-CLI trust no longer derives from the process SAPI (#72) — the trusted-operator decision keyed on
php_sapi_name()plus an optional header, so under a console-SAPI HTTP runtime (such as Laravel Octane) an authenticated web call could be treated as the local CLI operator and skip the author/site and privilege guards. A newMcpContextdecides from an attribute the web middleware sets on the request, never from the SAPI;AuthenticateForMcpmarks every web request, and all the guards read that marker.
CI
- The release and test workflows are hardened (#73) — they ran with the repository's default (write) token, left the job token in
.git/configthroughactions/checkout, and pinned third-party actions by mutable tag, so freshly resolved dependency code ran next to a write-scoped token. Every workflow now defaults topermissions: contents: read;release.ymlsplits the build/test work (read-only) from a minimal job that holdscontents: writeonly to create the GitHub release; every checkout setspersist-credentials: false; and all actions are pinned to commit SHAs.