Security
This page describes the security model of WebBlocks CMS and the steps an operator or agency should take to run it safely for client sites. For how to report a vulnerability, see SECURITY.md — please do not open public issues for security reports.
Deployment hardening
These are the most important controls for a production install:
- Serve only
public/as the web root. The application root contains.env,.git,.github,storage/,vendor/, and source code that must never be web-reachable. Point your Nginx/Apache document root atpublic/and confirm thathttps://your-site/.git/configandhttps://your-site/.envboth return 404. An exposed.gitdirectory leaks your entire source and any committed secrets. - Prefer a release artifact over a raw git clone in production. Release
packages exclude
.git,.github,project/, and other non-runtime files (see.gitattributesexport-ignorerules). If you do deploy a clone, disable push on the install (git remote set-url --push origin DISABLED). - Set
APP_DEBUG=falseand a strongAPP_KEYin production. Debug mode leaks stack traces, environment values, and internal paths. - Use HTTPS and set
SESSION_SECURE_COOKIE=truewhen serving over TLS. - File permissions: the web/PHP user needs read/write on
storage/and thebackupsdisk root (defaultstorage/app/backups). Do not use777; grant ownership or group access instead. - Keep secrets in
.env..env,.env.*(except.env.example), andauth.jsonare git-ignored. CMS API tokens and mail passwords are never written to the repository.
Authentication and authorization
- CMS auth is Laravel-native (no Breeze/Jetstream/Fortify requirement). Admins
sign in at
/webadmin/login; passwords are bcrypt-hashed. - Three install roles gate access:
super_admin(install-wide),site_admin(assigned sites), andeditor(draft content within assigned sites). Cross-site actions (move/duplicate, Shared Slots) enforce access to both source and target sites. See Users & Permissions. - The
/webadminadmin tree is protected by the CMS web + auth + admin-access middleware stack. Public routes never render draft content; draft/preview access requires an authenticated admin or a trusted token withcontent.read. - Sign-in and password-reset requests are rate-limited. Failed logins are
throttled per email+IP (default 5 attempts, then a short lockout that a
successful sign-in clears; tune with
WEBBLOCKS_CMS_MAX_LOGIN_ATTEMPTSandWEBBLOCKS_CMS_LOGIN_DECAY_SECONDS). A per-IP backstop additionally caps the login, forgot-password, and reset-password endpoints against floods and email-rotation attempts.
Internal Content API tokens
The Internal Content API (/webadmin/api) is for trusted operator/AI tooling.
- Personal tokens are created by any active CMS user from Profile → Personal
API Tokens; installation-level System tokens are created by a
super_adminfrom System → API Tokens. Both are stored as hashes and shown in plain text only once. - Each token carries explicit capabilities (read, publish, media, plugin lifecycle, commerce, …). Advanced/destructive capabilities are separate opt-ins; normal page-building capabilities are the default.
- Tokens can be revoked (keeping the audit row) or deleted. A per-token activity log records time, method/path, route, capability result, IP, and a user-agent summary — but never request bodies, query strings, responses, or token values.
- Treat API tokens as secrets. Scope them to the minimum capabilities a tool needs, and rotate them if exposed.
- Personal API tokens continuously intersect selected capabilities and sites with their owner's live role, active state, site assignments, and page workflow authority. Installation-level operations remain System-token only.
- Personal API tokens can be restricted to exact IPv4/IPv6 addresses or CIDR networks and carry a token-specific per-minute request ceiling. These checks are enforced in addition to live user, site, workflow, and capability access.
- Both the canonical
/webadmin/apiroutes and the legacy/admin-apicompatibility routes share the Internal Content API rate limiter.
When a reverse proxy or CDN is present, configure Laravel to trust only the actual proxy addresses and verify the resolved client IP before enabling an allowlist. Never accept spoofable forwarded headers directly from the internet.
See Personal API Tokens and Internal Content API.
Update security
In-app System Updates replace the CMS package code at
vendor/fklavyenet/webblocks-cms on the live site. Because an update executes new
code, the integrity of the downloaded package is a remote-code-execution
boundary. WebBlocks CMS mitigates this as follows:
- Mandatory SHA-256 checksum. The updater downloads the release ZIP, then
verifies
hash_file('sha256', …)against thechecksum_sha256provided by the release metadata using a timing-safehash_equals. If the checksum is missing or does not match, the update is refused — it does not fall back to applying an unverified package. - Canonical update service. Update metadata and downloads come from
publisher.webblocksui.com. Because the checksum is delivered alongside the download by the same service, the checksum protects against corrupted or tampered artifacts, but not against a fully compromised update service. - Installs are consumers, not publishers. Installed sites fetch updates but
must not push to the upstream repository. Publishing is done only from the
maintenance checkout with a
WEBBLOCKS_PUBLISHER_TOKEN.
Signature verification (Ed25519)
For defense-in-depth against a compromised update service — where the checksum
travels alongside the artifact — releases can be cryptographically signed.
The publisher signs the release checksum with an Ed25519 secret key, and installs
verify the signature against a pinned public key (sodium_crypto_sign), so an
install rejects any release not signed by the real key.
To enable it:
- Generate a key pair once, on the maintenance/publisher machine:
php artisan webblocks:updates:keygen. - Keep the printed
WEBBLOCKS_PUBLISHER_SIGNING_KEY(secret) private — set it only where you publish releases. Never commit it or set it on an install. - Pin the printed public key so installs verify signed releases: set
WEBBLOCKS_UPDATE_PUBLIC_KEY, or setReleaseDefaults::UPDATE_PUBLIC_KEYso the key ships in the CMS code (recommended — a code-pinned key cannot be swapped through a compromised.env). - Publish as usual; the publisher signs each release automatically.
Rollout is safe: while no public key is pinned, signature verification is not enforced (checksum verification still applies). Once a public key is pinned and installs receive that code, every future release must carry a valid Ed25519 signature over its checksum, or the update is refused.
Content and input safety
- Public forms share a local protection pipeline: form-bound signed proof,
generated honeypots, timing, content/repetition scoring, keyed sender and source
counters,
/24or/64network pressure, and site-local learned fingerprints. It makes no external request. Protection counters use keyed hashes; daily metrics contain aggregate decisions only. See Public Submission Protection. - Contact forms store scored submissions for review. Quarantine and spam suppress notification while remaining separate from notification delivery history. See Contact Forms & Messages.
- Comments default to
pending; a spam decision is retained as spam and never appears publicly without moderation. - Trusted HTML is limited to wrapper-adjacent layout markup and must not be used to inject scripts; prefer the native block contracts, which the Internal Content API validates draft-first.
- Media uploads are restricted to an allowlist of image, video, and document
types (content-sniffed, not extension-trusted). SVG uploads are disabled
by default, because an SVG can carry inline script and media is served from
the same origin as the admin. Enable it only on installs where every account
that can upload media is trusted, via
WEBBLOCKS_CMS_ALLOW_SVG_UPLOADS=true. The same allowlist governs server-side remote media fetches. - Remote media fetching validates every redirect target and pins the HTTP connection to the public IP address that passed validation. This closes the DNS lookup/connection race used by DNS-rebinding attacks. Remote fetching fails closed when PHP cURL address pinning is unavailable.
- Managed Embedded Application iframes are opaque-origin sandboxes. Their
entry responses apply restrictive CSP and referrer headers. CSP names the
current registered site origin explicitly so opaque-origin documents can load
their same-site scripts, styles, media, and
<base>URL without granting the iframe same-origin access to CMS cookies, storage, the parent page, or authenticated panel requests. - Complete Embedded Application packages stay isolated. Immutable,
versioned package files are served through an application-only public route
with anonymous CORS and cross-origin resource headers, allowing opaque-origin
games to load images, audio, fonts, JSON, and locale files without granting
allow-same-origin. ZIP installation rejects traversal, duplicate paths, executable server files, and bounded-count or expanded-size violations.
Telemetry and privacy
- Update checks may send privacy-preserving adoption telemetry to the Publisher:
only
product_key,installed_version,channel, a random locally persistedinstallation_id, andtelemetry_schema_version. No domains, URLs, admin emails, paths, database details, user counts, tokens, or arbitrary env/config values are sent. SetWEBBLOCKS_TELEMETRY=falseto opt out; metadata checks continue without an installation ID. - Visitor Reports retain page-view records with path, time, normalized referrer host, UTM values, device category and bot classification. Consent-based full tracking may also retain a session key and IP HMAC; these are pseudonymous identifiers, not a guarantee of anonymity. New charts and page detail modals introduce no additional identifiers or public tracking scripts. Scheduled retention replaces expired detail with daily site/locale counts and later expires those counts. See Operations.
Reporting a vulnerability
Report privately via GitHub Security Advisories or the maintainer contact in SECURITY.md. We aim to acknowledge reports within 5 business days and will coordinate a disclosure timeline with you.
Plugin Startup And Recovery (1.94.0–1.94.2)
CMS-managed plugin install, update, and activation validate the candidate in a separate PHP process before normal-runtime activation. Invalid source, provider/route failures, early exits, and timeouts reject it; a rejected update retains the working package. Database setup failures keep the plugin disabled and preserve its data. Runtime source or route failures quarantine the affected plugin and remove partially registered routes.
The /webadmin/plugin-recovery screen and login load without installed plugin source. CMS 1.94.2 applies active-account admin access and Super admin authorization, including existing sessions. Recovery can disable the failing plugin or restore the retained previous package only when no database migration ran; restoration repeats startup validation and republishes the previous assets. Normal login controls and CSRF protection remain in force. This protects the managed-plugin recovery path, not arbitrary host providers or executable PHP isolation. See Plugin System.
System Updates API Authority (1.90.0)
Installation updates require an installation-wide system token owned by an active user with system access. system-updates.read and system-updates.run are separate opt-ins; neither personal tokens nor site-scoped tokens can use them. Execution requires explicit approval of the installed version, target version, and checksum, with a durable idempotency receipt for uncertain responses. See Updates.