<?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: 幻灵末士</title>
    <description>The latest articles on DEV Community by 幻灵末士 (@thecrazyrabbit).</description>
    <link>https://dev.to/thecrazyrabbit</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%2F4099068%2F34fa0750-00a8-46d2-9543-34dc8efad8dc.jpg</url>
      <title>DEV Community: 幻灵末士</title>
      <link>https://dev.to/thecrazyrabbit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thecrazyrabbit"/>
    <language>en</language>
    <item>
      <title>How I Found a postMessage Origin Bypass in an OAuth SDK</title>
      <dc:creator>幻灵末士</dc:creator>
      <pubDate>Fri, 28 Aug 2026 14:33:30 +0000</pubDate>
      <link>https://dev.to/thecrazyrabbit/how-i-found-a-postmessage-origin-bypass-in-an-oauth-sdk-3a8j</link>
      <guid>https://dev.to/thecrazyrabbit/how-i-found-a-postmessage-origin-bypass-in-an-oauth-sdk-3a8j</guid>
      <description>&lt;p&gt;I spend a lot of time reading other people's code. Not because I enjoy it—though honestly, I kind of do—but because that's where the interesting bugs live. Not the flashy ones that get all the attention on Twitter. The quiet ones. The ones hiding in plain sight, inside a single missing &lt;code&gt;if&lt;/code&gt; statement.&lt;/p&gt;

&lt;p&gt;This is the story of one of those bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Backstory
&lt;/h2&gt;

&lt;p&gt;A few weeks ago I was doing a security review of a web3 project. You know the drill: connect wallet, sign in with Google, maybe an NFT drop if you're lucky. The usual.&lt;/p&gt;

&lt;p&gt;The project used an OAuth SDK for their "Sign in with Google" flow. Pretty standard stuff. You click a button, a popup opens, you authenticate with Google, the popup closes, and you're logged in. Smooth UX, works great.&lt;/p&gt;

&lt;p&gt;Under the hood, this flow relies on &lt;code&gt;postMessage&lt;/code&gt; to communicate between the popup window and the main application window. And if you've done any web security work, you know where this is going.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;postMessage&lt;/code&gt; is one of those APIs that's incredibly useful and incredibly easy to get wrong. The browser happily delivers messages between windows regardless of where they come from. It's up to you, the developer, to check &lt;code&gt;event.origin&lt;/code&gt; and make sure you're only accepting messages from places you actually trust.&lt;/p&gt;

&lt;p&gt;Spoiler alert: sometimes people forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hunt
&lt;/h2&gt;

&lt;p&gt;I started by looking at how the SDK handled messages coming back from the OAuth popup. Here's roughly what I found:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirectEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MessageEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIntermediaryEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;OAuthPopupEventEmit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PopupEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;requestPayload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;redirectEvent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take a good look at that. What's missing?&lt;/p&gt;

&lt;p&gt;There's no &lt;code&gt;event.origin&lt;/code&gt; check. No validation of where the message is coming from. The listener receives a message, grabs &lt;code&gt;event.data&lt;/code&gt;, and forwards it along to be processed. It doesn't care if the message came from the legitimate OAuth popup, a malicious page, or the void.&lt;/p&gt;

&lt;p&gt;This is the equivalent of answering your front door without looking through the peephole. Sure, it's &lt;em&gt;probably&lt;/em&gt; your friend, but you're not even checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Interesting Part
&lt;/h2&gt;

&lt;p&gt;Now, a missing origin check is bad on its own. But I wanted to know: how bad? What can an attacker actually &lt;em&gt;do&lt;/em&gt; with this?&lt;/p&gt;

&lt;p&gt;The answer depended on how the SDK matches incoming messages to pending OAuth requests. And here's where it got interesting.&lt;/p&gt;

&lt;p&gt;The SDK uses a &lt;code&gt;payloadId&lt;/code&gt; to correlate popup messages with the original login request. When you initiate an OAuth flow, the SDK generates an ID, sends it along with the popup URL, and waits for a message that references that same ID.&lt;/p&gt;

&lt;p&gt;I traced this ID generation back to its source, expecting to find a cryptographically secure random generator. Something with &lt;code&gt;crypto.getRandomValues()&lt;/code&gt; or at least a timestamp mixed in. Instead, I found something much simpler:&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getPayloadId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. A module-level counter that increments by one every time it's called. On the web platform, every OAuth request gets the next integer in sequence. If you see request #7 go out, you know the next one will be #8.&lt;/p&gt;

&lt;p&gt;Let me walk through how an attacker could chain this together. As soon as the victim's OAuth popup opens, the SDK registers its message listener. At that exact moment, the attacker's page—which the victim has open in another tab—can send a message via &lt;code&gt;window.opener.postMessage()&lt;/code&gt;. That message includes a &lt;code&gt;payloadId&lt;/code&gt; that the attacker predicted by simply counting the current request number. The listener, with no origin check, passes the message to the SDK's internal logic, which processes it as if it came from the legitimate Google popup. The attacker doesn't need to guess anything else.&lt;/p&gt;

&lt;p&gt;That's what turned this from "interesting oddity" into "real exploit."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cherry on Top
&lt;/h2&gt;

&lt;p&gt;While digging through this SDK's codebase, I noticed something that made me chuckle. The same company maintained another package in the same SDK family. That package &lt;em&gt;did&lt;/em&gt; do the right thing:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They knew about origin validation. They'd done it correctly elsewhere. But in the OAuth extension, it was just... absent. A lapse in consistency between two codebases that should have followed the same pattern.&lt;/p&gt;

&lt;p&gt;This is actually pretty common in larger codebases. Different teams, different timelines, different levels of review. Knowledge gets siloed. What one maintainer knows, another doesn't. And sometimes a critical check that exists in &lt;code&gt;iframe-controller.ts&lt;/code&gt; never makes it to &lt;code&gt;oauth2/src/index.ts&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;I reported the issue, and the fix was about as simple as you'd expect. Add the origin check, mirror the pattern that already existed elsewhere in their own codebase:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirectEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MessageEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIntermediaryEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;OAuthPopupEventEmit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PopupEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;requestPayload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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;One line. That's it. One line separates "vulnerable" from "not vulnerable."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned (And You Should Too)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Always validate &lt;code&gt;event.origin&lt;/code&gt;.&lt;/strong&gt; I know, you've heard this a thousand times. But I just found a production SDK that forgot, so apparently we need to keep saying it. Never assume a &lt;code&gt;message&lt;/code&gt; event comes from where you expect. Always check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predictable IDs are dangerous.&lt;/strong&gt; Sequential IDs aren't just bad for enumeration attacks—they can enable cross-window attacks like this one. Use cryptographically random IDs when correlating async operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency matters in security.&lt;/strong&gt; If one part of your codebase does security right and another doesn't, that's not just an inconsistency. It's a roadmap for attackers. They'll find the weakest link.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read code.&lt;/strong&gt; I didn't find this with a scanner. I just opened the source code, traced the message flow, and followed it until something didn't add up. Sometimes the best tool is just a text editor and a willingness to ask "what if?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;I can't share specifics about the SDK or the exact details of my report—responsible disclosure means giving the vendor time to patch and publish their own advisory. But the &lt;em&gt;pattern&lt;/em&gt; is what matters here. Missing origin validation is one of those bugs that shows up everywhere: SDKs, wallets, analytics tools, chat widgets. If your app uses &lt;code&gt;postMessage&lt;/code&gt; anywhere, go check your listeners right now. I'll wait.&lt;/p&gt;

&lt;p&gt;Seriously, go check.&lt;/p&gt;

&lt;p&gt;The best bugs aren't always the ones with the most complex attack chains. Sometimes they're the ones hiding in the gap between what the code assumes and what the browser actually delivers.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading! If you enjoyed this, I write about web security, code review, and the occasional "how did that even work" bug. Follow along for more.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>cybersecurity</category>
      <category>security</category>
    </item>
  </channel>
</rss>
