Skip to content

Commit 379ebb6

Browse files
Security: patch critical vulnerabilities and bump to v0.31.4.0
- Mitigate Stored XSS in UserController by enforcing `esc()` on blacklist notes. - Fortify Fileeditor to prevent Authorization Bypass by extending `isHiddenPath()` validation to all file operations, safeguarding `.env` and system structures. - Implement strict allowlist filtering (`preg_replace_callback`) to block `srcdoc` XSS exploits in the Settings Map iframe config. - Enforce `html_purify` validation on Pages module endpoints to neutralize injected XSS. - Replace volatile cache-based install guard with a secure persistent `install.lock` mechanism in both web `InstallFilter` and CLI setup, fixing a critical re-entry bypass. - Resolve CRLF Injection inside environment setup by rigorously stripping carriage returns from untrusted inputs (`Install.php`). - Update the system version parameters to 0.31.4.0 across setup processes. - Honor security researcher 'offset' in the README.md Security Hall of Fame.
1 parent e8249d4 commit 379ebb6

39 files changed

Lines changed: 12039 additions & 78 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ 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.4.0] - 2026-04-06
8+
9+
### Security
10+
11+
- **XSS Protection:** Mitigated Stored XSS vulnerability in `UserController` by wrapping blacklist status notes in `esc()`.
12+
- **Authorization Bypass:** Fortified `Fileeditor` module by implementing `isHiddenPath` validation across all file operations (`readFile`, `saveFile`, `createFile`, `createFolder`, `renameFile`, `deleteFileOrFolder`), preventing unauthorized disclosure and modification of protected system files like `.env` and `composer.json`.
13+
- **Settings Security:** Reformed Google Maps iframe validation (`cMap`) in `Settings` controller to utilize a strict `preg_replace_callback` allowlist, mitigating a sophisticated srcdoc-based Cross-Site Scripting (XSS) exploit.
14+
- **Pages Security:** Appended the stringent `html_purify` validation rule to page creation and update flows to intercept and neutralize injected JavaScript securely.
15+
- **Installation Integrity:** Eliminated a volatile cache-dependent installation guard in favor of a persistent filesystem lock (`install.lock`) verification within both Web (`InstallFilter`) and CLI (`Ci4msSetup.php`) boot lifecycles. This successfully remediates a critical post-installation re-entry bypass.
16+
- **Input Validation:** Patched a CRLF Injection flaw within the initial environment setup by meticulously stripping `\r\n` carriage returns from arbitrary injected payload components inside `Install.php`.
17+
718
## [0.31.3.0] - 2026-04-02
819

920
### Added
@@ -226,6 +237,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
226237

227238
- Expanded database migrations and introduced new supporting libraries.
228239

240+
[0.31.4.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.4.0
241+
[0.31.3.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.3.0
229242
[0.31.2.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.2.0
230243
[0.31.1.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.1.0
231244
[0.31.0.0]: https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.0.0

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,5 +196,6 @@ A huge thank you to the security researchers who have helped make **ci4ms** more
196196
| **[Hunter.](https://github.com/LAW6ZX7)** | Identified Critical Stored XSS in Backend & Blog modules allowing Session Hijacking. | Feb 2026 |
197197
| **[m1scher](https://github.com/m1scher)** | Assisted with vulnerability triaging and security testing. | Feb 2026 |
198198
| **[alpernae](https://github.com/alpernae)** | Assisted with vulnerability triaging and security testing. | Feb 2026 |
199+
| **[offset](https://github.com/offset)** | Identified Critical vulnerabilities including multiple Stored XSS, Authorization Bypass in Fileeditor, Install Guard Bypass, and CRLF Injection. | Apr 2026 |
199200

200201
> If you find a security vulnerability, please report it via [Security Policy](SECURITY.md).

app/Config/Filters.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ private function loadDynamicFilters(array $directories): void
183183
\CodeIgniter\Shield\Filters\ForcePasswordResetFilter::class,
184184
\Modules\Auth\Filters\Ci4MsAuthFilter::class,
185185
\Modules\Backend\Filters\BackendLogFilter::class,
186+
\Modules\Auth\Filters\SessionTracker::class,
186187
];
187188
$this->aliases['langfilter'] = [
188189
\App\Filters\Ci4ms::class,

app/Controllers/BaseController.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,10 @@ protected function getDefaultData(): array
175175
'menus' => $menus,
176176
'languages' => cache('frontend_languages') ?? [],
177177
'alternateLinks' => [], // Default empty, filled by child controllers
178-
'agent' => $this->request->getUserAgent(),
179178
'seoConfig' => new Seo()
180179
];
180+
if(is_cli()) $defData['agent']='CLI';
181+
else $defData['agent'] = $this->request->getUserAgent();
181182

182183
// If languages are empty in cache, load them (fallback)
183184
if (empty($defData['languages'])) {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
namespace Modules\Auth\Database\Migrations;
4+
5+
use CodeIgniter\Database\Migration;
6+
7+
class CreateUserSessionsTable extends Migration
8+
{
9+
public function up(): void
10+
{
11+
$this->forge->addField([
12+
'id' => [
13+
'type' => 'INT',
14+
'constraint' => 11,
15+
'unsigned' => true,
16+
'auto_increment' => true,
17+
],
18+
'user_id' => [
19+
'type' => 'INT',
20+
'constraint' => 11,
21+
'unsigned' => true,
22+
],
23+
'session_id' => [
24+
'type' => 'VARCHAR',
25+
'constraint' => 128,
26+
],
27+
'ip_address' => [
28+
'type' => 'VARCHAR',
29+
'constraint' => 45,
30+
],
31+
'user_agent' => [
32+
'type' => 'VARCHAR',
33+
'constraint' => 512,
34+
'null' => true,
35+
],
36+
'browser' => [
37+
'type' => 'VARCHAR',
38+
'constraint' => 100,
39+
'null' => true,
40+
],
41+
'browser_version' => [
42+
'type' => 'VARCHAR',
43+
'constraint' => 50,
44+
'null' => true,
45+
],
46+
'os' => [
47+
'type' => 'VARCHAR',
48+
'constraint' => 100,
49+
'null' => true,
50+
],
51+
'device_type' => [
52+
'type' => 'ENUM',
53+
'constraint' => ['desktop', 'mobile', 'tablet', 'bot', 'unknown'],
54+
'default' => 'unknown',
55+
],
56+
'device_name' => [
57+
'type' => 'VARCHAR',
58+
'constraint' => 150,
59+
'null' => true,
60+
],
61+
'region' => [
62+
'type' => 'VARCHAR',
63+
'constraint' => 100,
64+
'null' => true,
65+
],
66+
'country' => [
67+
'type' => 'VARCHAR',
68+
'constraint' => 100,
69+
'null' => true,
70+
],
71+
'city' => [
72+
'type' => 'VARCHAR',
73+
'constraint' => 100,
74+
'null' => true,
75+
],
76+
'last_activity' => [
77+
'type' => 'DATETIME',
78+
'null' => true,
79+
],
80+
'is_active' => [
81+
'type' => 'TINYINT',
82+
'constraint' => 1,
83+
'default' => 1,
84+
],
85+
'created_at' => [
86+
'type' => 'DATETIME',
87+
'null' => true,
88+
],
89+
'terminated_at' => [
90+
'type' => 'DATETIME',
91+
'null' => true,
92+
],
93+
]);
94+
95+
$this->forge->addPrimaryKey('id');
96+
$this->forge->addKey('user_id');
97+
$this->forge->addKey('session_id');
98+
$this->forge->addKey(['user_id', 'is_active']); // composite index — liste sorgusu için
99+
100+
$this->forge->createTable('user_sessions', true);
101+
}
102+
103+
public function down(): void
104+
{
105+
$this->forge->dropTable('user_sessions', true);
106+
}
107+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace Modules\Auth\Filters;
4+
5+
use Modules\Auth\Models\UserSessionModel;
6+
use CodeIgniter\Filters\FilterInterface;
7+
use CodeIgniter\HTTP\RequestInterface;
8+
use CodeIgniter\HTTP\ResponseInterface;
9+
10+
/**
11+
* Security and Tracking Filter: User Session Tracker
12+
*
13+
* Designed to track the real-time device information and connection durations
14+
* of logged-in users. It also synchronously prevents database-controlled (DB-Driven)
15+
* session termination (revocation) at the Filter level.
16+
*/
17+
class SessionTracker implements FilterInterface
18+
{
19+
/**
20+
* Intercepts the request before it reaches the Controller.
21+
* Checks for a permanent "Device ID" (Tracker ID) belonging to the user, generates one if missing.
22+
* Uses this ID to verify the active status in the database; if inactive, terminates the process and logs the user out.
23+
*
24+
* @param RequestInterface $request Incoming HTTP Request
25+
* @param mixed $arguments Additional arguments
26+
* @return mixed
27+
*/
28+
public function before(RequestInterface $request, $arguments = null)
29+
{
30+
helper('device');
31+
32+
$userId = auth()->id();
33+
34+
if (! $userId) {
35+
return;
36+
}
37+
38+
$session = session();
39+
$sessionId = $session->get('ci4ms_session_tracker_id');
40+
41+
if (! $sessionId) {
42+
$sessionId = bin2hex(random_bytes(16));
43+
$session->set('ci4ms_session_tracker_id', $sessionId);
44+
}
45+
46+
$model = new UserSessionModel();
47+
48+
$exists = $model->where('session_id', $sessionId)->first();
49+
50+
if (! $exists) {
51+
$agent = $request->getUserAgent();
52+
$deviceInfo = extract_device_info($agent);
53+
54+
$model->recordLogin(
55+
userId: (int) $userId,
56+
sessionId: $sessionId,
57+
deviceInfo: $deviceInfo,
58+
ip: $request->getIPAddress()
59+
);
60+
} else {
61+
// If the current device's session status in the database has been remotely set to "is_active = 0",
62+
// forces the visitor out of their current device.
63+
if ($exists['is_active'] == 0) {
64+
auth()->logout();
65+
session()->destroy();
66+
return redirect()->route('login')->with('error', lang('Users.currentSessionTerminated'));
67+
}
68+
69+
$model->touchSession($sessionId);
70+
}
71+
}
72+
73+
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
74+
{
75+
// No further manipulation needed afterwards.
76+
}
77+
}

0 commit comments

Comments
 (0)