<?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 an SSR Cache Isolation Failure in a React Data Fetching Library</title>
      <dc:creator>幻灵末士</dc:creator>
      <pubDate>Sat, 29 Aug 2026 16:36:52 +0000</pubDate>
      <link>https://dev.to/thecrazyrabbit/how-i-found-an-ssr-cache-isolation-failure-in-a-react-data-fetching-library-36l0</link>
      <guid>https://dev.to/thecrazyrabbit/how-i-found-an-ssr-cache-isolation-failure-in-a-react-data-fetching-library-36l0</guid>
      <description>&lt;p&gt;There's a certain kind of bug that only shows up when you stop thinking about what code &lt;em&gt;does&lt;/em&gt; and start thinking about &lt;em&gt;when&lt;/em&gt; it does it.&lt;/p&gt;

&lt;p&gt;Most security researchers spend their time asking "can an attacker make this code do something bad?" That's a fine question. But it misses an entire category of vulnerabilities that have nothing to do with malicious input. Sometimes the code does something bad all on its own, just because of how it's structured.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I was auditing a popular React data fetching library. You've probably used it or at least seen it in a Next.js project. It's the kind of library that handles caching, revalidation, and all the other unglamorous parts of making API calls from React components.&lt;/p&gt;

&lt;p&gt;The library has a feature that's pretty common in the React ecosystem: it works differently depending on whether you're running on the client or the server. On the client, you want caching to persist across page navigations. On the server—during server-side rendering—each request should be completely isolated from every other request. That's Security 101 for SSR: if user A and user B both trigger a server render at the same time, neither should be able to see the other's data.&lt;/p&gt;

&lt;p&gt;So I started asking a simple question: how does this library manage its cache during server-side rendering?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Deep Dive
&lt;/h2&gt;

&lt;p&gt;I traced the cache initialization back to its source. What I found was about as simple as it gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&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="nx"&gt;mutate&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="nx"&gt;unload&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;initCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&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 a module-level variable. A single &lt;code&gt;Map&lt;/code&gt; object, created once, sitting at the top of a file. Every part of the library that needs a cache references this same object.&lt;/p&gt;

&lt;p&gt;On the client, this makes total sense. You want one cache for the entire application. When the user navigates between pages, the cache persists. That's the whole point.&lt;/p&gt;

&lt;p&gt;But on the server, module-level variables are dangerous. In a Node.js environment, modules are loaded once and shared across all incoming requests. That means this single cache &lt;code&gt;Map&lt;/code&gt; is shared across every user who hits the server. Concurrently.&lt;/p&gt;

&lt;p&gt;I looked for a mechanism that would provide request-scoped isolation by default. Some kind of &lt;code&gt;AsyncLocalStorage&lt;/code&gt; wrapper, or a factory function that creates a fresh cache per request, or even a check that said "hey, you're in SSR mode, here's an isolated cache instead."&lt;/p&gt;

&lt;p&gt;I didn't find one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Oh No" Moment
&lt;/h2&gt;

&lt;p&gt;Here's what the vulnerability looks like in practice. Imagine a Next.js application that uses this library to fetch user-specific data during server-side rendering. The component looks something like this:&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;function&lt;/span&gt; &lt;span class="nf"&gt;UserProfile&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="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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSWR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/user-data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Hello&lt;/span&gt;&lt;span class="p"&gt;,&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cache key is &lt;code&gt;/api/user-data&lt;/code&gt;. It's the same for every user. The library uses this key to look up cached data before making a fetch request.&lt;/p&gt;

&lt;p&gt;Now here's what happens when two users hit the server around the same time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;User A's request starts rendering. The library checks the global cache for &lt;code&gt;/api/user-data&lt;/code&gt;. It's empty, so it fetches the data from the API and stores it in the cache. The cache now contains User A's data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;User B's request starts rendering a few milliseconds later. The library checks the global cache for &lt;code&gt;/api/user-data&lt;/code&gt;. It finds User A's data sitting there. It uses it. No fetch needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;User B's HTML response contains User A's name, email, or whatever else was in that cached payload.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No attacker needed. No malicious input. Just two users using the application at the same time, and the library's default behavior hands one user's data to another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Happens
&lt;/h2&gt;

&lt;p&gt;The library does provide a way to isolate caches per request. There's a provider component that you can wrap your app in, and it creates a fresh cache scope. But it's opt-in. The default configuration doesn't do this.&lt;/p&gt;

&lt;p&gt;And here's the thing about defaults: they're what most people use. If you're a developer integrating this library into your Next.js app, you'll probably follow the docs, use the default setup, and never realize that you're sharing cache state across all your users. The docs might mention the provider in passing, but unless you're specifically looking for SSR isolation concerns, you're not going to connect the dots.&lt;/p&gt;

&lt;p&gt;This is one of those situations where the secure option exists but the insecure option is the default. And in security, defaults matter more than anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Broader Pattern
&lt;/h2&gt;

&lt;p&gt;This isn't unique to this particular library. Module-level mutable state in server-side JavaScript is a pattern that shows up everywhere. Any time a Node.js application shares a mutable variable across request boundaries, there's potential for data leakage.&lt;/p&gt;

&lt;p&gt;The difference here is that this library is explicitly designed to work in SSR environments. It knows that server-side rendering means concurrent requests sharing the same module instances. And yet the default cache is still a module-level singleton.&lt;/p&gt;

&lt;p&gt;I've seen this same pattern in other libraries too. A utility module that stores configuration in a top-level object. A logger that keeps a buffer in module scope. A session manager that initializes once and keeps state between requests. The details vary, but the underlying mistake is the same: assuming that "loaded once" means "safe to share."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Reported
&lt;/h2&gt;

&lt;p&gt;I put together a proof of concept that demonstrated the issue clearly. The setup simulated three concurrent requests from three different users, all using the same cache key. The first user's data ended up being served to the second and third users.&lt;/p&gt;

&lt;p&gt;The output was pretty damning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[VULNERABILITY CONFIRMED]
  User B received User A private data from shared global cache!

[CRITICAL] User C received User A private data from polluted cache!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No elaborate exploit chain. No tricky timing attack. Just three requests happening in the normal course of application usage, and the library's default behavior leaking data between them.&lt;/p&gt;

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

&lt;p&gt;The fix has two parts. First, the library should use something like &lt;code&gt;AsyncLocalStorage&lt;/code&gt; to provide request-scoped cache isolation on the server by default. Second, the documentation needs to be much more explicit about the risks of using the default configuration in SSR environments.&lt;/p&gt;

&lt;p&gt;The first part is a code change. The second part is arguably harder—you can't fix a documentation gap with a patch, and you can't force developers to read the docs you've already written. The real fix is making the secure option the default, so developers don't have to opt in to safety.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Server-side module state is a liability.&lt;/strong&gt; Every time you create a module-level mutable variable in a Node.js application, ask yourself: what happens when two requests access this at the same time? If the answer involves one request seeing another request's data, you have a problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Defaults define security posture.&lt;/strong&gt; An opt-in security feature is not a security feature. If the safe configuration requires the developer to know about the danger and take action, most developers won't do it. Not because they don't care, but because they don't know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrency bugs are often invisible in development.&lt;/strong&gt; If you're testing your app locally, you're probably making one request at a time. The cache never collides because there's nothing to collide with. The bug only shows up in production, under load, when multiple users happen to hit the same cache key at the same time. That's what makes this class of vulnerability so insidious—it's invisible until it's catastrophic.&lt;/p&gt;

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

&lt;p&gt;I can't share the library name or the vendor's response yet—standard responsible disclosure practice. But the pattern is worth understanding regardless of which library it happened to be.&lt;/p&gt;

&lt;p&gt;If you're building React applications with server-side rendering, go check your data fetching libraries. Look at how they manage cache state. If the cache is a module-level singleton and there's no obvious per-request isolation, you might be one concurrent request away from leaking user data.&lt;/p&gt;

&lt;p&gt;And if you're on the security research side, don't just look for code that can be exploited. Look for code that behaves differently under concurrency than it does in isolation. Those bugs are harder to find, harder to trigger in development, and often far more damaging in production.&lt;/p&gt;

&lt;p&gt;The best bugs aren't the ones that require a sophisticated attacker. They're the ones that happen all by themselves, in the normal course of things, while everyone assumes the code is working correctly.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>javascript</category>
      <category>react</category>
      <category>security</category>
    </item>
    <item>
      <title>How I Found an SSRF in an AI SDK's OAuth Metadata Discovery</title>
      <dc:creator>幻灵末士</dc:creator>
      <pubDate>Sat, 29 Aug 2026 07:14:55 +0000</pubDate>
      <link>https://dev.to/thecrazyrabbit/how-i-found-an-ssrf-in-an-ai-sdks-oauth-metadata-discovery-4mkp</link>
      <guid>https://dev.to/thecrazyrabbit/how-i-found-an-ssrf-in-an-ai-sdks-oauth-metadata-discovery-4mkp</guid>
      <description>&lt;p&gt;Some bugs announce themselves. You're reading through a codebase and the vulnerability practically waves at you from the screen. This was not one of those bugs.&lt;/p&gt;

&lt;p&gt;This one required me to ask a question that I almost didn't ask. And that question led down a rabbit hole that ended with a server-side request forgery in a fairly popular AI SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Context
&lt;/h2&gt;

&lt;p&gt;I've been doing security reviews of AI-related tooling lately. Partly because it's obviously where the industry is heading, and partly because the pace of development in that space means security often takes a back seat. New features ship fast. Validation logic gets copy-pasted. Corners get cut.&lt;/p&gt;

&lt;p&gt;The target this time was an SDK that handles authentication for something called MCP servers. If you're not familiar, MCP is a protocol that lets AI applications connect to external tools and data sources. Think of it as a standardized way for an AI assistant to talk to your calendar, your database, or your internal APIs.&lt;/p&gt;

&lt;p&gt;The auth flow I was looking at involves OAuth. The SDK needs to discover where the OAuth endpoints live, so it makes a request to a well-known URL on the server and parses the metadata from the response. Pretty standard.&lt;/p&gt;

&lt;p&gt;Here's where my curiosity kicked in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Question
&lt;/h2&gt;

&lt;p&gt;I was reading through the OAuth implementation when I noticed something that seemed off. There was a function that validated URLs before fetching them. It was doing the right thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertSafeOAuthEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpointUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ... some checks ...&lt;/span&gt;
  &lt;span class="nf"&gt;validateDownloadUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpointUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&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;Good. That's the pattern I want to see. Someone thought about SSRF here and added a guard.&lt;/p&gt;

&lt;p&gt;But then I kept scrolling. And I found another function that fetches URLs during the OAuth flow. It was responsible for discovering OAuth metadata—the part where the SDK asks the server "hey, where are your OAuth endpoints?"&lt;/p&gt;

&lt;p&gt;That function did not call &lt;code&gt;validateDownloadUrl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I stared at it for a minute. Was that intentional? Did the metadata discovery path not need validation for some reason I wasn't seeing?&lt;/p&gt;

&lt;p&gt;I pulled up the call chain. The public &lt;code&gt;auth()&lt;/code&gt; API eventually calls &lt;code&gt;discoverOAuthProtectedResourceMetadata()&lt;/code&gt;, which calls &lt;code&gt;discoverMetadataWithFallback()&lt;/code&gt;, which calls &lt;code&gt;tryMetadataDiscovery()&lt;/code&gt;, which calls &lt;code&gt;fetch()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No validation anywhere in that chain.&lt;/p&gt;

&lt;p&gt;And here's the thing: the URL being fetched comes from the user. When a developer sets up their MCP connection, they provide a &lt;code&gt;serverUrl&lt;/code&gt;. The SDK takes that URL and starts making HTTP requests to it to discover OAuth metadata.&lt;/p&gt;

&lt;p&gt;If an attacker can control that &lt;code&gt;serverUrl&lt;/code&gt;—which is the entire threat model of "connecting to an untrusted MCP server"—then the SDK will happily fetch whatever URL you give it. Including internal addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Wait, Really?" Moment
&lt;/h2&gt;

&lt;p&gt;I didn't trust my initial read. So I built a test.&lt;/p&gt;

&lt;p&gt;I wrote a small mock server that would listen on localhost and log any incoming requests. Then I pointed the SDK at it using its public API, passing in a &lt;code&gt;serverUrl&lt;/code&gt; that should have been rejected if any validation existed.&lt;/p&gt;

&lt;p&gt;The result: my mock server received the requests. The SDK connected to &lt;code&gt;127.0.0.1&lt;/code&gt; without hesitation. It sent the standard OAuth discovery requests to my local service, complete with headers identifying itself.&lt;/p&gt;

&lt;p&gt;That's when I realized this was actually a problem.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;validateDownloadUrl&lt;/code&gt; function existed in the codebase. The developers clearly knew they needed URL validation for OAuth endpoints. But they'd only applied it to one part of the flow—the part where credentials are submitted—and missed the metadata discovery path entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Could Go Wrong
&lt;/h2&gt;

&lt;p&gt;Let me paint the picture.&lt;/p&gt;

&lt;p&gt;A developer builds an application using this AI SDK. They configure it to connect to an MCP server. Maybe that server URL comes from a configuration file, a database, or even from a user-supplied value in their app. It doesn't really matter how. What matters is that the SDK trusts that URL completely.&lt;/p&gt;

&lt;p&gt;An attacker who can influence that &lt;code&gt;serverUrl&lt;/code&gt; can point it at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;169.254.169.254&lt;/code&gt; — the AWS metadata service, which can leak IAM credentials&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;127.0.0.1:6379&lt;/code&gt; — a local Redis instance&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;127.0.0.1:9200&lt;/code&gt; — Elasticsearch&lt;/li&gt;
&lt;li&gt;Any other internal service on the network&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SDK will send HTTP requests to those addresses from within whatever environment it's running in. If the application is deployed in a cloud environment with an attached IAM role, that's credential theft. If it's on a corporate network, that's internal reconnaissance.&lt;/p&gt;

&lt;p&gt;And the attacker doesn't need any special privileges. They don't need to authenticate. They just need to convince a developer to connect to a malicious MCP server, which is exactly the scenario the OAuth flow is supposed to handle safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Second Layer
&lt;/h2&gt;

&lt;p&gt;While I was digging into this, I found something else that made it worse.&lt;/p&gt;

&lt;p&gt;The fetch function used in the metadata discovery path had &lt;code&gt;redirect: 'follow'&lt;/code&gt; enabled by default. That means even if someone added URL validation to the initial request, an attacker could potentially bypass it by having the initial URL redirect to an internal address.&lt;/p&gt;

&lt;p&gt;The SDK would follow the redirect and end up at the internal service anyway.&lt;/p&gt;

&lt;p&gt;This is a classic SSRF pattern: validate the first URL, forget that redirects can take you anywhere. The presence of &lt;code&gt;redirect: 'follow'&lt;/code&gt; meant that fixing the missing validation on the initial URL wouldn't even be sufficient—the redirect behavior needed attention too.&lt;/p&gt;

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

&lt;p&gt;I reported the issue through the proper channels. The fix involved two parts: adding URL validation to the metadata discovery path, and addressing the redirect-follow behavior.&lt;/p&gt;

&lt;p&gt;Simple in concept. But the fact that both layers existed—missing validation on one path, and redirect-follow on another—meant that a naive fix would have left the vulnerability half-open.&lt;/p&gt;

&lt;p&gt;That's actually a lesson I keep learning in security research: the first bug you find is rarely the whole story. There's usually a second layer if you keep digging.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Consistency in security controls is hard.&lt;/strong&gt; The developers had a validation function. They used it in one place. They didn't use it in another place that needed it just as much. This happens all the time, especially in codebases that are growing quickly. The security control wasn't missing—it was just incomplete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redirects are SSRF's best friend.&lt;/strong&gt; If your URL validation doesn't account for redirects, it's not real validation. Fetch APIs follow redirects silently. An attacker who can control the response from a validated URL can redirect you anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Untrusted server URLs are an SSRF waiting to happen.&lt;/strong&gt; If your application fetches URLs that a user or external party can influence, you need to think carefully about where those requests might end up. This is especially true in cloud environments where internal addresses can expose sensitive credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test your assumptions.&lt;/strong&gt; I didn't just read the code and assume the missing validation was exploitable. I built a quick proof of concept that demonstrated the SDK making requests to a local service. If you're reporting a security issue, concrete evidence is worth a lot more than "I noticed something in the code."&lt;/p&gt;

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

&lt;p&gt;I can't share specifics about the SDK or the vendor's response—that's between me and them for now. But the pattern here is worth understanding because it's everywhere.&lt;/p&gt;

&lt;p&gt;SSRF via missing URL validation in OAuth flows. SSRF via redirect-follow. These aren't exotic bugs. They're the result of a simple oversight in a complex flow, hidden in the gap between two functions that were supposed to do the same thing but only one of them did.&lt;/p&gt;

&lt;p&gt;If you're building anything that involves fetching URLs based on user input, go look at every &lt;code&gt;fetch()&lt;/code&gt; call in your codebase right now. Not just the obvious ones. Every single one. Ask yourself: could this URL ever be influenced by someone other than the developer?&lt;/p&gt;

&lt;p&gt;If the answer is yes, you might have an SSRF.&lt;/p&gt;

&lt;p&gt;And if you're on the security research side, the next time you're reading through a codebase and something feels slightly off—a validation function used in one place but not another—don't just note it and move on. Dig in. That inconsistency might be the thread that unravels the whole thing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>security</category>
    </item>
    <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>
