Skip to content

Commit e1aad28

Browse files
security: patch Stored XSS in Blog/Pages and Fileeditor destructive ops bypass (reported by offset)
- Fix Stored XSS in Blog content: CustomRules::getClean() output now persisted on both create and update flows in Blog.php controller - Fix Stored XSS in Pages content: identical html_purify bypass remediated in Pages.php controller for create and update endpoints - Fix Fileeditor destructive operations extension bypass: dangerous-extension allowlist check extended to deleteFileOrFolder and renameFile operations so critical files (.env, composer.json, etc.) cannot be renamed or deleted - Update CHANGELOG.md: add three security entries to [0.31.9.0] block, correct release date to 2026-05-08, add [0.31.9.0] reference link - Update README.md: expand Security note with new Fileeditor and XSS details; update offset Hall of Fame entry with full vulnerability list (Apr-May 2026)
1 parent bb4cf56 commit e1aad28

90 files changed

Lines changed: 4198 additions & 3139 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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ 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
7+
## [0.31.9.0] - 2026-05-08
88

99
### Security
1010

@@ -15,6 +15,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
1515
- **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`.
1616
- **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`.
1717
- **Hardcoded Credentials:** Removed plaintext passwords from `DevGate` configuration. Implemented `bcrypt` hashed passwords and enabled `$useHashedPasswords` by default to protect developer credentials.
18+
- **Stored XSS — Blog Content (reported by offset):** The `html_purify` custom validation rule was applied to the Blog content field but did not enforce sanitization during update operations, allowing authenticated authors to persist malicious scripts. Fixed by ensuring `CustomRules::getClean()` is invoked and its output persisted on both `create` and `update` flows in `Blog.php` controller.
19+
- **Stored XSS — Pages Content (reported by offset):** Identical bypass in the Pages module: the `html_purify` rule ran validation but the raw unsanitized value was written to the database on update. Fixed by enforcing `CustomRules::getClean()` output persistence in `Pages.php` controller for both creation and editing endpoints.
20+
- **Fileeditor Destructive Operations Extension Bypass (reported by offset):** The dangerous-extension blacklist was only enforced on `createFile`, `saveFile`, and `renameFile` but not on `deleteFileOrFolder` and `renameFile` when the target was a critical application file (e.g. `.env`, `composer.json`). Added an explicit extension allowlist check to all destructive operations (`deleteFileOrFolder`, `renameFile`) so that renaming or deleting files with critical extensions is blocked regardless of the operation type.
1821

1922
### Changed
2023

@@ -311,6 +314,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
311314

312315
- Expanded database migrations and introduced new supporting libraries.
313316

317+
[0.31.9.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.9.0
314318
[0.31.8.1]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.8.1
315319
[0.31.8.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.8.0
316320
[0.31.7.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.7.0

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ Standard CodeIgniter commands (`php spark db:seed`, `php spark key:generate`, et
185185
- `composer test` — runs PHPUnit.
186186
- The GitHub Actions workflow (`.github/workflows/docker-test.yaml`) automatically builds the Docker image and runs migrations on every push to `master`.
187187
- **Maintenance mode**: When `settings.maintenanceMode.scalar == 1`, the `Ci4ms` filter redirects visitors to `maintenance-mode`.
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.
188+
- **Security**: `Fileeditor` enforces `realpath` guards and a dangerous extension blacklist (`.php`, `.phtml`, `.phar`, `.htaccess`) to prevent RCE; destructive operations (`deleteFileOrFolder`, `renameFile`) additionally validate against an extension allowlist to block renaming or deleting critical application files. `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) and `CustomRules::getClean()` output is persisted on every `create` and `update` flow in Blog and Pages controllers to prevent Stored XSS. All `$_SERVER` reads replaced with CI4 `base_url()`/`site_url()` helpers. Configure `App.php::$proxyIPs` if behind Cloudflare/Nginx.
189189

190190
## Additional Docs
191191

@@ -209,7 +209,7 @@ A huge thank you to the security researchers who have helped make **ci4ms** more
209209
| **[Hunter.](https://github.com/LAW6ZX7)** | Identified Critical Stored XSS in Backend & Blog modules allowing Session Hijacking. | Feb 2026 |
210210
| **[m1scher](https://github.com/m1scher)** | Assisted with vulnerability triaging and security testing. | Feb 2026 |
211211
| **[alpernae](https://github.com/alpernae)** | Assisted with vulnerability triaging and security testing. | Feb 2026 |
212-
| **[offset](https://github.com/offset)** | Identified Critical vulnerabilities including multiple Stored XSS, Authorization Bypass in Fileeditor, Install Guard Bypass, and CRLF Injection. | Apr 2026 |
212+
| **[offset](https://github.com/offset)** | Identified Critical vulnerabilities including multiple Stored XSS (Blog & Pages content via broken `html_purify` validation), Authorization Bypass in Fileeditor destructive operations (delete/rename extension allowlist missing), Install Guard Bypass, and CRLF Injection. | Apr – May 2026 |
213213
| **[fg0x0](https://github.com/fg0x0)** | Identified Critical Arbitrary File Write (Zip Slip RCE) vulnerabilities in Theme::upload and Backup::restore modules. | Apr 2026 |
214214
| **[0xAlchemist](https://github.com/bugmithlegend)** , **[peeefour](https://github.com/peeefour)** and **[DexterHK](https://github.com/DexterHK)** | Identified Critical Full Account Takeover and Privilege Escalation via Stored DOM Blind XSS in Backup Management (v2). | Apr 2026 |
215215
| **[dapickle](https://github.com/dapickle)** | Identified Critical Authenticated RCE in Theme installation, Arbitrary Database Table Drop in Theme module, and a Session Management Bypass. | Apr 2026 |

app/Common.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
* @link: https://codeigniter4.github.io/CodeIgniter4/
1515
*/
1616

17-
use CodeIgniter\I18n\Time;
18-
1917
if (!function_exists('clearFilter')) {
2018
/**
2119
* @param array $array
@@ -160,7 +158,7 @@ function menu($navigations, $parent = null)
160158
}
161159
$title = (str_starts_with($menu->title, 'Frontend.')) ? lang($menu->title) : esc($menu->title);
162160
echo '>' . $title . '</a>';
163-
161+
164162
if ((bool)$menu->hasChildren === true) {
165163
$menuClass = empty($menu->parent) ? 'dropdown-menu dropdown-menu-end' : 'dropdown-menu';
166164
echo '<ul class="' . $menuClass . '" aria-labelledby="' . $dropdownId . '">';

app/Config/ContentSecurityPolicy.php

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ class ContentSecurityPolicy extends BaseConfig
3030
*/
3131
public ?string $reportURI = null;
3232

33+
/**
34+
* Specifies a reporting endpoint to which violation reports ought to be sent.
35+
*/
36+
public ?string $reportTo = null;
37+
3338
/**
3439
* Instructs user agents to rewrite URL schemes, changing
3540
* HTTP to HTTPS. This directive is for websites with
@@ -38,12 +43,12 @@ class ContentSecurityPolicy extends BaseConfig
3843
public bool $upgradeInsecureRequests = false;
3944

4045
// -------------------------------------------------------------------------
41-
// Sources allowed
46+
// CSP DIRECTIVES SETTINGS
4247
// NOTE: once you set a policy to 'none', it cannot be further restricted
4348
// -------------------------------------------------------------------------
4449

4550
/**
46-
* Will default to self if not overridden
51+
* Will default to `'self'` if not overridden
4752
*
4853
* @var list<string>|string|null
4954
*/
@@ -54,32 +59,62 @@ class ContentSecurityPolicy extends BaseConfig
5459
*
5560
* @var list<string>|string
5661
*/
57-
public $scriptSrc = [
62+
public $scriptSrc = 'self';
63+
64+
/**
65+
* Specifies valid sources for JavaScript <script> elements.
66+
*
67+
* @var list<string>|string
68+
*/
69+
public array|string $scriptSrcElem = 'self';
70+
71+
/**
72+
* Specifies valid sources for JavaScript inline event
73+
* handlers and JavaScript URLs.
74+
*
75+
* @var list<string>|string
76+
*/
77+
public array|string $scriptSrcAttr = 'self';
78+
79+
/**
80+
* Lists allowed stylesheets' URLs.
81+
*
82+
* @var list<string>|string
83+
*/
84+
public array|string $styleSrc = [
5885
'self',
5986
'unsafe-inline',
60-
'unsafe-eval'
87+
'https://fonts.googleapis.com',
6188
];
6289

6390
/**
64-
* Lists allowed stylesheets' URLs.
91+
* Specifies valid sources for stylesheets <link> elements.
6592
*
6693
* @var list<string>|string
6794
*/
68-
public $styleSrc = [
95+
public array|string $styleSrcElem = [
6996
'self',
70-
'unsafe-inline',
71-
'https://fonts.googleapis.com'
97+
'https://fonts.googleapis.com',
7298
];
7399

100+
/**
101+
* Specifies valid sources for stylesheets inline
102+
* style attributes and `<style>` elements.
103+
*
104+
* @var list<string>|string
105+
*/
106+
public array|string $styleSrcAttr = ['self', 'unsafe-inline'];
107+
74108
/**
75109
* Defines the origins from which images can be loaded.
76110
*
77111
* @var list<string>|string
78112
*/
79-
public $imageSrc = [
113+
public array|string $imageSrc = [
80114
'self',
81-
'data:',
82-
'https://*'
115+
'data:', // base64 embedded images (elFinder thumbnails, captcha)
116+
'blob:', // elFinder file preview blobs
117+
'https:', // any HTTPS image (tightened from wildcard https://* to scheme-only)
83118
];
84119

85120
/**
@@ -96,7 +131,7 @@ class ContentSecurityPolicy extends BaseConfig
96131
*
97132
* @var list<string>|string
98133
*/
99-
public $childSrc = 'self';
134+
public array|string $childSrc = ['self', 'blob:', 'data:'];
100135

101136
/**
102137
* Limits the origins that you can connect to (via XHR,
@@ -111,10 +146,11 @@ class ContentSecurityPolicy extends BaseConfig
111146
*
112147
* @var list<string>|string
113148
*/
114-
public $fontSrc = [
149+
public array|string $fontSrc = [
115150
'self',
116151
'https://fonts.gstatic.com',
117-
'data:'
152+
'https://fonts.gstatic.com/*',
153+
'data:',
118154
];
119155

120156
/**
@@ -161,6 +197,11 @@ class ContentSecurityPolicy extends BaseConfig
161197
*/
162198
public $manifestSrc;
163199

200+
/**
201+
* @var list<string>|string
202+
*/
203+
public array|string $workerSrc = [];
204+
164205
/**
165206
* Limits the kinds of plugins a page may invoke.
166207
*
@@ -176,17 +217,17 @@ class ContentSecurityPolicy extends BaseConfig
176217
public $sandbox;
177218

178219
/**
179-
* Nonce tag for style
220+
* Nonce placeholder for style tags.
180221
*/
181-
public string $styleNonceTag = '{csp-style-nonce}';
222+
public string $styleNonceTag = '<?php echo csp_style_nonce(); ?>';
182223

183224
/**
184-
* Nonce tag for script
225+
* Nonce placeholder for script tags.
185226
*/
186-
public string $scriptNonceTag = '{csp-script-nonce}';
227+
public string $scriptNonceTag = '<?php echo csp_script_nonce(); ?>';
187228

188229
/**
189-
* Replace nonce tag automatically
230+
* Replace nonce tag automatically?
190231
*/
191-
public bool $autoNonce = false;
232+
public bool $autoNonce = true;
192233
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Config\templates\default;
4+
5+
use CodeIgniter\Config\BaseConfig;
6+
7+
class DefaultConfig extends BaseConfig
8+
{
9+
public $csrfExcept = [
10+
'forms/searchForm',
11+
'commentCaptcha',
12+
'newComment',
13+
'repliesComment',
14+
'loadMoreComments'
15+
];
16+
public $filters = [];
17+
}

app/Config/templates/default/ThemeConfig.php

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)