← All writing findings

Anonymous, Allegedly: Reverse-Engineering the 2022 YikYak Relaunch

A responsible-disclosure writeup of privacy and data-leakage flaws in the relaunched YikYak Android app — where 'anonymous' turned out to be a UI decision, not a backend guarantee.

Anonymous, Allegedly: Reverse-Engineering the 2022 YikYak Relaunch

TL;DR — In July 2022 I beta-tested the relaunched YikYak Android app and reverse-engineered how it talks to its backend. The app promises anonymous, hyperlocal posting, but the backend trusted its client far too much: a modified client could send arbitrary GraphQL queries, pull hidden fields (including precise GPS coordinates and the "anonymous" identity markers of incognito users), dump the entire dataset regardless of the client's location, spoof another user's identity on a post, and — with the right malformed input — briefly knock the backend over. Everything below was reported privately to YikYak, discussed with their engineering team, and (per their word) patched. This is a retrospective; the issues described here are old and resolved.


A note before we start

Everything in this post was disclosed responsibly and coordinated with YikYak's team before anything was made public. The vulnerabilities were reported in July 2022 and YikYak confirmed fixes in September 2022. I did this work in my own time and personal capacity — it doesn't represent any employer or organization I'm affiliated with. My goal was never internet points; it was to get real privacy problems in front of the people who could fix them. This writeup keeps things at the what and why it matters level rather than shipping a copy-paste exploit kit, and I've referred to the people I dealt with by role and first name only.

Fair warning: this is an anonymous confessions app, so some of the captured post content is exactly as crude as you'd expect. I've left it in the screenshots because the candor is the point — people say things they'd never attach their name to when they believe they're anonymous and un-locatable. That belief is what these bugs break.


Background: what YikYak is, and why "anonymous" is the whole product

If you were in college around 2013, you remember YikYak: a hyperlocal, anonymous message board where you saw posts ("yaks") from people physically near you. It burned bright, got tangled up in harassment and safety problems, and shut down in 2017. In 2021–2022 it came back.

The product promise is simple and it's the entire value proposition:

  • You're anonymous. There are no usernames and no profiles. On a given post you're identified only by a small emoji + color pairing.
  • It's hyperlocal. You see and post to a feed built from people around you. Location isn't a feature bolted on the side — it's the point.

Put those two together and you get the thing that matters for this writeup: location privacy and identity integrity aren't nice-to-haves for YikYak, they are the product. If an attacker can pull a user's precise coordinates, or tie their "anonymous" posts to a stable identifier, or post as someone else, the core promise breaks.

I remembered the privacy issues from the original 2013 launch and was curious whether the relaunch had learned from them. I got into the first Android beta to find out. It had not.


How I looked: SSL unpinning and a look at the wire

All of YikYak's traffic is HTTPS, and the app ships with certificate pinning — the standard Android control that's supposed to stop anyone (including the user) from intercepting the app's TLS traffic with their own certificate.

The app was vulnerable to SSL unpinning. Bypassing the pinning let me perform a man-in-the-middle on the client using a trusted root certificate and inspect the otherwise-encrypted traffic as cleartext. I'm not going to write the how-to-unpin-this-specific-app tutorial here — that part is well-trodden and, more importantly, beside the point. What matters is what it revealed.

Burp HTTP history showing decrypted GraphQL POSTs to api.yikyak.com
With pinning bypassed, every call to api.yikyak.com/graphql/ is readable in the clear. Here's a Feed operation and its response — distance, interestAreas, createdAt, vote counts and per-post identity fields all coming back to the client over okhttp/4.9.3.

Burp WebSockets history showing decrypted Pusher notification traffic
Real-time notifications ride a Pusher WebSocket, which is just as readable once intercepted — here a new_comment notification payload carrying the yak ID, recipient ID, and message ("New comment on your yak…").

Once I could see the wire, the shape of the system was obvious: the app is a client for a GraphQL backend (Apollo). Every user action — posting (CreateYak), commenting (CreateComment), fetching the feed (Feed) — is a GraphQL operation sent to that backend. Which brings us to the root cause of nearly everything that follows.


The root cause: the backend trusts the client

Here's the single sentence that ties this whole post together (and yes, the mascot warned you):

The backend did not validate that the requests coming from the client were the requests it expected.

Think about the app's actual use cases. A user makes a post, comments, upvotes/downvotes, maybe deletes their own thing. For each of those, the client sends a specific query and the server returns a specific, appropriate slice of data. That's the contract the designers had in mind.

But a GraphQL endpoint doesn't enforce that contract by itself. If the client is willing to send a different query — asking for extra fields, calling operations outside those use cases, or requesting data the UI would never display — the backend happily answered, because nothing was checking whether the request made sense for a legitimate client.

And since I controlled the client (via the MITM), I could send whatever I wanted.

Attack flow: client to MITM proxy to GraphQL backend, where the request is never validated

Everything from here is a variation on that theme.


The findings

1. The GraphQL endpoint was an open playground

Because the endpoint answered arbitrary queries, I could interrogate it about itself.

Schema dump (introspection). A single introspection query — { __schema { queryType { fields { name } } } } — returned the backend's entire query surface.

GraphQL introspection returning the top-level query fields
The backend cheerfully lists its query root: yak, allYaks, feed, me, user, usernameAvailability, notifications, node. Two of those — allYaks and usernameAvailability — are about to become very interesting.

That's a complete map of the data model. And usernameAvailability is a tell: it implies a username system existed under the hood of an app that presents itself as purely emoji-anonymous.

Helpful "suggestions." As a bonus, when I queried for fields that didn't quite exist, the endpoint returned "did you mean…?" hints — quietly handing over valid names and confirming that tell about usernames.

GraphQL error suggesting the correct field name
"Cannot query field name on type User. Did you mean username?" — free schema discovery, and a second confirmation that User objects carry a username.

Reading the schema I could also see the outlines of features that had nothing to do with the shipping client — video-upload mutations, for instance, which the app didn't do. Leftover, in-development, and orphaned functionality was all reachable through the same over-permissive endpoint.


2. allYaks: the entire dataset, regardless of where you are

The allYaks operation surfaced in the schema returns every yak coming into the system, in what looked like real time, independent of the client's geolocation.

Sit with that against the product design. YikYak's feed is supposed to be a curated, hyperlocal slice. There is no legitimate reason a random end-user client should be able to say "forget my location — give me everything." But it could.

allYaks query returning posts nationwide with GPS points
My client claims to be in Mountain View (POINT(-122.084 37.42)), but allYaks returns posts from all over — this incognito one sits at [-118.99, 34.23] near LA. Every node carries a point, so exact per-post GPS comes right back.

It gets worse when you ask for everything a node exposes:

allYaks queried for every available field, including userId, geohash, and nested comments
The same operation, queried for the full field set: userId, userEmoji, userColor, geohash, interestAreas, precise coordinates, even nested comments. Note the request location is POINT(-0.0 0.0) — null island — and the firehose flows anyway.

Here's the kicker, and it's the single most important finding: isIncognito: true posts still return userEmoji, userColor, userId, and coordinates. Incognito hides those from the UI; it does nothing at the API. Combine "give me all yaks" + "include the coordinates" + "include the stable userId" and you have a data-mining and user-tracking primitive: pull everything, filter by a user's ID, and reconstruct where a specific "anonymous" person has posted from. For an app whose whole identity is anonymous and hyperlocal, this is about as fundamental a break as it gets.

During disclosure, YikYak noted the coordinates were approximated / rounded rather than pinpoint — a mitigation I believe came out of an earlier researcher's report. That shrinks the blast radius, but "approximate location of an anonymous user, plus a stable ID, exposed to any client that asks" is still a serious location-privacy exposure at scale.


3. Breaking anonymity and impersonation via emoji/color

On a YikYak post, the only thing other users see identifying you is your emoji + color pairing. So those markers are, functionally, identity — and I could set them to arbitrary values on my own posts and comments, overriding whatever the backend assigned me.

CreateComment mutation with a custom taco-emoji identity
A CreateComment mutation where I've set userEmoji to a string of 🌮 and picked my own userColor/secondaryUserColor. None of it was assigned to me; the backend accepts it and echoes it straight back.

The spoofed comment rendered in the app
…and it renders in-app under the identity I chose.

Same trick, different disguise:

CreateComment mutation using a penguin emoji identity
Another comment, this time wearing a penguin 🐧 I picked.

The penguin-identity comment as other users see it
How it looks to everyone else in the thread.

I couldn't (as far as I found) permanently rewrite the identity globally assigned to my account, but I could sidestep it per-post. In an anonymous community where the emoji is the identity, being able to post under an arbitrary marker is an impersonation problem — and because I could also read other users' emoji, colors, and userId (incognito included, per finding #2), those "anonymous" markers can be harvested and correlated rather than staying anonymous.


4. Empty emoji and blank posts

By setting the emoji to braille-blank characters (U+2800) and the text to the same, I could publish yaks with no identity marker and no content.

CreateYak mutation with braille-blank emoji and blank text
A CreateYak with userEmoji set to a run of \u2800 and text set to a single blank. The server validates none of it and creates the post.

The local feed filled with an empty, marker-less post
Repeat at will and the local feed fills with empty, un-attributable posts — a cheap way to flood a community with a wall of nothing.


5. The part that actually worried me: deanonymizing sensitive users

This one goes beyond "college campus gossip," and it's the finding I most wanted YikYak to internalize.

YikYak labels posts with an interest area — a general geographic label for where the user is. On a campus that's the university's name. But scrolling the (globally queryable) feed, I kept seeing posts whose interest areas were U.S. military and government facilities, each carrying real coordinates.

An incognito post tagged to the Army's National Training Center at Fort Irwin
An incognito post tagged interestAreas: "National Training Center/ Fort Irwin" at [-116.68, 35.26] — the U.S. Army's premier desert-warfare training center.

A second post from the same military facility
Another from the same facility. People post candidly when they think they're anonymous and only visible to those nearby — the coordinates and label say otherwise.

An incognito post from Denver International Airport where the author identifies as TSA
And one tagged to "Denver International Airport" whose author volunteers that they work TSA. Interest-area label + coordinates + candid venting = a targeting signal.

Here's the privacy chain of custody: people post more openly when they believe they're anonymous, un-locatable, and only visible to others physically nearby. YikYak reinforces that belief. But if any client can (a) pull the global feed, (b) filter by interest area to home in on sensitive facilities, and (c) attach a stable userId and coordinates, then a bad actor can target specific sensitive populations and glean information that connects dots which were never meant to be connected.

To be clear, it is emphatically not YikYak's job to police what a service member or federal employee chooses to post. But labeling where they are while they do it and making that queryable to anyone who asks turns a personal privacy tradeoff into a targeting surface. That's a categorically heavier issue than knowing the vibe of a college campus, and it's the example I led with when I sat down with their engineers.


6. Crashing the backend

By sending a feedType value the backend couldn't handle, I could take feed retrieval down.

A feedType of NATIONWIDE returning HTTP 500 Internal Server Error
A Feed operation with "feedType":"NATIONWIDE" returns an HTTP 500 and a raw Django "Server Error (500)" page. While it was choking, my client — emulator and physical device alike — couldn't pull the feed for a few minutes at a time.

A short, self-healing outage, but a reminder that unvalidated input reached code paths that weren't ready for it.


7. Odds and ends

A public Django/Pusher auth endpoint. I found a public Pusher Auth endpoint served by the Django REST Framework.

The browsable Django REST Framework Pusher Auth page returning 403
GET /pusher/auth/ — it 403s without credentials, but the browsable DRF page is exposed, confirming the stack. (YikYak's backend work was handled by a third-party Python/Django shop, which lines up with everything I saw.)

Not a bug, just funny. The local YikYak community had no idea an Android client was even in the works.

In-app thread where users insist YikYak isn't on Android
"y'all on iOS or android?" → "yik yak isn't on android bro." Filed under observations, not vulnerabilities — I was posting from the Android beta at the time.


The meeting: sitting down with YikYak's engineers

On July 20, 2022 I had a video call with YikYak's engineering side — I'll call them Nico (Head of Engineering) and James (a backend engineer from the third-party firm that builds their backend). It was a genuinely good conversation, and worth summarizing because the human side of this disclosure is as instructive as the technical side.

A few things that stood out:

  • They were receptive and grateful, not defensive. That's not a given, and it made the whole thing work.
  • They already knew the root cause was real. In their words, some APIs had been created early in development with overly generous scope that the client ended up leaning on, plus leftover bits of old features that were never cleaned up — including the emoji handling. That matches exactly what I saw: over-permissive queries and orphaned functionality reachable through the same endpoint.
  • It signaled immature prod/dev hygiene. If old feature artifacts and in-development operations are live and queryable, there probably isn't a clean separation and process keeping production in a known-good state.
  • This was new territory for them. Nico said it was his first time operating in the security-disclosure space. Not a knock — context. They didn't yet have a security mindset baked into engineering, and they seemed to know it and want to fix it.
  • They asked the good questions: how did you do this, and what should we do about it? We talked through fixes — validate what comes from the client, scope APIs to actual client use cases, stop treating "we use HTTPS" as the end of the security conversation, handle location server-side so a client can never see precise per-user coordinates, and add logging + alerting + human review so anomalous requests don't just rot in a log unseen.

We also spent real time on disclosure timelines, because they had no policy. My take, with the caveat that policy isn't my wheelhouse: coordinated-disclosure norms usually run 30–90 days from acknowledgment, the whole thing runs on good-faith communication both ways, researchers are almost always happy to extend when you ask and keep talking, and the cases you hear about where someone "just publishes" are typically ones where the company went dark. A clear public reporting policy — a point of contact and simple in/out-of-scope rules — removes a lot of the fear on the researcher's side and gets a company better reports.

I made it explicit that I wasn't going to publish anything until they were comfortable, that I had nothing written up at the time, and that I was happy to keep it confidential and verify any fixes they wanted checked.


Disclosure timeline

Date Event
Jul 3, 2022 Reached out to YikYak support asking for a bug-bounty / responsible-disclosure process.
Jul 5, 2022 Support said there was no formal process, but I was welcome to report findings to them.
Jul 6, 2022 Initial disclosure of findings sent to support.
Jul 7, 2022 Support confirmed the report was passed to engineering.
Jul 7, 2022 Heard directly from YikYak's Head of Engineering.
Jul 18, 2022 Video call scheduled with the engineering team.
Jul 20, 2022 The meeting (above) took place.
Jul 29, 2022 Requested a status update.
Aug 31, 2022 Requested a status update.
Sep 2, 2022 Engineering said they were working on fixes.
Sep 7, 2022 YikYak said the reported issues were patched, and mentioned standing up a HackerOne bug-bounty program.
Oct 17, 2022 Asked for an update on the HackerOne program.
Oct 18, 2022 They'd been steered away from HackerOne (their use case was "too simple"); a security contact said they'd be open to a direct line for reporting issues, with compensation.
Oct 20, 2022 Replied that I was interested in that direct line for future reporting.
Nov 29, 2022 Followed up to reiterate interest.
Jan 24, 2023 No further communication received.

So the arc was: reported → engaged well → confirmed patched → floated a paid reporting relationship → and then it quietly trailed off. I hold no grudge about the last part; the important thing — the privacy problems getting fixed — happened. But it's an honest illustration of how these things often end, even the ones that start great.


Takeaways

If you build software, a few of these are worth tattooing somewhere visible:

  1. The client is not a trust boundary. Anything your app can send, an attacker can send — differently. Validate on the server that requests match the operations and fields a legitimate client should be making. "The UI doesn't request that field" protects nobody.
  2. "We use HTTPS" is table stakes, not a security posture. TLS protects data in transit from third parties. It does nothing about a malicious first party who controls the client, and cert pinning that can be unpinned buys you less than you think.
  3. GraphQL will tell on you if you let it. Introspection, over-generous schemas, and helpful error suggestions are wonderful in development and dangerous in production. Lock introspection down, scope your resolvers, and don't expose operations (allYaks-style firehoses, half-built features) that no legitimate client needs.
  4. Anonymity and location privacy are things you engineer, not things you label. "Incognito" that only hides fields in the UI is theater. If precise coordinates or stable identifiers exist server-side, assume a determined client will ask for them. Compute the minimum the client needs (a rounded distance, not a point and a userId) and never send more.
  5. Have a disclosure policy before you need one. A named contact and simple scope rules turn scary, ambiguous emails into useful, coordinated reports — and make researchers want to help you.
  6. Culture is a control. The best outcome here came from a team that listened, asked good questions, and admitted what they didn't know. You can buy a lot of security maturity with humility.

At the end of the day, I do this kind of thing for fun, and the reward is watching real problems get fixed before they hurt someone. Thanks to the YikYak folks who took the meeting and did the work.


Found this useful, or spotted something I got wrong? I'm always happy to talk shop.

Much love,

<3