GitHub Security Advisory: GHSA-37j7-56xc-c468
Package: idno/known (COMPOSER)
Severity: HIGH (CVSS 0.0)
CWE: CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
Vulnerable versions: < 1.6.4
Patched in: 1.6.4

Summary: Idno Vulnerable to Remote Code Execution via Chained Import File Write and Template Path Traversal

Details:
**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](https://www.first.org/cvss/calculator/4.0#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 `<img>` 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 fi