How I single-handedly bypassed #OpenSea's backend security model for profit. 🧵 👇

tl;dr code at github.com/cawfree/opense…
For some background, I've been trying to code the upper hand on #NFT marketplaces for a couple of years now:

github.com/cawfree/opense…
github.com/cawfree/opense…

These are some projects that made it, but there's a mountain of failed attempts that lay hidden behind the scenes.
I would scrape OpenSea's webpages and extract meaningful content from them.

Since most of the content is lazy-loaded via infinite scrolling, I'd use #puppeteer to scroll through the pages for me.

github.com/puppeteer/pupp…
Here, @opensea would heavily, dynamically obfucscate returned HTML for each requester. This made the challenge of parsing them into sanitized data models far more inconvenient, since it was difficult to pivot programmatic assumptions on.
@opensea Now before we get deeper, you may wonder why was I doing this since @opensea already maintain a rich public API? 🤔

Well, I needed to access data that wasn't officially served via the API. Furthermore, I needed to request that data in *ways* that weren't supported.
@opensea As a concrete example, if I wanted to monitor the floor price of the top #NFTs, the official API would require I specify the exact collections I was interested in watching.

But in my case, I needed to watch *all* of them. 👀
@opensea Another time, I was also intrigued by the possibility of pump-and-dumps organized by certain token-gated groups, and wanted to export all of the holder addresses:

@opensea And above all else, I wanted to know this information faster than anyone else. 🔥

In #Ethereum, miners nearly always have the advantage because they watch every transaction which comes through the mempool.
@opensea Every listing you see on the OpenSea however, lives in a centralized database. Whoever gets the information out of there first is in the privileged position to act upon it. 🏆

I didn't need #Solidity; just plain old #JavaScript.
@opensea Back when I was scraping user addresses, it would take a couple of minutes to export maybe twenty or thirty wallets.

Collections have thousands of holders, so web scraping was no longer scaling.

So I hit a brick wall. 🧱
@opensea I spent some time analyzing network requests made by OpenSea from within the browser, and was surprised to find that it was impossible to source the same information surfaced via the frontend using just the @apiopensea SDK.

They were using #GraphQL. 🧪
@opensea @apiopensea I tried replicating these requests using #cURL, but no luck.

OpenSea's GraphQL API is protected by @cloudflare, who succeeded in making the task of forging a request exponentially difficult.

But then I had a breakthrough. 💡
@opensea @apiopensea @Cloudflare I'd been using #puppeteer to interact with OpenSea this whole time.

Surely I could just use puppeteer to trick the frontend into making requests on my behalf; since the website clearly had all the information it needed to format a proper request.
@opensea @apiopensea @Cloudflare Before I go any further, huge shoutout to #puppeteer-extra's stealth extension! 🎸 🙏

Without this amazing tool, OpenSea was easily dismissing my monkey business 🍌:

github.com/berstend/puppe…
@opensea @apiopensea @Cloudflare Using #puppeteer's evaluate() function, I could 200 OK a totally arbitrary fetch() request that originated from the OpenSea frontend... even ones that targeted their private backend.

🤯 🤯 🤯
@opensea @apiopensea @Cloudflare I don't know if there's a name for this technique, it's not quite #XSS.

Back in the coding dungeon, I've been kicking around the name "Same Origin Resource Crossing (SORC)".
@opensea @apiopensea @Cloudflare I was extremely happy with this discovery and made efforts to quickly generalize. 🥳

I served the exploit as an @UseExpressJS middleware that masqueraded identically to OpenSea, and served @GraphiQL alongside it for steez. 🛹
@opensea @apiopensea @Cloudflare @UseExpressJS @GraphiQL I launched the exploit server, loaded up GraphiQL and wrote my very first query.

It didn't work. 😐
@opensea @apiopensea @Cloudflare @UseExpressJS @GraphiQL I figured I must have typed something wrong. I copy a working query from @Brave's Networking tab. It succeeds.

I introduce a non-breaking change to that working query.

It still doesn't work. 😬
@opensea @apiopensea @Cloudflare @UseExpressJS @GraphiQL @brave I needed to go deeper, so I export the unobfuscated source code from OpenSea.

Yes, that's right. Many, many crypto projects don't realise they're serving their plaintext source alongside their mangled application.
@opensea @apiopensea @Cloudflare @UseExpressJS @GraphiQL @brave I exported the source code using Chrome's Save All Resources extension, and grep'd through the codebase for anything interesting.

I'd find mentions in the comments of previous coders who would eventually begin to feel like colleagues. @xanderatallah
@opensea @apiopensea @Cloudflare @UseExpressJS @GraphiQL @brave @xanderatallah I eventually stumble upon it.

The hash of GraphQL query must first be cryptographically signed by OpenSea. If you write your own custom GraphQL; tough luck. This is why their queries had worked, but my tiny deviations would fail.
@opensea @apiopensea @Cloudflare @UseExpressJS @GraphiQL @brave @xanderatallah When you query OpenSea GraphQL, this signature must be sent alongside. This is the "x-signed-query" header, whose value can only be generated by internally. 💭

This felt like the nail in the coffin. ⚰️

There's no way I could work around it. 😔
@opensea @apiopensea @Cloudflare @UseExpressJS @GraphiQL @brave @xanderatallah Except... 😏

The frontend somehow knew how to do it, and I had all of the frontend code already. So I went looking, and discovered that every signature for every query made by the application is stored in a look up table:
@opensea @apiopensea @Cloudflare @UseExpressJS @GraphiQL @brave @xanderatallah This was the missing piece needed to help me remotely execute every possible query on OpenSea.

All without an API key. 🔑

• • •

Missing some Tweet in this thread? You can try to force a refresh
 

Keep Current with 🦇🔊 cawfree.⟠ ᵍᵐ

🦇🔊 cawfree.⟠ ᵍᵐ Profile picture

Stay in touch and get notified when new unrolls are available from this author!

Read all threads

This Thread may be Removed Anytime!

PDF

Twitter may remove this content at anytime! Save it as PDF for later use!

Try unrolling a thread yourself!

how to unroll video
  1. Follow @ThreadReaderApp to mention us!

  2. From a Twitter thread mention us with a keyword "unroll"
@threadreaderapp unroll

Practice here first or read more on our help page!

Did Thread Reader help you today?

Support us! We are indie developers!


This site is made by just two indie developers on a laptop doing marketing, support and development! Read more about the story.

Become a Premium Member ($3/month or $30/year) and get exclusive features!

Become Premium

Don't want to be a Premium member but still want to support us?

Make a small donation by buying us coffee ($5) or help with server cost ($10)

Donate via Paypal

Or Donate anonymously using crypto!

Ethereum

0xfe58350B80634f60Fa6Dc149a72b4DFbc17D341E copy

Bitcoin

3ATGMxNzCUFzxpMCHL5sWSt4DVtS8UqXpi copy

Thank you for your support!

Follow Us on Twitter!

:(