Operations

Markdown

Pre-release security checklist

Use this checklist before production — application gates (permissions, ACL, GraphQL, MFA). For host TLS / disk encryption / activity & chat data-at-rest assumptions, see Threat model & hosting.

Related docs

Host / data-at-rest posture: Threat model & hosting · surface → gate → Pest: AuthZ map · repo SECURITY.md · Roles & permissions · Passkeys · SSO / OIDC · Public CMS API · GraphQL · Outbound webhooks · Collections API · Operations · externa-bruno

PHP security notice (CVE-2026-109618 / CVE-2026-109619)

On 21 Oct 2026 PHP released emergency patches for two critical CVEs (CVSS v4.0 9.2): 8.4.27, 8.5.12, 8.3.36, and 8.2.35. Every Externa deployment runs PHP 8.4+ and is affected until upgraded — upgrading is the only full mitigation. Advisories: GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp. Operator summary: externa-core discussion #247. Keep PHP on the latest patch of your minor line; after upgrading, restart php-fpm and verify with php -v (CLI) plus FPM phpinfo() / status.

Must pass

  • [ ] PHP is on a supported, patched release (php -v ≥ 8.4.27 / 8.5.12; externa-core 1.2.2+ requires ^8.4.27) — discussion #247. Docker: re-pull ghcr.io/qiick-io/externa-core:1.2.2 / latest and recreate containers; source builds use --pull --no-cache
  • [ ] After a PHP upgrade on bare metal / Herd / Forge: restart php-fpm (or Herd’s PHP service) so workers load the patched binary — CLI php -v alone is not enough
  • [ ] php.ini hardening (not a substitute for patching; keep after patch): session.upload_progress.enabled = Off, session.use_strict_mode = 1; on PHP-FPM lock with php_admin_flag[session.upload_progress.enabled] = off so .user.ini cannot override — see Deployment — PHP runtime hardening
  • [ ] Body / multipart limits match the reverse proxy (post_max_size, upload_max_filesize, max_input_vars, max_input_nesting_level, max_file_uploads ↔ nginx client_max_body_size)
  • [ ] nginx does not set PHP_VALUE / PHP_ADMIN_VALUE via fastcgi_param (advisory exploitation path)
  • [ ] AuthZ surfaces match AuthZ map (admin Spatie, Public API / GraphQL collection+file ACL, AI tools + inbound HMAC, outbound secret, private files / read_private)
  • [ ] All routes/collections.php actions use Spatie permission: middleware (bulk via FormRequest)
  • [ ] Form requests for collections authorize with the same permissions
  • [ ] Pest: verified user without collection perms → 403 on schema/item writes (CollectionAuthorizationTest)
  • [ ] Pest: user with perms → OK; super-admin bypass still works
  • [ ] collection_permissions.rules.fields strip/deny unreadable/unwritable fields (REST + admin)
  • [ ] item_filter hides/blocks items that do not match
  • [ ] Roles UI can edit field ACL + item filter (Fields & item filter panel)
  • [ ] GraphQL /api/graphql uses ResolveApiAccess + same guards + throttle:api
  • [ ] Project settings / appearance gated by can-manage-project-settings
  • [ ] If two_factor_required is on in production, users without Fortify TOTP and without a passkey are forced to /settings/security with a clear required-MFA banner (TOTP or passkey satisfies; optional personal 2FA/passkeys still work when the setting is off) — decision table
  • [ ] Passkeys: php artisan migrate applied (passkeys table); APP_URL origin matches WebAuthn relying_party_id / allowed_origins; HTTPS in production (passkeys need a secure context; rest of CMS can be HTTP but prefer HTTPS); Fortify confirmPassword still on for security settings; no vendor forks/patches of Fortify or @laravel/passkeys; RP checks not widened — Passkeys
  • [ ] If OIDC/SSO is enabled: redirect URI is {APP_URL}/auth/oidc/callback; client secret encrypted (oidc_client_secret_configured); link only on IdP-verified email; JIT off unless intended; OIDC does not satisfy two_factor_required — SSO / OIDC
  • [ ] Smoke: register passkey on /settings/security, sign in with Sign in with passkey, Confirm with passkey on /user/confirm-password (HTTPS / secure context), and password confirm + TOTP paths still work (password confirm also on HTTP when the passkey CTA is hidden) — Confirm password with a passkey
  • [ ] No collections admin endpoint without Gate/permission middleware
  • [ ] (1.4.0) Admin responses carry X-Frame-Options, Content-Security-Policy, Referrer-Policy, Permissions-Policy (SECURITY_HEADERS_ENABLED=true); HSTS is set on the reverse proxy — Security headers
  • [ ] (1.4.0) AI_WRITE_TOOLS stays false unless you need AI writes; when on, only the needed tool classes are allowlisted and someone watches the approval inbox — AI tool approvals & audit
  • [ ] (1.4.0) If AI_MCP_ENABLED=true: MCP keys have an active owner, a non-admin role, narrow collection_slugs / files_scope, and mcp:write only where approved writes are planned — MCP server
  • [ ] (1.4.0) registration_enabled is off unless self-signup is intended (fresh installs default to off)

Should verify

  • [ ] Migrations applied: passkeys + collection_item_revisions + M2M junction object migration + user_created_id / user_updated_id on collections_items
  • [ ] Revisions restore creates a new revision (History UI)
  • [ ] M2M junction object shape documented for API consumers; item form preserves meta on save
  • [ ] Jobs monitor gated by can-show-jobs / can-manage-jobs
  • [ ] GraphQL query cache mode is opcache (or Redis), not database-serialized AST objects
  • [ ] Rate limit / CORS / optional cost·GraphQL·payload caps / API key scopes for /api/v1 and /api/graphql reviewed — API abuse controls
  • [ ] Production: set project public_api_allowed_origins (or explicit CORS_ALLOWED_ORIGINS); do not rely on Origin alone — use API key + IP for strong protection (Origin allowlist)
  • [ ] item_filter SQL pushdown: list meta.total / pages match filtered set (not post-paginate PHP)
  • [ ] Items Export CSV/JSON respects filters + cap (CollectionItemExportService)
  • [ ] TipTap wysiwyg: toolbar works; script paste stripped by WysiwygHtmlSanitizer
  • [ ] File Manager: dangerous extensions rejected (ForbiddenUploadExtension, including svg/html/htm and SSI/markup shtml/shtm/xml/xhtml/xht/mht/xsl/svgz from 1.3.0); MIME allowlist + magic bytes (MimeTypeValidator; SVG/JSON/XML out); name/metadata/tags HTML-stripped (PlainTextSanitizer) — Upload security
  • [ ] Docker: nginx /storage/ has nosniff + CSP sandbox (core PR #314) — Deploy with Docker
  • [ ] Effective-private files not reachable via unauthenticated /storage/assets/...; files:migrate-private-disk run after upgrade — Files configuration
  • [ ] Behind a reverse proxy: TRUSTED_PROXIES set and verified (client IP, HTTPS detection, per-IP rate limits, API key IP allowlists) — Reverse proxy
  • [ ] Super-admin escalation: only super-admins grant/manage super-admin principals; collection grants fail closed for ACL-scoped admins (core PRs #307, #312) — AuthZ map
  • [ ] File Manager UI strings resolve under files.* for en/it/de (shell locale)
  • [ ] Outbound webhook URL + secret both required for delivery; empty secret refuses queue/POST (no empty-key HMAC); secret not exposed in shared Inertia props; queue worker delivers DeliverOutboundWebhookJob — Outbound webhooks
  • [ ] SSRF: leave WEBHOOKS_ALLOW_PRIVATE_HOSTS=false unless a trusted LAN receiver needs it; private/reserved webhook & Slack URLs rejected on save + delivery — SSRF guard
  • [ ] AI collection-import webhook: AI_WEBHOOK_TOKEN set wherever the webhook is used (including local); clients send X-AI-Webhook-Signature (HMAC over the raw body only; 401 on failure or empty token in every environment — no unsigned bypass); CSRF exception only for that path — AI API
  • [ ] After rotating AI_WEBHOOK_TOKEN (ai:rotate-webhook-token), clear config cache / restart workers so grace + new secret load — Rotate

Dependency audits (CI)

GitHub Actions workflow audit (.github/workflows/audit.yml, core#184):

TriggerWhen
pull_request / pushPRs and pushes to develop / main / master
scheduleWeekly Monday 06:00 UTC (0 6 * * 1)
workflow_dispatchManual re-run

Jobs (blocking — no continue-on-error):

  • composer audit --locked — fails on high/critical (low/medium ignored for now)
  • npm audit --omit=dev --audit-level=high — production/runtime deps only

Policy is blocking from beta.3 onward (including GA). Never use npm audit fix --force in CI. Triage steps: Contributing — Dependency audits. Companion: Dependabot (#33). Report app vulns via SECURITY.md.

Code scanning & secrets

Maintainer hygiene for the public qiick-io/externa-core repo (not a substitute for the app gates above):

  • [ ] CodeQL workflow present (.github/workflows/codeql.yml) — JS/TS on PRs / default branches + weekly schedule (PHP when CodeQL pack supports it)
  • [ ] First-wave CodeQL alerts triaged (fix or dismiss with reason) under Security → Code scanning
  • [ ] Secret scanning enabled; push protection on (repo Settings → Code security, or org default)
  • [ ] No committed secrets; if push protection fires, rotate and remove from history

See SECURITY.md · Operations.

Pest smoke (PHP 8.4)

herd php artisan test --filter='CollectionAuthorization|CollectionPermissionRules|Graphql|AdvancedFilters|CollectionItemRevision|CollectionItemAudit|CollectionItemExport|WysiwygSanitize|JobMonitor|ForbiddenUploadExtension|PlainTextSanitizer|FilesLocaleParity|FileManager|PrivateDiskFiles|many to many accepts junction'
Previous
AuthZ map