Exploiting Tinder to get paid features for free
Tags: hacktoberfest, javascript, node, tinder
Published: 2019-10-03 00:00:00

---
layout: post
title: "Exploiting Tinder to get paid features for free"
subtitle: "Reverse engineer your way through the paywall and have something interesting to say in your next date"
author: ElianCordoba
date: "03-10-2019"
image: https://thepracticaldev.s3.amazonaws.com/i/gwvi429uuaq21ms9p8kb.jpg
tags: [ "Hacktoberfest", "Javascript", "Node.js", "Tinder"]
twittertags: [ "javascript", "100daysofcode", "webdev", "infosec", "Tinder", "hacking", "codenewbie" ]
draft: false
published: true
---

_Originally posted on [Software on the road](softwareontheroad.com/utm_source=dev-to-post&utm_medium=group_post). Check it out for more technical post and to see our consulting services._


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.

Most likely this will not help you find a partner but I hope it sparks some curiosity to understand how things work. 

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

> Post highlight:
> You can see <b>ALL</b> the pictures of the people who liked you <b>UNBLURRED</b> by looking at the response of the <i><u>teasers</u></i> 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](https://github.com/ElianCordoba) and reach me via [email](mailto:business.eliancordoba@gmail.com).

## 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 the 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](https://thepracticaldev.s3.amazonaws.com/i/jwis5rzpyxycu5mjquhb.png)

<i>Click inspect element on one of the portraits and uncheck those two styles</i>

![Blured people list](https://thepracticaldev.s3.amazonaws.com/i/inzi6c9txa2igbozxdx5.jpg)
![Unblured people list](https://thepracticaldev.s3.amazonaws.com/i/blxuhfo9eejitpoffuuy.jpg)
<i>Just like magic!</i>

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](https://github.com/lovell/sharp) can do the job just right, assuming they use Node 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 the 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…. **GET**s... seriously? Anyway, the URLs are:

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

And the superlike is:

```javascript
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 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](https://thepracticaldev.s3.amazonaws.com/i/glzp8wy0ouvwhznsginz.png)
<i>Firefox</i>
![Chrome indexedDB](https://thepracticaldev.s3.amazonaws.com/i/2wuhn7rahfdt2yl0sx7o.png)
<i>Chrome</i>

Look for the key **_persist::recs_** which will have the following structure:

```json
{
  "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 <i>Copy as fetch</i>. 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:

```javascript
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.

```javascript
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:

```json
{
  "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 the 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.

```json
{ 
  "user": { 
    "city": { 
      "name": "What's Obama's last name?", 
      "region": "Kekistan" 
    } 
  } 
}
```

_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](https://thepracticaldev.s3.amazonaws.com/i/kndwkho5zk7olkv5wnwl.png)
<i>You got me on this one</i>

<h2>Random bits</h2>

- 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 the 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

```javascript
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](https://user-images.githubusercontent.com/34920585/65845096-32ee3f80-e30f-11e9-89f2-fcf4196f15a8.gif)
<i class="caption">Triggering a match from the console</i>

## 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 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!