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 at public/ and confirm that https://your-site/.git/config and https://your-site/.env both return 404. An exposed .git directory 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 .gitattributes export-ignore rules). If you do deploy a clone, disable push on the install (git remote set-url --push origin DISABLED).
  • Set APP_DEBUG=false and a strong APP_KEY in production. Debug mode leaks stack traces, environment values, and internal paths.
  • Use HTTPS and set SESSION_SECURE_COOKIE=true when serving over TLS.
  • File permissions: the web/PHP user needs read/write on storage/ and the backups disk root (default storage/app/backups). Do not use 777; grant ownership or group access instead.
  • Keep secrets in .env. .env, .env.* (except .env.example), and auth.json are 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), and editor (draft content within assigned sites). Cross-site actions (move/duplicate, Shared Slots) enforce access to both source and target sites. See Users & Permissions.
  • The /webadmin admin 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 with content.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_ATTEMPTS and WEBBLOCKS_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_admin from 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/api routes and the legacy /admin-api compatibility 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 the checksum_sha256 provided by the release metadata using a timing-safe hash_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:

  1. Generate a key pair once, on the maintenance/publisher machine: php artisan webblocks:updates:keygen.
  2. 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.
  3. Pin the printed public key so installs verify signed releases: set WEBBLOCKS_UPDATE_PUBLIC_KEY, or set ReleaseDefaults::UPDATE_PUBLIC_KEY so the key ships in the CMS code (recommended — a code-pinned key cannot be swapped through a compromised .env).
  4. 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, /24 or /64 network 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 persisted installation_id, and telemetry_schema_version. No domains, URLs, admin emails, paths, database details, user counts, tokens, or arbitrary env/config values are sent. Set WEBBLOCKS_TELEMETRY=false to 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.