Three small hardening fixes found during a self-review security pass over the #40 changes, applied on top of that already-merged branch:
cms/bootstrap.php: display_errors/display_startup_errors are now explicitly disabled (errors still reach PHP's own error log via log_errors). Previously an uncaught engine-level error raised before CMSController's try/catch could render a stack trace with filesystem paths straight to the client, depending on the host's php.ini defaults.
cms/src/NeoCMS/LoginRateLimiter.php: the login-attempt state file now caps tracked address/identity buckets at 20,000, evicting the least recently active first. Previously a distributed flood of distinct addresses or login names (each staying under the per-bucket threshold) could grow login-attempts.json without bound.
cms/src/NeoCMS/Logger.php: expired audit logs are now pruned probabilistically (1-in-100 requests) instead of on every single request. A Logger is constructed on every request, and the previous unconditional prune did a full directory scan + filemtime() per rotated log file every time — the same approach PHP's own session garbage collector uses (session.gc_probability/gc_divisor).
No behaviour change for normal use; all three are defence-in-depth against low-probability/high-volume scenarios.
## Summary
Three small hardening fixes found during a self-review security pass over the #40 changes, applied on top of that already-merged branch:
- **`cms/bootstrap.php`**: `display_errors`/`display_startup_errors` are now explicitly disabled (errors still reach PHP's own error log via `log_errors`). Previously an uncaught engine-level error raised before `CMSController`'s try/catch could render a stack trace with filesystem paths straight to the client, depending on the host's `php.ini` defaults.
- **`cms/src/NeoCMS/LoginRateLimiter.php`**: the login-attempt state file now caps tracked address/identity buckets at 20,000, evicting the least recently active first. Previously a distributed flood of distinct addresses or login names (each staying under the per-bucket threshold) could grow `login-attempts.json` without bound.
- **`cms/src/NeoCMS/Logger.php`**: expired audit logs are now pruned probabilistically (1-in-100 requests) instead of on every single request. A `Logger` is constructed on every request, and the previous unconditional prune did a full directory scan + `filemtime()` per rotated log file every time — the same approach PHP's own session garbage collector uses (`session.gc_probability`/`gc_divisor`).
No behaviour change for normal use; all three are defence-in-depth against low-probability/high-volume scenarios.
## Test plan
- [x] `php -l` on all three changed files
- [x] `php tests/run.php` passes (full suite, unchanged)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Three small hardening fixes found during a self-review security pass over the #40 changes, applied on top of that already-merged branch:
cms/bootstrap.php:display_errors/display_startup_errorsare now explicitly disabled (errors still reach PHP's own error log vialog_errors). Previously an uncaught engine-level error raised beforeCMSController's try/catch could render a stack trace with filesystem paths straight to the client, depending on the host'sphp.inidefaults.cms/src/NeoCMS/LoginRateLimiter.php: the login-attempt state file now caps tracked address/identity buckets at 20,000, evicting the least recently active first. Previously a distributed flood of distinct addresses or login names (each staying under the per-bucket threshold) could growlogin-attempts.jsonwithout bound.cms/src/NeoCMS/Logger.php: expired audit logs are now pruned probabilistically (1-in-100 requests) instead of on every single request. ALoggeris constructed on every request, and the previous unconditional prune did a full directory scan +filemtime()per rotated log file every time — the same approach PHP's own session garbage collector uses (session.gc_probability/gc_divisor).No behaviour change for normal use; all three are defence-in-depth against low-probability/high-volume scenarios.
Test plan
php -lon all three changed filesphp tests/run.phppasses (full suite, unchanged)🤖 Generated with Claude Code