Skip to content

Commit bb4cf56

Browse files
comprehensive security hardening
This release includes major security enhancements, addressing all remaining vulnerability findings from the recent penetration test report. * Security: Implement global CSRF protection for all AJAX requests via `ci4ms.js` prefilter and clear `csrfExcept` arrays. * Security: Hide PHP version disclosure by actively removing the `X-Powered-By` header. * Security: Enforce `cookie.secure = true` for session protection against MitM attacks. * Security: Replace plaintext passwords in DevGate configuration with Bcrypt hashes. * Security: Eliminate raw `$_SERVER` superglobal reads across core classes, replacing them with CI4 native helpers (BULGU-10). * Security: Implement strict file extension blacklisting (.php, .phtml, etc.) and boundary validation in Fileeditor to prevent RCE. * Security: Restrict DbBackup SQL restore with a strict statement whitelist to prevent malicious database manipulation. * Security: Upgrade HTMLPurifier to block base64 image data URIs and filter dangerous CSS properties. * Docs: Update CHANGELOG.md, README.md, and developer handbook with the new Security Architecture specifications.
1 parent a549dc2 commit bb4cf56

77 files changed

Lines changed: 1651 additions & 476 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) conventions adapted to the existing four-component version numbers.
66

7+
## [0.31.9.0] - 2026-05-01
8+
9+
### Security
10+
11+
- **CSRF Architecture Overhaul:** Implemented centralized `ajaxPrefilter` in `ci4ms.js` for automatic CSRF token injection on all AJAX requests. elFinder route exempted from CSRF via `MediaConfig::$csrfExcept` to prevent stale-token 403 errors during multi-request operations.
12+
- **HTMLPurifier Hardening:** Removed `data:` URI scheme from `AllowedSchemes` to block `data:text/html;base64` XSS bypass attacks (Base64 images use a custom placeholder mechanism). Disabled `CSS.Trusted` to filter dangerous CSS properties. Enabled `HTML.TargetBlank` for automatic `rel="noopener noreferrer"` on external links. Ensured Blog and Pages controllers always persist `CustomRules::getClean()` sanitized content to the database.
13+
- **IP Spoofing Fix:** Removed raw `$_SERVER['HTTP_X_FORWARDED_FOR']` and `$_SERVER['HTTP_CLIENT_IP']` reads from `BackendLogFilter`. Now relies solely on CI4's `$request->getIPAddress()` which respects `App.proxyIPs` config for trusted proxy detection.
14+
- **Raw `$_SERVER` Elimination:** Replaced all raw `$_SERVER['HTTP_HOST']`, `$_SERVER['HTTPS']`, and `$_SERVER['SERVER_NAME']` reads with CI4's `base_url()`, `site_url()`, and `parse_url()` helpers across `Email.php`, `Ci4ms.php`, `Install.php`, and `Settings.php`.
15+
- **Fileeditor RCE Prevention:** Added `$dangerousExtensions` blacklist (`.php`, `.phtml`, `.phar`, `.htaccess`, etc.) to block creating, writing, or renaming executable files via the file editor. Added `file_exists()` overwrite protection for `createFile` and `realpath` boundary validation for `renameFile`.
16+
- **SQL Restore Hardening:** Implemented a SQL statement whitelist (`INSERT`, `CREATE TABLE`, `DROP TABLE`, etc.) and dangerous command blacklist (`LOAD_FILE`, `INTO OUTFILE`, `GRANT`, `xp_cmdshell`, etc.) in `DbBackup::restore()`. Added path traversal protection requiring backup files to reside within `WRITEPATH`.
17+
- **Hardcoded Credentials:** Removed plaintext passwords from `DevGate` configuration. Implemented `bcrypt` hashed passwords and enabled `$useHashedPasswords` by default to protect developer credentials.
18+
19+
### Changed
20+
21+
- **DevGate CLI Sync:** `php spark ci4ms:setup` now automatically updates `DevGate.php` with the admin credentials provided during installation, matching the web installer's behaviour.
22+
- **Proxy Configuration:** Added comprehensive Cloudflare and Nginx reverse proxy configuration examples as comments in `App.php::$proxyIPs`.
23+
- **URI Schemes:** Removed unused `nntp` and `news` URI schemes from HTMLPurifier configuration.
24+
725
## [0.31.8.0] - 2026-04-19
826

927
### Fixed
@@ -293,6 +311,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
293311

294312
- Expanded database migrations and introduced new supporting libraries.
295313

314+
[0.31.8.1]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.8.1
296315
[0.31.8.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.8.0
297316
[0.31.7.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.7.0
298317
[0.31.6.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.6.0

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ CI4MS is a CodeIgniter 4-based CMS skeleton that delivers a production-ready, mo
55
[![Release](https://img.shields.io/github/v/release/ci4-cms-erp/ci4ms?style=for-the-badge&label=release)](https://github.com/ci4-cms-erp/ci4ms/releases)
66
[![License](https://img.shields.io/github/license/ci4-cms-erp/ci4ms?style=for-the-badge)](https://github.com/ci4-cms-erp/ci4ms/blob/main/LICENSE)
77
[![Build](https://img.shields.io/github/actions/workflow/status/ci4-cms-erp/ci4ms/docker-test.yml?style=for-the-badge&label=build)](https://github.com/ci4-cms-erp/ci4ms/actions)
8-
![PHP](https://img.shields.io/badge/PHP-%3E%3D8.1-777BB4?style=for-the-badge&logo=php&logoColor=white)
8+
![PHP](https://img.shields.io/badge/PHP-%3E%3D8.2-777BB4?style=for-the-badge&logo=php&logoColor=white)
99
![CodeIgniter](https://img.shields.io/badge/CodeIgniter-4.x-EF4223?style=for-the-badge&logo=codeigniter&logoColor=white)
1010
![Packagist Downloads](https://img.shields.io/packagist/dt/ci4-cms-erp/ci4ms?style=for-the-badge)
1111
[![Stars](https://img.shields.io/github/stars/ci4-cms-erp/ci4ms?style=for-the-badge)](https://github.com/ci4-cms-erp/ci4ms/stargazers)
@@ -22,6 +22,7 @@ CI4MS is a CodeIgniter 4-based CMS skeleton that delivers a production-ready, mo
2222
- **Flexible content management:** Page and blog entries include SEO metadata, categories, tags, and full comment workflows.
2323
- **Media & files:** Includes elFinder-powered media management, a built-in file editor, and an in-panel log viewer.
2424
- **Automatic Updates:** Modernized `UpdateService` provides a "One-Click Update" system with atomic file operations, automated GitHub version discovery (bypassing 300-file limits), and secure rollback management.
25+
- **Security Architecture:** Global CSRF protection across all AJAX endpoints, strict HTTP security headers (CSP, HSTS, X-Frame-Options), executable file upload blacklists, and HTMLPurifier sanitization to prevent XSS and RCE attacks.
2526
- **Backup Support:** Updates automatically trigger a full backup of modified files before applying patches, with a dedicated management interface for restores.
2627
- **Theme system:** The `public/templates/*` structure and the `Modules\Theme` module enable installing or upgrading themes from ZIP packages.
2728
- **Setup & automation:** Offers a web-based installer (`/install`) plus a single CLI command (`php spark ci4ms:setup`) for automated installation, default data seeding, and route generation. Module scaffolding is available via `php spark make:module`.
@@ -142,15 +143,15 @@ Key files:
142143
| Pages | Static page management | SEO fields, inline shortcode parsing |
143144
| Menu | Menu builder | Drag-and-drop ordering, slug helpers |
144145
| Media | Media manager | elFinder integration, optional WebP conversion |
145-
| Fileeditor | Project file editor | Safe read/write/rename/move/delete |
146+
| Fileeditor | Project file editor | Safe read/write/rename; dangerous extension blacklist |
146147
| Settings | System configuration | One-click updates, company/social/mail settings, i18n support |
147148
| Users | User & role management | Shield groups, reset tracking |
148149
| Methods | Route → permission mapping | Module toggling, router scan |
149150
| Logs | Log viewer | Browses CodeIgniter log files inside the backend |
150151
| ModulesInstaller | Module ZIP installer | Upload + cache invalidation |
151152
| Theme | Theme manager | ZIP upload, DB migration support, duplicate checks |
152153
| Install | Web installer | Creates `.env`, triggers migrations |
153-
| Backup | Database backup manager | Create, download, and restore backups |
154+
| Backup | Database backup manager | Create, download, and restore with SQL sanitization |
154155
| DashboardWidgets | Dashboard statistics | Modular widget system for admin overview |
155156
| LanguageManager | Language file manager | Edit and manage translation files from the backend |
156157

@@ -184,7 +185,7 @@ Standard CodeIgniter commands (`php spark db:seed`, `php spark key:generate`, et
184185
- `composer test` — runs PHPUnit.
185186
- The GitHub Actions workflow (`.github/workflows/docker-test.yaml`) automatically builds the Docker image and runs migrations on every push to `master`.
186187
- **Maintenance mode**: When `settings.maintenanceMode.scalar == 1`, the `Ci4ms` filter redirects visitors to `maintenance-mode`.
187-
- **Security**: `Fileeditor` and `Media` enforce `realpath` guards. Limit access in production environments.
188+
- **Security**: `Fileeditor` enforces `realpath` guards and a dangerous extension blacklist (`.php`, `.phtml`, `.phar`, `.htaccess`) to prevent RCE. `Backup` restore uses SQL statement whitelist to block malicious queries (`LOAD_FILE`, `GRANT`, etc.). `HTMLPurifier` config is hardened against XSS bypass (`data:` URIs blocked, `CSS.Trusted` disabled). All `$_SERVER` reads replaced with CI4 `base_url()`/`site_url()` helpers. Configure `App.php::$proxyIPs` if behind Cloudflare/Nginx.
188189

189190
## Additional Docs
190191

app/Config/App.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,38 @@ public function __construct()
212212
* '192.168.5.0/24' => 'X-Real-IP',
213213
* ]
214214
*
215+
* ── Cloudflare Example ──
216+
* If your site is behind Cloudflare, add their IP ranges below.
217+
* Full list: https://www.cloudflare.com/ips/
218+
*
219+
* [
220+
* '173.245.48.0/20' => 'CF-Connecting-IP',
221+
* '103.21.244.0/22' => 'CF-Connecting-IP',
222+
* '103.22.200.0/22' => 'CF-Connecting-IP',
223+
* '103.31.4.0/22' => 'CF-Connecting-IP',
224+
* '141.101.64.0/18' => 'CF-Connecting-IP',
225+
* '108.162.192.0/18' => 'CF-Connecting-IP',
226+
* '190.93.240.0/20' => 'CF-Connecting-IP',
227+
* '188.114.96.0/20' => 'CF-Connecting-IP',
228+
* '197.234.240.0/22' => 'CF-Connecting-IP',
229+
* '198.41.128.0/17' => 'CF-Connecting-IP',
230+
* '162.158.0.0/15' => 'CF-Connecting-IP',
231+
* '104.16.0.0/13' => 'CF-Connecting-IP',
232+
* '104.24.0.0/14' => 'CF-Connecting-IP',
233+
* '172.64.0.0/13' => 'CF-Connecting-IP',
234+
* '131.0.72.0/22' => 'CF-Connecting-IP',
235+
* ]
236+
*
237+
* ── Nginx Reverse Proxy Example ──
238+
* If your app runs behind a local Nginx proxy:
239+
*
240+
* [
241+
* '127.0.0.1' => 'X-Real-IP',
242+
* '10.0.0.0/8' => 'X-Forwarded-For',
243+
* '172.16.0.0/12' => 'X-Forwarded-For',
244+
* '192.168.0.0/16' => 'X-Forwarded-For',
245+
* ]
246+
*
215247
* @var array<string, string>
216248
*/
217249
public array $proxyIPs = [];

app/Config/ContentSecurityPolicy.php

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,33 @@ class ContentSecurityPolicy extends BaseConfig
5454
*
5555
* @var list<string>|string
5656
*/
57-
public $scriptSrc = 'self';
57+
public $scriptSrc = [
58+
'self',
59+
'unsafe-inline',
60+
'unsafe-eval'
61+
];
5862

5963
/**
6064
* Lists allowed stylesheets' URLs.
6165
*
6266
* @var list<string>|string
6367
*/
64-
public $styleSrc = 'self';
68+
public $styleSrc = [
69+
'self',
70+
'unsafe-inline',
71+
'https://fonts.googleapis.com'
72+
];
6573

6674
/**
6775
* Defines the origins from which images can be loaded.
6876
*
6977
* @var list<string>|string
7078
*/
71-
public $imageSrc = 'self';
79+
public $imageSrc = [
80+
'self',
81+
'data:',
82+
'https://*'
83+
];
7284

7385
/**
7486
* Restricts the URLs that can appear in a page's `<base>` element.
@@ -99,7 +111,11 @@ class ContentSecurityPolicy extends BaseConfig
99111
*
100112
* @var list<string>|string
101113
*/
102-
public $fontSrc;
114+
public $fontSrc = [
115+
'self',
116+
'https://fonts.gstatic.com',
117+
'data:'
118+
];
103119

104120
/**
105121
* Lists valid endpoints for submission from `<form>` tags.
@@ -172,5 +188,5 @@ class ContentSecurityPolicy extends BaseConfig
172188
/**
173189
* Replace nonce tag automatically
174190
*/
175-
public bool $autoNonce = true;
191+
public bool $autoNonce = false;
176192
}

app/Config/Email.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,9 @@ public function __construct()
139139
}
140140

141141
if (isset($settings->mail)) {
142-
$this->fromEmail = 'noreply@' . $_SERVER['HTTP_HOST'];
143-
$this->fromName = 'noreply@' . $_SERVER['HTTP_HOST'];
142+
$host = parse_url(base_url(), PHP_URL_HOST) ?: 'localhost';
143+
$this->fromEmail = 'noreply@' . $host;
144+
$this->fromName = 'noreply@' . $host;
144145
$this->recipients = $settings->mail->recipients ?? '';
145146

146147
$mailConfig = $settings->mail;

app/Controllers/BaseController.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@
2121
* class Home extends BaseController
2222
*
2323
* For security be sure to declare any new methods as protected or private.
24+
*
25+
* @package App\Controllers
26+
*
27+
* @property \CodeIgniter\HTTP\IncomingRequest $request
28+
* @property \CodeIgniter\HTTP\Response $response
29+
* @property \Psr\Log\LoggerInterface $logger
30+
* @property \CodeIgniter\Session\Session $session
31+
* @property \CodeIgniter\Validation\ValidationInterface $validator
32+
* @property \ci4commonmodel\CommonModel $commonModel
33+
* @property \Modules\LanguageManager\Libraries\TranslationService $translationService
2434
*/
2535
abstract class BaseController extends Controller
2636
{
@@ -177,7 +187,7 @@ protected function getDefaultData(): array
177187
'alternateLinks' => [], // Default empty, filled by child controllers
178188
'seoConfig' => new Seo()
179189
];
180-
if(is_cli()) $defData['agent']='CLI';
190+
if (is_cli()) $defData['agent'] = 'CLI';
181191
else $defData['agent'] = $this->request->getUserAgent();
182192

183193
// If languages are empty in cache, load them (fallback)

app/Filters/Ci4ms.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ public function __construct()
3535
public function before(RequestInterface $request, $arguments = null)
3636
{
3737
if (!file_exists(ROOTPATH . '.env')) {
38-
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https://" : "http://";
39-
return redirect()->to($protocol . $_SERVER['SERVER_NAME'] . '/install');
38+
return redirect()->to(site_url('install'));
4039
}
4140
if ((bool)cache()->get('settings')['maintenanceMode']->scalar === true) return redirect()->route('maintenance-mode');
4241
}

app/Views/templates/default/base.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<link href="<?php echo base_url('templates/default/assets/ci4ms.css') ?>" rel="stylesheet" />
2525
<?php endif; ?>
2626
<link href="<?php echo base_url('templates/default/assets/modern-ci4ms.css') ?>" rel="stylesheet" />
27-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
27+
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
2828

2929
<link href="<?php echo base_url('be-assets/plugins/jquery-ui/jquery-ui.min.css') ?>" rel="stylesheet" />
3030
<?php $isMulti = ($settings->siteLanguageMode ?? 'single') === 'multi';

composer.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,18 @@
1717
"bertugfahriozer/sql2migration": "^1.0",
1818
"ci4-cms-erp/ext_module_generator": "^1.1",
1919
"claviska/simpleimage": "4.4.0",
20-
"codeigniter4/framework": "4.7.1",
20+
"codeigniter4/framework": "4.7.2",
2121
"codeigniter4/settings": "2.2",
2222
"codeigniter4/shield": "1.3.0",
23-
"codeigniter4/translations": "4.7.0",
23+
"codeigniter4/translations": "4.7.2",
2424
"ezyang/htmlpurifier": "4.19",
2525
"gregwar/captcha": "1.3.0",
2626
"studio-42/elfinder": "2.1.67"
2727
},
2828
"require-dev": {
2929
"fakerphp/faker": "^1.9",
3030
"mikey179/vfsstream": "^1.6",
31+
"phpstan/phpstan": "^2.1",
3132
"phpunit/phpunit": "^10.5.16"
3233
},
3334
"autoload": {
@@ -54,5 +55,7 @@
5455
},
5556
"scripts": {
5657
"test": "phpunit"
57-
}
58+
},
59+
"minimum-stability": "dev",
60+
"prefer-stable": true
5861
}

0 commit comments

Comments
 (0)