Skip to content

Security Hardening

github-actions[bot] edited this page Aug 19, 2026 · 2 revisions

One-sentence purpose: the checklist an operator should work through before exposing a ci4ms install to the internet — closing the open registration endpoint, confirming production mode, understanding captcha and DevGate, and applying web-server-level hardening.

English

This is the most important page in this wiki. Read it fully before going to production.

Closing the open /backend/register endpoint

Status: left open by product decision, not a bug you need to report. By default, /backend/register accepts requests from unauthenticated visitors and lets them create an account in the backend user table with no email verification. The account lands in Shield's default group, which ships with zero permissions, so it cannot reach any backend/* screen behind the permission filter — but it is still an unauthenticated write to the users / auth_identities tables and a spam/foothold surface. Nothing in the shipped codebase closes it automatically; if you want it closed, you have two independent options, verified against source. They solve different problems — pick based on what you need.

Option 1 — Auth.allowRegistration = false (functional kill-switch)

File: modules/Auth/Config/Auth.php

public bool $allowRegistration = false;

The check lives inside the controller (RegisterController), so it holds regardless of how the controller is reached — this route, a future route pointing at the same controller, or anything else. The route still exists and still responds (a redirect with a flash error, not a 403/404), but no row is ever written to users.

Option 2 — remove the route

File: modules/Auth/Config/Routes.php

service('auth')->routes($routes, ['namespace' => 'Modules\Auth\Controllers', 'except' => ['register']]);

This drops the route entirely — /backend/register returns a plain 404, before the controller (and therefore before the allowRegistration check) ever runs. It's route-table-only: it does not touch allowRegistration, so if a later change adds another route or call path to RegisterController, registration works again on that new path.

Applying both is the strongest option: the route 404s directly, and even if a route to the controller reappears later, allowRegistration = false still blocks it. The full technical comparison (enforcement point, response codes, what each option depends on) is in docs/web-server-hardening.md — read it before deciding.

Confirm the production environment

The web installer writes CI_ENVIRONMENT = production into .env automatically during setup. Still, confirm it directly in .env after installation — this matters because running in development mode enables the debug toolbar and detailed stack traces, which leak internal application details to anyone who can reach the site. See Configuration for where this lives.

Captcha

Login (and comment) captcha checks are controlled by the Auth.captchaBypassInDevelopment setting. The bypass only ever applies when both ENVIRONMENT === 'development' and the setting is enabled — the default seeded value is false, so captcha is enforced even in development unless you explicitly turn the bypass on. In production, ENVIRONMENT is never 'development', so the bypass path can never trigger regardless of the setting — captcha is effectively always required.

DevGate

DevGate is a separate, development-only Basic-Auth gate in front of the whole site (modules/DevGate/), unrelated to your admin account. It has zero effect once CI_ENVIRONMENT is anything other than development — nothing runs in production.

Both installation paths — the web installer and php spark ci4ms:setup — provision a fresh, independent DevGate credential automatically (reusing your admin username, but with its own generated, hashed password — never your admin password, so a DevGate credential leak cannot compromise the admin account). This password is shown exactly once — on a one-time disclosure page after the web installer, or printed at the end of the CLI setup run — and cannot be recovered afterward. Save it immediately. If you didn't save it, or if DevGate's config file couldn't be updated (e.g. not writable), you can generate a new hash yourself:

php -r "echo password_hash('yourpassword', PASSWORD_BCRYPT);"

...and paste it into modules/DevGate/Config/DevGate.php. See the DevGate module README for full configuration details (plaintext vs. hashed passwords, excluding paths).

Audit notifications

Privilege-affecting actions raise a ci4ms.audit event that is delivered to the superadmin group as an in-app notification (bell dropdown). You do not have to switch this on — it is wired by default.

Two severities reach you:

  • critical — a rejected privilege-escalation attempt. This fires when an account holding a permission-editing permission tries to grant itself or another user more access than it holds. The request is refused either way; the notification is how you find out it was made. These cannot be muted: the per-user opt-out screen deliberately cannot filter critical events.
  • warning — a successful privilege-affecting change: a group created or updated, a user's groups or password changed, a permission page added or edited, a database backup restored (including a count of the RBAC statements the restore refused to replay), a migration or seed run, a file edited through the backend file editor.

Two things follow from this for an operator. First, a critical notification is worth investigating even though the attempt failed — it means an account with backend access is probing the delegation ceiling. Second, a burst of warning events you did not initiate is the earliest signal you get that a backend account has been taken over.

Superadmins can review the full list in the bell dropdown; notifications are also what Backup::restore() uses to tell you that RBAC rows in a restored dump were skipped rather than applied.

Web-server hardening

Beyond application-level settings, ci4ms ships defense-in-depth .htaccess rules that block PHP execution inside upload directories (public/templates/, public/media/, public/uploads/) — but these only work on Apache. If you deploy on nginx, Caddy, or FrankenPHP, .htaccess is silently ignored and you must add the equivalent rules to your server config manually. docs/web-server-hardening.md covers:

  • The exact nginx location blocks to mirror the Apache rules
  • A known FlyEnv/PhpWebStudy PHP-location misconfiguration that can allow path-info-based execution tricks (/media/photo.jpg/x.php), and how to close it
  • HSTS, X-Frame-Options, Referrer-Policy, and other production response headers
  • A curl command to verify uploaded payloads genuinely cannot execute after deploy

Read this document if you deploy on anything other than Apache.

Reporting a vulnerability

If you find a security vulnerability in ci4ms, do not open a public issue. Report it by email as described in SECURITY.md. Past reporters are credited in the README's Security Hall of Fame.

Türkçe

Bu, wiki'deki en önemli sayfadır. Production'a çıkmadan önce baştan sona okuyun.

Açık /backend/register endpoint'ini kapatma

Durum: ürün kararı olarak açık bırakıldı, rapor etmeniz gereken bir hata değil. Varsayılan olarak /backend/register, kimliği doğrulanmamış ziyaretçilerden gelen istekleri kabul eder ve e-posta doğrulaması olmadan backend kullanıcı tablosunda bir hesap oluşturmalarına izin verir. Hesap, sıfır izinle gelen Shield'in varsayılan grubuna düşer, bu yüzden izin filtresinin arkasındaki hiçbir backend/* ekranına ulaşamaz — ama yine de users / auth_identities tablolarına kimliksiz bir yazma işlemi ve bir spam/dayanak noktası yüzeyidir. Mevcut kod tabanında bunu otomatik kapatan hiçbir şey yoktur; kapatmak isterseniz, kaynak koddan doğrulanmış iki bağımsız seçeneğiniz vardır. Farklı sorunları çözerler — ihtiyacınıza göre seçin.

Seçenek 1 — Auth.allowRegistration = false (fonksiyonel kapatma anahtarı)

Dosya: modules/Auth/Config/Auth.php

public bool $allowRegistration = false;

Kontrol controller'ın içinde (RegisterController) yaşar, bu yüzden controller'a nasıl ulaşılırsa ulaşılsın geçerlidir — bu route, aynı controller'a işaret eden gelecekteki bir route veya başka herhangi bir yol. Route hâlâ var olur ve hâlâ yanıt verir (403/404 değil, flash hata mesajlı bir redirect), ama users tablosuna asla satır yazılmaz.

Seçenek 2 — route'u kaldırma

Dosya: modules/Auth/Config/Routes.php

service('auth')->routes($routes, ['namespace' => 'Modules\Auth\Controllers', 'except' => ['register']]);

Bu, route'u tamamen kaldırır — /backend/register, controller çalışmadan (dolayısıyla allowRegistration kontrolü hiç işletilmeden) önce düz bir 404 döner. Yalnızca route tablosuna özeldir: allowRegistration'a dokunmaz, bu yüzden ileride RegisterController'a işaret eden başka bir route veya çağrı yolu eklenirse, register o yeni yolda tekrar çalışır.

İkisini birden uygulamak en güçlü seçenektir: route doğrudan 404 verir, ve ileride controller'a giden bir route yeniden ortaya çıksa bile allowRegistration = false onu yine engeller. Tam teknik karşılaştırma (uygulama noktası, yanıt kodları, her seçeneğin neye bağlı olduğu) docs/web-server-hardening.md içindedir — karar vermeden önce okuyun.

Production ortamını teyit edin

Web installer, kurulum sırasında .env'e otomatik olarak CI_ENVIRONMENT = production yazar. Yine de kurulumdan sonra bunu .env içinde doğrudan teyit edin — çünkü development modunda çalışmak debug toolbar'ı ve ayrıntılı stack trace'leri etkinleştirir, bu da siteye ulaşabilen herkese uygulamanın iç detaylarını sızdırır. Bunun nerede olduğu için Yapılandırma sayfasına bakın.

Captcha

Login (ve yorum) captcha kontrolleri Auth.captchaBypassInDevelopment ayarıyla kontrol edilir. Bypass yalnızca hem ENVIRONMENT === 'development' hem de ayar etkinleştirilmişse devreye girer — seed edilen varsayılan değer false'tur, yani bypass'ı açıkça açmadığınız sürece development ortamında bile captcha zorunludur. Production'da ENVIRONMENT asla 'development' olmayacağından, ayarın değeri ne olursa olsun bypass yolu hiçbir zaman tetiklenemez — captcha fiilen her zaman zorunludur.

DevGate

DevGate, tüm sitenin önünde ayrı, yalnızca geliştirme ortamı için bir Basic-Auth kapısıdır (modules/DevGate/), admin hesabınızla ilgisi yoktur. CI_ENVIRONMENT, development dışında herhangi bir değer olduğunda hiçbir etkisi yoktur — production'da hiçbir şey çalışmaz.

Her iki kurulum yolu da — web installer ve php spark ci4ms:setup — otomatik olarak yeni ve bağımsız bir DevGate kimlik bilgisi üretir (admin kullanıcı adınızı yeniden kullanır, ama kendi üretilmiş, hash'lenmiş şifresiyle — asla admin şifreniz değildir, bu yüzden bir DevGate kimlik bilgisi sızıntısı admin hesabını tehlikeye atamaz). Bu şifre yalnızca bir kez gösterilir — web installer'dan sonra tek seferlik bir açıklama sayfasında, ya da CLI setup çalışmasının sonunda yazdırılır — ve sonrasında geri alınamaz. Hemen kaydedin. Kaydetmediyseniz veya DevGate'in config dosyası güncellenemediyse (örneğin yazılabilir değilse), kendiniz yeni bir hash üretebilirsiniz:

php -r "echo password_hash('yourpassword', PASSWORD_BCRYPT);"

...ve bunu modules/DevGate/Config/DevGate.php içine yapıştırın. Tam yapılandırma detayları (düz metin ve hash'lenmiş şifreler, yol hariç tutma) için DevGate modülü README dosyasına bakın.

Denetim bildirimleri

Yetkiyi etkileyen işlemler, superadmin grubuna uygulama içi bildirim (zil menüsü) olarak iletilen bir ci4ms.audit olayı üretir. Bunu açmanız gerekmez — varsayılan olarak bağlıdır.

Size iki önem derecesi ulaşır:

  • critical — reddedilmiş bir yetki yükseltme denemesi. İzin düzenleme yetkisi olan bir hesap, kendisine veya başka bir kullanıcıya sahip olduğundan fazla yetki vermeye çalıştığında tetiklenir. İstek her hâlükârda reddedilir; bildirim, denemenin yapıldığını öğrenme yolunuzdur. Bunlar susturulamaz: kullanıcı bazlı bildirim kapatma ekranı critical olayları bilinçli olarak filtreleyemez.
  • warning — yetkiyi etkileyen başarılı bir değişiklik: grup oluşturma veya güncelleme, bir kullanıcının gruplarının veya parolasının değişmesi, izin sayfası ekleme/düzenleme, veritabanı yedeği geri yükleme (geri yüklemenin uygulamayı reddettiği RBAC ifadelerinin sayısı dahil), migration veya seed koşusu, backend dosya düzenleyicisinden dosya değişikliği.

Operatör açısından bundan iki sonuç çıkar. Birincisi, deneme başarısız olsa bile bir critical bildirimi incelemeye değer — backend erişimi olan bir hesabın delegasyon tavanını yokladığı anlamına gelir. İkincisi, sizin başlatmadığınız bir warning yığını, bir backend hesabının ele geçirildiğine dair alacağınız en erken sinyaldir.

Superadmin'ler tam listeyi zil menüsünden inceleyebilir; Backup::restore() de geri yüklenen bir dump içindeki RBAC satırlarının uygulanmayıp atlandığını size bu bildirimlerle söyler.

Web-server sertleştirme

Uygulama seviyesindeki ayarların ötesinde, ci4ms yükleme dizinleri içinde (public/templates/, public/media/, public/uploads/) PHP çalıştırmayı engelleyen derinlemesine savunma .htaccess kuralları ile gelir — ama bunlar yalnızca Apache'de çalışır. nginx, Caddy veya FrankenPHP üzerinde deploy ediyorsanız .htaccess sessizce yok sayılır ve eşdeğer kuralları sunucu yapılandırmanıza elle eklemeniz gerekir. docs/web-server-hardening.md şunları kapsar:

  • Apache kurallarının aynısını sağlayan tam nginx location blokları
  • FlyEnv/PhpWebStudy'de bilinen bir PHP-location yanlış yapılandırması, path-info tabanlı çalıştırma hilelerine (/media/photo.jpg/x.php) izin verebilir — nasıl kapatılacağı dahil
  • HSTS, X-Frame-Options, Referrer-Policy ve diğer production yanıt başlıkları
  • Deploy sonrası yüklenen payload'ların gerçekten çalıştırılamadığını doğrulamak için bir curl komutu

Apache dışında herhangi bir şey üzerine deploy ediyorsanız bu dokümanı okuyun.

Güvenlik açığı bildirimi

ci4ms'te bir güvenlik açığı bulursanız, herkese açık bir issue açmayın. SECURITY.md içinde açıklandığı şekilde e-posta ile bildirin. Geçmiş bildirenler README'nin Security Hall of Fame bölümünde onurlandırılır.