Exploiting Tinder to get paid features for free


By:
Elian Cordoba

What is this?

In this article, I will be focused on the things I found and thought the process I went through in my adventure with, arguably, the most popular dating app, Tinder.

Most likely this will not help you find a partner but I hope it sparks some curiosity to understand how things work behind the scenes in the Tinder app.

If you are someone related to Tinder please read the conclusion at the bottom.

Post highlight:

You can see ALL the pictures of the people who liked you UNBLURRED by looking at the response of the teasers request that gets fired when you click on the button that open the list of thouse people.

But wait, who are you?

Glad you ask, I'm Elian Cordoba and like my friend Sam, I'm a full-stack web developer, doing mostly Angular, Ionic and Node, but I'm not scared of the JS framework/library/tool that is trending at the moment of reading this.

You can find me on github and reach me via email.

How did I end up here?

I always try to snoop around to see if I can find interesting things, this time was Tinder’s turn. I started using the web version because I felt lonely for some reason I got logged out from the mobile version and I couldn't log back in (In the web you can use Facebook to do so).

Once in, the button with the number of people who liked me caught my attention (Not everyone will have it though 😕).

After clicking on it, it opened a page with the list of people but with a catch, their profile pictures were blurred.

To see them properly you have to pay a monthly subscription.

So I thought, well most likely these photos came already blurred from Tinder's backend, right?

Well.... no, they come unblurred and get the effect in the frontend with one CSS class, ouch.

Css that disable the blur effect
Click inspect element on one of the portraits and uncheck those two styles
Blured people list Unblured people list
Just like magic!

This is pretty bad for them because anyone can get one of the main Tinder's gold features for free pretty easily*.

Also, this wasn’t complicated to prevent either**, they could have stored a blurred image already or apply the effect it before sending it.

Something like sharp can do the job just right, assuming they are using Node.js for the backend, if they don't but still like that package a microservice would work fine.

Coincidentally, moments after discovering this I got liked by someone and by looking into the actual response I could see her photo and later I recognize her on my swipe list.

To be honest, it ended up being a double-edged sword because I also found out that a really cute girl liked me and for some reason, I did not like her back 😔

*Is not as good as the real feature, you don’t get to see the person’s profile information such as the name or bio.

**Both solutions I'm about to mention, as many things in life, have tradeoffs, the first one they would use more storage per user and also will have update it whenever the user updates its main profile picture. The second one will introduce overhead on the response, which may be a problem considering the number of users they have, although not everyone will trigger it

Another interesting thing is that the teasers request (The one that gets the list of people that liked you) not only has the URL of the portrait image but all the URLs of their pictures, ouch again.

This could have been prevented by doing a projection in the query or deleting the unused properties.

The extra data* burden the response so much that makes it weight 4 times more.

*To be honest I’m not 100% sure of what is used in the frontend because I don't work at Tinder but, knowing that the request gets fired when you click on the button to see who liked you and they only show one picture it's safe to assume that they could omit all other data.

**The 4x extra weight claim comes from taking the original response (~54 KB) and removing all other properties but the portrait picture (Final size ~11.5 KB).

Give me moar 🔥

With this sort of eureka moment my already high curiosity got even higher, the next thing I wanted to know was how Tinder's swiping worked.

When you load the page the core request gets fired, bringing with it an array of 16 users (Fired again if you swipe them all). Remember this, we will come back to it in a bit.

Next, I tried was to do a like and a pass they were…. GETs... seriously? Anyway, the URLs are:

GET - api.gotinder.com/like/ID_PERSON
GET - api.gotinder.com/pass/ID_PERSON

And the superlike is:

POST - api.gotinder.com/like/ID_PERSON/super

I didn't find any utility for the pass and superlike but a really good one for the like, again, keep on reading, we still need one extra piece to solve one of the puzzles!

Messing around with the persistent storage 💽

Another one of the useful Tinder's premium features is that you can redo a swipe, well we can also hack our way through to get this one for free too by using what we just learned.

To do so, go to the IndexDB storage and then keyval:

Firefox indexedDB Chrome indexedDB

Firefox on the left, Chrome on the right

Look for the key persist::recs which will have the following structure:

{
  "previouslySwiped": [
    {
      "id": "5d61ab62a0d7e91610c0b0c6",
      "rating": "like",
      "timestamp": 1566769731872,
      "sNumber": 793832917
    },
    {
      "id": "5c6b475172e7651200a590b2",
      "rating": "dislike",
      "timestamp": 1566781244135,
      "sNumber": 691913683
    },
    ....
  ]
}

Tip:

To reproduce and modify any request go to the Network tab, right-clicking on it and then Copy as fetch. Then go to the console, paste it and hit enter. At the end of the post there is a gif doing just that.

So, we just have to take the ID of the person that we want to show our interest to and put it in the like request:

fetch(
  'https://api.gotinder.com/like/5a94cc13b191566e1c13a85e?locale=en&s_number=489904711',
  {
    credentials: 'omit',
    headers: { ... }, // !important, copy the headers from a recent 'like' request, as they your session data
    referrer: 'https://tinder.com/',
    referrerPolicy: 'origin',
    body: null,
    method: 'GET',
    mode: 'cors'
  }
);

On a side note, I also found that when you have a match* it lets you chat with that person, by clicking on their profile you trigger the usual get by ID.

This is useful because if you want to redo a like but you are not sure which ID is the correct one, with this you can check it.

fetch('https://api.gotinder.com/user/ID?locale=en', { // The ID goes here
  credentials: 'omit',
  headers: {...}, // Same thing here as explained in the last last example
  referrer: 'https://tinder.com/',
  referrerPolicy: 'origin',
  body: null,
  method: 'GET',
  mode: 'cors'
});

*It's that beautiful moment when you like someone and it also happens that the like you too. Truly wonderful.

Hacking the 'save profile' section 🕵️

Of course, when you can update some existing values there's the possibility that the devs don’t validate in the backend what you are sending, so you could alter the payload to do something like:

{
  "firstName": "Elian",
  "lastName": "Cordoba",
  "account": {
    "balance": 9007199254740991 // Gotta stay safe
  }
}

Most likely your home banking has this covered but, Tinder is not a home banking so I tried anyway.

I found that in Tinder's web version you can’t change your city (In the mobile app you can), but you can edit the payload to do so:

Tip:

You can create your own cities! Trigger a profile update to copy the request and then modify the payload.
{ 
  "user": { 
    "city": { 
      "name": "Area 51", 
      "region": "20 september never forget" 
    } 
  } 
}

To be fair this is a difficult one to validate since you depend on some library or service on the frontend to get the valid values (In this case most likely Google Map API).

To prevent this they would have to also call the same service in the backend to check if whatever the user is sending is valid but, let's be honest, I don't think that creating your own cities is such a big deal to do that.

Also, the phone number is stored as... phone_id ¯_(ツ)_/¯

Just for fun, I tried to do some XSS but it turns that they have that covered.

Example of a sanitized input
You got me on this one

Random bits

  • I was talking with one girl after a match and for some reason she deleted all her photos No, it wasn't because I creep her out but I had copied her profile as a JSON Okay that may be considered creepy and because of that I tried to get one of her picture URLs and… they were still there. Most likely Tinder have the rights to hold them for some time (maybe forever, read terms and conditions kids) but it’s a reminder that we left a lot of data on the internet, even when we stop using that site/app.

  • The superlike request gets validated on Tinder's backend, I tried modifying my profile data to add me some of these powerups but it also gets validated.

  • When you put a wrong code in promo code input the status code of the response will be a 500, am I the only one to feel it like a microaggression? Jokes aside this one has some implications, if they have some error monitoring it's likely the will register 5XX errors, so you could trigger some alarms by spamming this request. No, don't do it.

  • You cannot like yourself 😢

  • Once someone like you, sooner than later you will encounter them if, for some reason, you don’t want to either like nor dislike them (coward) you can reload the page, don’t worry they will appear again later. If you want to be sure of that just save their ID so you can trigger the match via the console (Example below).

  • Sadly the teasers response does not come with the person ID, otherwise, we could have reproduced the full paid feature by not only getting the photos but also all of their information.

  • To improve your chances of getting to know someone, you can socialize do a script!!11

async function partnerFinder() {
  const carefullySelectedCandidates = await fetch(...); // The 'core' request
  
  const ids = carefullySelectedCandidates.data.results.map(user => user._id);
 
  await Promise.all(ids.map(id => fetch(...id))); // The 'like' request
 
  partnerFinder(); // Oh sh*t, here we go again
}

There is a 100 likes limit which doesn't seem to get triggered if when you use the site normally but, if you do hundreds of request per minute most likely they will stop you. So combine this with 'script' with a CRON job that runs every X* and you are good to go. Also, it will be better if you do them one by one and with some random delay in between, you know, to try to distract any possible simple DDos or bot detector.

*X been whatever Tinder says is the reset time for the likes.

Triggering a match from the console
Triggering a match from the console

Conclusion

To be clear the objective of this post is not to make Tinder lose money or to promote this sort of behavior (Exploiting paid features for free), in my opinion, it could be considered a soft version of piracy.

My objective was and it will always be to learn, in this case, by reverse-engineering the Tinder's site, a skill that I consider very important for software development.

I didn't disclose these findings because they are not security-related as far as I'm aware.

I'm done with this 'research' project, I thought about doing an extension to auto-reveal the pictures or to auto-like people but it contradicts what I said in the last paragraph, that doesn't mean if someone does something related to this I won't check it out, just let me know!

Finally, I would like to encourage everyone to always try to see what's going on under the hood, to see what request and responses (Sometimes they carry extra data that shouldn't be there), to the sources (Sites may update their code with source maps, ouch), check the console for logs and variables, etc.

I like to think about it as it is a treasure hunt, you never know what you will find!

Get the latest articles in your inbox.

Join the other 2000+ savvy node.js developers who get article updates. You will receive only high-quality articles about Node.js, Cloud Computing and Javascript front-end frameworks.


ElianCordoba

Fullstack dev, young and passionate. Doing mostly Angular, Ionic and Node, but I'm not scared of the JS framework/library/tool that is trending at the moment of reading this. Looking for new challenges ;)