# Unauthenticated Path Traversal Allowing Derivative Image Writes Outside the Intended `_data/i` Cache Directory in Piwigo v16.3.0 ## 1. Vulnerability Description An unauthenticated path traversal vulnerability exists in `i.php` in Piwigo v16.3.0. The endpoint accepts a user-controlled derivative request path from the URL and validates each path token only with `sync_chars_regex`. Under the default configuration, this regular expression allows dot characters, so path traversal tokens such as `..` are accepted as valid path segments. The same untrusted request path is then concatenated into the derivative cache output path: ```php $page['derivative_path'] = PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR.$req; ``` Because the code does not normalize and constrain the resulting path to remain under the intended `_data/i/` cache directory, an attacker can use `../` segments to move the output path out of `_data/i/`. The endpoint then creates the attacker-influenced directory tree and writes a generated derivative image there. In the tested Windows environment, this issue can be triggered without authentication by requesting local images located outside the Piwigo application directory, as long as the source image path and derivative filename satisfy the constraints described below. This is a path traversal issue affecting derivative image writes. It is not a fully arbitrary byte-write primitive: the server writes image output generated by Piwigo's image handling code, and the requested filename must follow Piwigo's derivative naming rules. - Vulnerability Type: Path Traversal / Path-Controlled File Write - CWE ID: CWE-22 ## 2. Affected Version Piwigo v16.3.0 Other versions have not been verified yet. ## 3. Trigger Conditions and Important Constraints ### 3.1 Public reachability The vulnerable endpoint is directly reachable without authentication: - `GET /i.php?...` No login, API key, or `pwg_token` is required. ### 3.2 Source file type restrictions The endpoint does not read arbitrary local files. It only processes files that Piwigo accepts as picture sources. By default, Piwigo declares the following allowed picture extensions: ```php $conf['picture_ext'] = array('jpg','jpeg','png','gif','webp'); ``` In the tested Windows installation, the successful proof-of-concept requests used valid PNG files. The current GD code path explicitly decodes source images as: - `jpg` - `jpeg` - `png` - `gif` Files whose extension says `.png` but whose actual content is another format will fail during decoding. ### 3.3 URL construction rules The requested filename is not fully arbitrary. Piwigo expects a derivative filename in this form: ```text -. ``` The code splits the last hyphen-separated segment as the derivative identifier. The source image path is computed from the remaining basename plus the original extension. Examples: - Request: `test1-sq.png` - Source image expected by Piwigo: `test1.png` - Request: `test2-th.png` - Source image expected by Piwigo: `test2.png` If the source file name already ends with a derivative-looking suffix, an additional valid derivative suffix is still required in the request. For example: - Source file: `A-sq.png` - Valid request form: `A-sq-sq.png` ### 3.4 Valid derivative suffixes The derivative identifier comes from `derivative_to_url()`, which uses two-character size identifiers. Common valid suffixes include: - `-sq` - `-th` - `-2s` - `-xs` - `-sm` - `-me` - `-la` - `-xl` - `-xx` - `-3x` - `-4x` If the request does not contain a final valid derivative suffix, parsing fails. ### 3.5 When the write remains under `_data/` The intended cache prefix is: ```text \_data\i\ ``` Whether the output stays there or escapes elsewhere depends on how many `../` segments appear in the attacker-controlled request path. - If no traversal is used, the derivative stays under `_data\i\...` - If traversal is used but not enough to escape `_data`, the file may still remain under `_data\...` - In the tested Windows setup, a single leading `../` cancels the trailing `i\` segment and causes the derivative to be written under `_data\...` instead of `_data\i\...` - Additional `../` segments can continue escaping farther, potentially outside `_data`, if later source-resolution and image-processing conditions are still satisfied This report includes minimal reproductions that stay inside the application `_data\` directory while still proving directory traversal out of the intended `_data/i/` cache location. ## 4. Tested Environment - Operating System: Windows - Web server / local host: `http://localhost` - Piwigo version: `16.3.0` - PHP version: `8.2.0` - MySQL version: `8.4.5` - Piwigo installation path: `D:\develop\Apache24\htdocs\Piwigo_16.3.0` - Local source images used for reproduction: - `D:\develop\Apache24\htdocs\Pictures\themes\test1.png` - `D:\develop\Apache24\htdocs\Pictures\plugins\test2.png` ## 5. Steps to Reproduce 1. Do not log in. Original directory structure: image-20260416165229695 2. Prepare the following valid local PNG files on the same Windows host: - `D:\develop\Apache24\htdocs\Pictures\themes\test1.png` - `D:\develop\Apache24\htdocs\Pictures\plugins\test2.png` image-20260416165312917 3. Send the following request: `GET http://localhost/i.php?/../Pictures/themes/test1-sq.png` image-20260416165346254 Observe that the request succeeds and returns an image response. A new derivative file is created at: `D:\develop\Apache24\htdocs\Piwigo_16.3.0\_data\Pictures\themes\test1-sq.png` image-20260416165527935 This proves that the attacker-controlled `../` escapes the intended `_data\i\` cache directory and writes into `_data\Pictures\themes\...` instead. 4. Send the following request: `GET http://localhost/i.php?/../Pictures/plugins/test2-th.png` image-20260416165548907 Observe that the request succeeds and returns an image response. A new derivative file is created at: `D:\develop\Apache24\htdocs\Piwigo_16.3.0\_data\Pictures\plugins\test2-th.png` image-20260416165627766 This again demonstrates that the attacker-controlled traversal changes the derivative output path from `_data\i\...` to `_data\...`. ## 6. Proof of Concept Requests ### (1) Write derivative image under `_data\Pictures\themes\...` `http://localhost/i.php?/../Pictures/themes/test1-sq.png` Observed output file: `D:\develop\Apache24\htdocs\Piwigo_16.3.0\_data\Pictures\themes\test1-sq.png` ### (2) Write derivative image under `_data\Pictures\plugins\...` `http://localhost/i.php?/../Pictures/plugins/test2-th.png` Observed output file: `D:\develop\Apache24\htdocs\Piwigo_16.3.0\_data\Pictures\plugins\test2-th.png` ## 7. Vulnerable Code ### (1) `i.php:182-187` The request path is tokenized and validated only with `sync_chars_regex`, then directly concatenated into the derivative output path: ```php foreach (preg_split('#/+#', $req) as $token) { preg_match($conf['sync_chars_regex'], $token) or ierror('Invalid chars in request', 400); } $page['derivative_path'] = PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR.$req; ``` ### (2) `include/config_default.inc.php:952` The default regex allows `.` characters, so a path token equal to `..` is accepted: ```php $conf['sync_chars_regex'] = '/^[a-zA-Z0-9-_.]+$/'; ``` ### (3) `i.php:189-198` The endpoint requires derivative-style filenames and parses the last hyphen-separated segment as the derivative identifier: ```php $pos = strrpos($req, '.'); $ext = substr($req, $pos); $req = substr($req, 0, $pos); $pos = strrpos($req, '-'); $deriv = substr($req, $pos+1); $req = substr($req, 0, $pos); ``` ### (4) `include/derivative_params.inc.php:20-23` Derivative identifiers are short path-controlled tokens derived from image size names: ```php function derivative_to_url($t) { return substr($t, 0, 2); } ``` ### (5) `i.php:248-258` The code resolves the local source image from attacker-influenced relative paths: ```php if (is_file(PHPWG_ROOT_PATH.$req.$ext)) { $req = './'.$req; } elseif (is_file(PHPWG_ROOT_PATH.'../'.$req.$ext)) { $req = '../'.$req; } $page['src_location'] = $req.$ext; $page['src_path'] = PHPWG_ROOT_PATH.$page['src_location']; ``` ### (6) `include/config_default.inc.php:46` Piwigo's default configured picture extensions are: ```php $conf['picture_ext'] = array('jpg','jpeg','png','gif','webp'); ``` ### (7) `admin/include/image.class.php:60-64` Only files whose extension is in `picture_ext` are accepted as source pictures: ```php if (!in_array($extension, $conf['picture_ext'])) { die('[Image] unsupported file extension'); } ``` ### (8) `admin/include/image.class.php:772-792` In the tested GD code path, source decoding is explicitly implemented for JPEG, PNG, and GIF: ```php if (in_array($extension, array('jpg', 'jpeg'))) { $this->image = imagecreatefromjpeg($source_filepath); } else if ($extension == 'png') { $this->image = imagecreatefrompng($source_filepath); } elseif ($extension == 'gif' and $gd_info['GIF Read Support'] and $gd_info['GIF Create Support']) { $this->image = imagecreatefromgif($source_filepath); } ``` ### (9) `i.php:522-628` The endpoint creates attacker-influenced directories and writes the derivative image to the attacker-influenced path: ```php if (!mkgetdir(dirname($page['derivative_path']))) { ierror("dir create error", 500); } $image = new pwg_image($page['src_path']); ... $image->write( $page['derivative_path'] ); ``` ## 8. Security Impact An unauthenticated attacker can cause Piwigo to: - process an existing local image file chosen through a relative path - write a generated derivative image outside the intended `_data/i/` cache subtree - create attacker-influenced directory structures during this process In the minimal reproductions above, the write primitive escapes from `_data/i/` to `_data/`. Because the final path is based on filesystem normalization of: ```text \_data\i\ ``` additional traversal segments can potentially move the output even farther than `_data\`, depending on the source path and later execution conditions. The write primitive is constrained as follows: - it writes image output, not arbitrary attacker-chosen bytes - the source file must be an acceptable local picture - the requested filename must follow Piwigo's derivative naming rules ## 9. Contact Information You can contact me at: `sliao25@m.fudan.edu.cn` If you determine that this report describes a valid security or privacy issue, I would appreciate being added to the related task or bug report so that I can follow its progress.