<?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: AIOil Security Shield</title>
    <description>The latest articles on DEV Community by AIOil Security Shield (@aioilshield).</description>
    <link>https://dev.to/aioilshield</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%2F4095464%2Fe14a9109-145b-4344-931f-8671440a1068.jpg</url>
      <title>DEV Community: AIOil Security Shield</title>
      <link>https://dev.to/aioilshield</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aioilshield"/>
    <language>en</language>
    <item>
      <title>Our benchmark was leaking the answers to the model. The numbers looked fine the whole time.</title>
      <dc:creator>AIOil Security Shield</dc:creator>
      <pubDate>Wed, 02 Sep 2026 13:25:00 +0000</pubDate>
      <link>https://dev.to/aioilshield/our-benchmark-was-leaking-the-answers-to-the-model-the-numbers-looked-fine-the-whole-time-b6d</link>
      <guid>https://dev.to/aioilshield/our-benchmark-was-leaking-the-answers-to-the-model-the-numbers-looked-fine-the-whole-time-b6d</guid>
      <description>&lt;p&gt;We build an automated security scanner for Solana programs. To know whether it works, we keep a small benchmark: 19 programs with a planted vulnerability each, 7 written to be deliberately clean, and a written description of every planted flaw so a judge can check whether the scanner's findings actually match. We ran it for weeks. We published detection and false-alarm rates from it.&lt;/p&gt;

&lt;p&gt;Then, the day before we open-sourced the corpus, we read it the way an outsider would.&lt;/p&gt;

&lt;p&gt;It had been handing the scanner the answers the entire time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four leaks, all inside the file the scanner reads
&lt;/h3&gt;

&lt;p&gt;The scanner takes a source file and returns findings. So anything in that file is input. We knew that in the abstract. We had not actually looked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comments.&lt;/strong&gt; Nineteen of nineteen vulnerable samples carried a comment describing the planted flaw, the kind you write for yourself while building a test case and never think about again. Three of the seven clean samples declared themselves correct, in words.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Program IDs.&lt;/strong&gt; Solana programs declare their own address in the source. Ours said things like &lt;code&gt;FeeByp111...&lt;/code&gt; on the fee-bypass sample, and &lt;code&gt;Guard1111...&lt;/code&gt; or &lt;code&gt;T22G0od...&lt;/code&gt; on the clean ones. Speaking names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Module names.&lt;/strong&gt; &lt;code&gt;pub mod vulnerable_vault&lt;/code&gt;. &lt;code&gt;trust_me_lending&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log strings.&lt;/strong&gt; One read, verbatim: &lt;code&gt;"Estimated output: {} (no minimum check)"&lt;/code&gt;. That is the bug, printed as a string, inside the sample meant to test whether the bug gets found.&lt;/p&gt;

&lt;p&gt;The one that took longest to spot was the quietest. We had a matched pair, same program, one clean and one vulnerable, and the only difference in their log lines was the word "real" versus "measured". Two words. Enough to separate the two classes, if you are a model reading the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it did to the numbers
&lt;/h3&gt;

&lt;p&gt;Detection went up, because the flaw was being announced rather than found. False alarms went down, because several clean files said they were clean.&lt;/p&gt;

&lt;p&gt;And nothing about the output looked wrong. That is the part I keep coming back to. A leaking benchmark does not produce absurd numbers. It produces plausible ones. We had no reason to look, so we didn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we did
&lt;/h3&gt;

&lt;p&gt;We withdrew the published numbers. Not adjusted. Withdrew. The scanner had been measured on different inputs from the ones we would now use, and no correction recovers a number produced under different conditions.&lt;/p&gt;

&lt;p&gt;Then we cleaned the corpus, and the rule we settled on is deliberately dumb: strip every comment, mechanically, all of them. We considered going through and removing only the ones that leaked. We decided against it, because case-by-case judgement is exactly the process that let the leak through in the first place. "&lt;code&gt;grep -rc '//' samples/&lt;/code&gt; returns 0" is a property anyone can verify without trusting us. "We removed the suspicious ones" is not.&lt;/p&gt;

&lt;p&gt;Program IDs became neutral strings of the same length. Modules were renamed for their domain rather than their verdict, so &lt;code&gt;vulnerable_vault&lt;/code&gt; became &lt;code&gt;user_vault&lt;/code&gt;. Log strings were rewritten to say what the code does, not what it fails to do.&lt;/p&gt;

&lt;p&gt;We ran one scan on the cleaned corpus before publishing, on the subtlest sample. It still found the bug, without the comment telling it where to look. That is the only number I will give you from the new corpus, and I am giving it as an anecdote, not a rate. We have not rebuilt the rates yet, and I would rather publish none than publish a hurried one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I think this generalises
&lt;/h3&gt;

&lt;p&gt;Nothing about this failure is Solana-specific, or even security-specific. Any benchmark whose items carry human-written prose can leak its labels to a model that reads the item. Comments, identifiers, docstrings, test names, string literals: all of it is input, and all of it was written by someone who knew the answer.&lt;/p&gt;

&lt;p&gt;The contamination literature I have been able to find is mostly about train/test overlap, whether the model saw this exact item during training. This is a different failure. The model may never have seen the item. The item is just telling it what to say. And it is invisible from the outside, because the numbers it produces keep looking reasonable.&lt;/p&gt;

&lt;p&gt;I do not have tooling for this. I would genuinely like to know if someone does.&lt;/p&gt;

&lt;h3&gt;
  
  
  The corpus
&lt;/h3&gt;

&lt;p&gt;It is public under MIT: &lt;a href="https://github.com/AIOilShield/solana-scanner-benchmark" rel="noopener noreferrer"&gt;github.com/AIOilShield/solana-scanner-benchmark&lt;/a&gt;. Twenty-six programs, written ground truth for each, and the README tells this story in more detail. If you want to poke holes in it, please do. That is what it is for.&lt;/p&gt;

&lt;p&gt;Disclosure, since it matters: we sell a scanner. It is the tool this benchmark caught out.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>benchmarking</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Nine checks, all true, all useless</title>
      <dc:creator>AIOil Security Shield</dc:creator>
      <pubDate>Mon, 31 Aug 2026 11:21:11 +0000</pubDate>
      <link>https://dev.to/aioilshield/nine-checks-all-true-all-useless-3flm</link>
      <guid>https://dev.to/aioilshield/nine-checks-all-true-all-useless-3flm</guid>
      <description>&lt;p&gt;On 23 March 2022 an attacker minted roughly $52 million of CASH out of nothing.&lt;/p&gt;

&lt;p&gt;Cashio's minting path was not missing its validation. It ran &lt;strong&gt;nine separate key comparisons&lt;/strong&gt; before allowing a single token to be printed. Every one of them returned true while the vault was being emptied.&lt;/p&gt;

&lt;p&gt;That is what makes this worth reading. The bug is not exotic, it is not dead, and it fits comfortably inside code that looks careful. It is a &lt;em&gt;shape&lt;/em&gt;: a validation chain that is perfectly self-consistent and anchored to nothing. If you write Anchor programs you can check for it in your own repo in about ten minutes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Everything below is quoted from the pre-exploit source at commit &lt;code&gt;a51c3c5&lt;/code&gt; (11 March 2022, twelve days before the attack). Do &lt;strong&gt;not&lt;/strong&gt; read today's &lt;code&gt;master&lt;/code&gt; for this: &lt;code&gt;print_cash&lt;/code&gt; and &lt;code&gt;burn_cash&lt;/code&gt; are disabled there with &lt;code&gt;vipers::invariant!(false, "temporarily disabled")&lt;/code&gt;, so it is not the code that was exploited.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What the program was supposed to guarantee
&lt;/h2&gt;

&lt;p&gt;Cashio issued CASH, a stablecoin backed by Saber LP tokens. You deposited collateral, the program checked the collateral was real, and it minted CASH against it. The security of the whole system rests on one question:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Is this collateral actually the collateral this bank is supposed to accept?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer lived in two &lt;code&gt;validate()&lt;/code&gt; implementations, run through Anchor's &lt;code&gt;#[access_control]&lt;/code&gt; before &lt;code&gt;print_cash&lt;/code&gt; executed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five checks in &lt;code&gt;BrrrCommon::validate()&lt;/code&gt;&lt;/strong&gt; - &lt;code&gt;programs/brrr/src/actions/mod.rs&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;assert_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.bank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.collateral.bank&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.crate_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.crate_collateral_tokens.owner&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.crate_mint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.crate_token.mint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.crate_collateral_tokens.mint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.collateral.mint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.collateral.mint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.saber_swap.arrow.mint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Four more in &lt;code&gt;SaberSwapAccounts::validate()&lt;/code&gt;&lt;/strong&gt; - &lt;code&gt;programs/brrr/src/saber.rs&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;assert_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.arrow.vendor_miner.mint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.pool_mint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.saber_swap.pool_mint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.pool_mint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.saber_swap.token_a.reserves&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.reserve_a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.saber_swap.token_b.reserves&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.reserve_b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read them as a group and the flaw becomes visible. &lt;strong&gt;Every single comparison has an account supplied by the caller on both sides.&lt;/strong&gt; The bank is checked against the collateral's bank - but the caller passed both. The crate mint is checked against the crate token's mint - but the caller passed both. The chain links to itself, all the way down, and never touches solid ground.&lt;/p&gt;

&lt;p&gt;A validation chain that is internally consistent but externally unanchored proves exactly one thing: that the attacker built a coherent set of accounts. Which they did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the attacker could build that chain
&lt;/h2&gt;

&lt;p&gt;Constructing a parallel universe of accounts is only useful if you are allowed to create the root of it. In Cashio you were.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;new_bank&lt;/code&gt; instruction in the companion &lt;code&gt;bankman&lt;/code&gt; program took its &lt;code&gt;admin&lt;/code&gt; as an &lt;code&gt;UncheckedAccount&lt;/code&gt;, with no signature and no relationship to the payer, the upgrade authority, or any known key. Anyone could call it and become curator of their own bank.&lt;/p&gt;

&lt;p&gt;From there the sequence writes itself: create your own bank, point your own collateral at it, wire that collateral to a Saber pool you control holding a token you invented, then hand the whole consistent bundle to &lt;code&gt;print_cash&lt;/code&gt;. All nine assertions compare your accounts to your other accounts. All nine pass. Real CASH comes out, backed by a worthless token.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anchor was three lines away
&lt;/h2&gt;

&lt;p&gt;Here is the detail that makes Cashio instructive rather than merely unfortunate: &lt;strong&gt;the codebase already knew how to do this correctly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PrintCash::validate()&lt;/code&gt;, in the same call path, has four checks - and the last one is a different species entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;assert_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.depositor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.depositor_source.owner&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.depositor_source.mint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.common.collateral.mint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.mint_destination.mint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.common.crate_token.mint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.issue_authority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ISSUE_AUTHORITY_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;- anchored&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fourth line compares a caller-supplied account against a &lt;strong&gt;hardcoded constant&lt;/strong&gt;. It is exactly the discipline the other nine needed, applied correctly, sitting three lines above them. It simply never propagated to the bank and collateral chain.&lt;/p&gt;

&lt;p&gt;This is the normal way the bug arrives. Not through ignorance - through a habit applied in one place and not carried to the next.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check your own program tonight
&lt;/h2&gt;

&lt;p&gt;Take every key comparison in your validation path and ask one question of each: &lt;strong&gt;could an attacker have chosen both sides?&lt;/strong&gt; Then sort them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Anchored - proves something&lt;/th&gt;
&lt;th&gt;Self-referential - proves nothing alone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compared to a &lt;code&gt;const&lt;/code&gt; address&lt;/td&gt;
&lt;td&gt;Two accounts both passed in this instruction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compared to a PDA you re-derive with &lt;code&gt;find_program_address&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A field of one caller account against a field of another&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compared to the program's own upgrade authority&lt;/td&gt;
&lt;td&gt;An &lt;code&gt;UncheckedAccount&lt;/code&gt; against anything except a constant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compared to a value stored in an account only you can write&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A chain of self-referential checks is not worthless - it stops honest mistakes and fat-fingered accounts. It just does not stop an attacker, because an attacker supplies the whole set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every validation chain needs at least one link that reaches outside the transaction.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The rule, stated plainly: if a validation chain contains no comparison to a canonical root - a constant, a re-derived PDA, or a value only your program can write - then it is forgeable in full, and its severity is Critical regardless of how many checks it contains.&lt;/p&gt;

&lt;h2&gt;
  
  
  What our scanner said about it
&lt;/h2&gt;

&lt;p&gt;Disclosure: I work on &lt;a href="https://aioil.tech/security-shield.html" rel="noopener noreferrer"&gt;AIOil Security Shield&lt;/a&gt;, an automated multi-agent scanner for Solana programs. We ran this code through it on 16 August 2026, on the pre-exploit source, and published the raw console output rather than describing it.&lt;/p&gt;

&lt;p&gt;It returned three findings: one High on Saber reserve balances being manipulable for pricing, one Medium, one Low. The Medium is the interesting one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;new_bank can be called by anyone to seize Bank curator/bankman role.&lt;/strong&gt; Location: &lt;code&gt;programs/bankman/src/lib.rs::new_bank&lt;/code&gt;. The instruction has no authorization constraint linking &lt;code&gt;admin&lt;/code&gt; to the payer or the upgrade authority; &lt;code&gt;admin&lt;/code&gt; is an &lt;code&gt;UncheckedAccount&lt;/code&gt; requiring no signature.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It identifies the precondition correctly and locates it precisely. What it frames wrongly is the consequence: it describes an attacker racing the deployer to squat the canonical bank, and suggests documenting the behaviour if permissionless operation is intentional. The real attacker never raced anyone - they created a parallel bank &lt;em&gt;after&lt;/em&gt; deployment, and the damage came from the minting path never checking that a bank is &lt;em&gt;the&lt;/em&gt; bank.&lt;/p&gt;

&lt;p&gt;Right door, wrong description of what lay behind it, and rated Medium rather than Critical.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/AIOilShield/solana-postmortem-scans/blob/main/scans/cashio-a5d505580e2aaa5f.log.txt" rel="noopener noreferrer"&gt;Raw scan output on GitHub&lt;/a&gt; - &lt;code&gt;scan_id a5d505580e2aaa5f&lt;/code&gt;, source SHA-256 &lt;code&gt;c2783d1c352fc333f310ea48a51187b8f82d65b8a11590c9a5a0b211eff3de69&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aioil.tech/case-wormhole.html" rel="noopener noreferrer"&gt;The companion run: the same pipeline on Wormhole's bridge, the day before that hack&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway that outlives the case
&lt;/h2&gt;

&lt;p&gt;Counting your checks tells you nothing about whether you are safe. Nine is not safer than one. What matters is whether &lt;strong&gt;any&lt;/strong&gt; of them is nailed to something the caller cannot choose - and that question takes a few minutes to answer for a whole program.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The Cashio program discussed here was exploited in March 2022 and has been disabled since; nothing here is an unreported vulnerability. The scan was run in 2026, on code whose outcome was already public - no claim is made that it would have prevented the incident. Automated static analysis is not a professional human audit: it reduces risk, it does not eliminate it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>security</category>
      <category>rust</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>We scanned Wormhole's bridge as of the day before the 2022 hack. It found the bug. Then we scanned Cashio, and it missed.</title>
      <dc:creator>AIOil Security Shield</dc:creator>
      <pubDate>Wed, 26 Aug 2026 10:02:10 +0000</pubDate>
      <link>https://dev.to/aioilshield/we-scanned-wormholes-bridge-as-of-the-day-before-the-2022-hack-it-found-the-bug-then-we-scanned-3ond</link>
      <guid>https://dev.to/aioilshield/we-scanned-wormholes-bridge-as-of-the-day-before-the-2022-hack-it-found-the-bug-then-we-scanned-3ond</guid>
      <description>&lt;p&gt;Static analysis demos are usually rigged. You pick the bug, you write the check, you show the check finding the bug. Nobody learns anything.&lt;/p&gt;

&lt;p&gt;So we did the opposite. We took the on-chain source of two Solana protocols &lt;strong&gt;as it stood the day before each was exploited&lt;/strong&gt;, ran it through our scanner with no hints and no knowledge of what the answer was supposed to be, and published both results — including the one where it failed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Real root cause&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wormhole core bridge&lt;/td&gt;
&lt;td&gt;Feb 2022 · $325M&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Found&lt;/strong&gt; — Critical, right location, right fix&lt;/td&gt;
&lt;td&gt;7m 33s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cashio &lt;code&gt;brrr&lt;/code&gt; / &lt;code&gt;bankman&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Mar 2022 · $52M&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Missed&lt;/strong&gt; — flagged a related symptom&lt;/td&gt;
&lt;td&gt;9m 36s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One caveat before anything else, because it matters: &lt;strong&gt;we are not claiming this would have prevented either incident.&lt;/strong&gt; These runs happened in 2026, on code whose outcome has been public for four years. Both programs were patched long ago. This is a re-run on public history, not a disclosure and not a counterfactual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wormhole: it found the bug
&lt;/h2&gt;

&lt;p&gt;The scanner reported five findings. The second one was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;instruction_acc&lt;/code&gt; is a raw &lt;code&gt;Info&lt;/code&gt; account and is never checked against &lt;code&gt;solana_program::sysvar::instructions::id()&lt;/code&gt;. The code parses its data as the serialized instructions sysvar, so an attacker can supply a fake account with crafted bytes that looks like a previous secp256k1 instruction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the bug. Not an adjacent one — the one that cost $325M. Forge the instructions sysvar and every guardian signature is treated as valid without a single ECDSA verification ever happening; the resulting VAA mints wrapped ETH out of nothing.&lt;/p&gt;

&lt;p&gt;You can confirm it yourself in about thirty seconds. In the pre-patch source, the account is declared untyped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="cd"&gt;/// Instruction reflection account (special sysvar)&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;instruction_acc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'b&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// no type, no owner check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its data is then parsed twice as the instructions sysvar — &lt;code&gt;load_current_index&lt;/code&gt; and &lt;code&gt;load_instruction_at&lt;/code&gt; — and in the entire program those two calls are the only places the sysvar appears anywhere. There is no comparison against &lt;code&gt;sysvar::instructions::id()&lt;/code&gt;. Nothing establishes that the account handed in is the real sysvar.&lt;/p&gt;

&lt;p&gt;The fix the scanner proposed is the fix that actually shipped: use a typed &lt;code&gt;Sysvar&amp;lt;Instructions&amp;gt;&lt;/code&gt;, or compare the key explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The other four findings, honestly
&lt;/h3&gt;

&lt;p&gt;A tool that reports five things and is right about one of them has not found five bugs. We verified every finding by hand against the source, and here is the honest breakdown:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Critical — permissionless &lt;code&gt;initialize&lt;/code&gt;. Does not apply.&lt;/strong&gt; The code property is real: &lt;code&gt;initialize&lt;/code&gt; has no constraint tying the caller to the deployer. But it requires the bridge accounts to be &lt;em&gt;uninitialized&lt;/em&gt;, and the bridge has been live since 2021. This describes a deployment-window risk, not a live one. Critical is the wrong severity for a deployed bridge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High — &lt;code&gt;transfer_fees&lt;/code&gt; does not update &lt;code&gt;bridge.last_lamports&lt;/code&gt;. Real.&lt;/strong&gt; &lt;code&gt;last_lamports&lt;/code&gt; is written in exactly two places, and the fee-withdrawal path is not one of them, so the next &lt;code&gt;post_message&lt;/code&gt; hits &lt;code&gt;MathOverflow&lt;/code&gt;. It needs a governance action to trigger, and anyone can unbrick it by topping the collector back up — but it is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medium — &lt;code&gt;post_message&lt;/code&gt; fee front-running. Over-rated.&lt;/strong&gt; The mechanism exists, but the source carries a comment saying the behaviour is deliberate: &lt;em&gt;"checking previously known balance allows us to not care who is the payer"&lt;/em&gt;. The scanner flagged an intentional design decision and did not notice the comment saying so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low — &lt;code&gt;upgrade_guardian_set&lt;/code&gt; accepts up to 255 keys. Real.&lt;/strong&gt; &lt;code&gt;initialize&lt;/code&gt; caps at 19 and the signer array is fixed at 19.&lt;/p&gt;

&lt;p&gt;So: one true Critical, one real High, one real Low, and one severity error in each direction. That is the actual shape of the result, and any writeup that hides it is selling something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cashio: it missed
&lt;/h2&gt;

&lt;p&gt;Cashio is where it gets interesting, because the failure is more instructive than the hit.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the exploit actually was
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;BrrrCommon::validate()&lt;/code&gt; and &lt;code&gt;SaberSwapAccounts::validate()&lt;/code&gt; perform nine &lt;code&gt;assert_keys_eq!&lt;/code&gt; checks. Every single one compares &lt;strong&gt;two caller-supplied accounts to each other&lt;/strong&gt;. Not one is anchored to a canonical root — a known address, a PDA with fixed seeds, an upgrade authority. An attacker who constructs the entire chain out of accounts they control passes all nine checks.&lt;/p&gt;

&lt;p&gt;The codebase clearly knows how to anchor: &lt;code&gt;print_cash&lt;/code&gt; checks &lt;code&gt;issue_authority&lt;/code&gt; against a hardcoded &lt;code&gt;ISSUE_AUTHORITY_ADDRESS&lt;/code&gt;. That discipline was simply never applied to the bank/collateral chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  What our scanner said instead
&lt;/h3&gt;

&lt;p&gt;It returned three findings. The relevant one was &lt;strong&gt;Medium&lt;/strong&gt;, on &lt;code&gt;new_bank&lt;/code&gt; being callable by anyone — a genuine precondition of the attack. But it framed the risk as &lt;em&gt;front-running the deployer to squat the canonical bank&lt;/em&gt;, and it closed with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If multiple banks are intentionally permissionless, document it and ignore this finding."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The real attacker never front-ran anyone. They created a parallel bank &lt;strong&gt;after&lt;/strong&gt; deployment — which the design permits — and the actual flaw is that the minting path never verifies the bank is &lt;em&gt;the&lt;/em&gt; bank. The scanner saw the unlocked door and described the wrong way through it. Worse, its own recommendation invites you to dismiss the finding.&lt;/p&gt;

&lt;p&gt;We are not going to call that a near-miss. It is a miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that was actually worth the exercise
&lt;/h2&gt;

&lt;p&gt;Put the two runs side by side and the same structural pattern appears in both: &lt;strong&gt;an unauthenticated initializer that hands the attacker the root of a validation chain.&lt;/strong&gt; On Wormhole it was rated Critical. On Cashio it was rated Medium, with a suggestion to ignore it.&lt;/p&gt;

&lt;p&gt;The scanner is not blind to the pattern. It weighs it inconsistently. That is a much more actionable defect than "it missed one", because inconsistent severity is fixable and blindness is not.&lt;/p&gt;

&lt;p&gt;So we wrote the rule down and added it to the reviewer's rulebook:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When a validation chain is only internally consistent — every check compares two caller-supplied accounts, and none is anchored to a canonical root — the severity is &lt;strong&gt;Critical&lt;/strong&gt;, not Medium. The attacker can forge the entire chain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What this says about automated scanning generally
&lt;/h2&gt;

&lt;p&gt;Three things we would take away from this, regardless of whose tool you use:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The finding text matters as much as the finding.&lt;/strong&gt; Cashio's Medium contained the right precondition and still led the reader away from the bug. A correct detection with a wrong exploit narrative can be worse than no detection, because it gets consciously dismissed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Severity is where these tools are weakest.&lt;/strong&gt; Both severity errors here — one up, one down — came from failing to reason about deployment state and about what an attacker actually controls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask any vendor for output on code they did not choose.&lt;/strong&gt; Including the failures. A page that shows only the win is a page nobody should believe.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Artifacts
&lt;/h2&gt;

&lt;p&gt;The full write-up, the console output of both runs, the scan IDs, and the SHA-256 of the exact source submitted are here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/AIOilShield/solana-postmortem-scans" rel="noopener noreferrer"&gt;https://github.com/AIOilShield/solana-postmortem-scans&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Write-up: &lt;a href="https://aioil.tech/case-wormhole.html" rel="noopener noreferrer"&gt;https://aioil.tech/case-wormhole.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scanner is AIOil Security Shield. It runs automated multi-agent static analysis on Solana Rust/Anchor programs and publishes a Scan Report — never a certificate. It is not a professional human audit and does not guarantee the absence of vulnerabilities: it reduces risk, it does not eliminate it. Both programs discussed here were patched years ago, and nothing on this page is an unreported vulnerability.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>security</category>
      <category>rust</category>
      <category>blockchain</category>
    </item>
  </channel>
</rss>
