For security reasons, the actual web services involved in this will not be named, so for the sake of this article I will invent a fictional online game store, called Theseus Games (TG) (https://theseus-games.co.uk).


I recently tried to buy a new game from TG which unfortunately was only available to Premium members (TG has two membership levels, Basic and Premium), and the Premium membership was expensive as hell. I bit my nails for a while and wondered what I could do since my future happiness seriously depended on playing several hours of said game as soon as possible. Then I realized, shit, I’m a software engineer and I decided to see if I could just play around with TG’s services for a while and just see how things worked.

I reloaded the game page with my Chrome DevTools open in order to see what requests the website makes, just to ‘educate’ myself. First thing I noticed was that TG uses AJAX to load some of its Javascript assets, about a dozen in number. Two of these scripts caught my eye: user.js and purchase.js, and I swiftly opened them up to see what treasures were buried within.

Snippet from https://theseus-games.co.uk/assets/js/user.js

componentDidMount() {
    this.retrieveContactInformation();
    this.retrieveUserInfo();
}

retrieveUserInfo() {
    axios.get(this.appUrl + '/user').then(
        function (response) {

            this.setState({
                application: response.data.application,
                active: response.data.active,
                isLoaded: true
            });

        }.bind(this)
    );
}

Of course, I had built several apps with React and React Native and I immediately realized that the web frontend was built with React, communicating with a Laravel backend (I got this from the session cookies). If you know a little Javascript (or pretty much any web language haha), you can work your way through these snippets.

Snippet from https://theseus-games.co.uk/assets/js/purchase.js

purchaseGame = (evt) => {
    evt.preventDefault();
    if (this.state.status == 2 || this.state.status == 33) {
        this.setState({
            isLoaded: false
        });

        var newGame = {
            gameId: this.state.gameId,
            session: this.state.session
        };
        
        axios({
                method: 'post',
                url: this.appUrl + "/purchaseGame",
                data: newGame
            }
        ).then(
            function (response) {
                this.setState({
                    isLoaded: true
                });
            }.bind(this)
        );
    } else {
        this.setState({
            message: 'Sorry! This game is only available to our premium members.'
        });
    }
}

From purchase.js (line 3) and user.js (line 12) I discovered that apparently user membership information were indicated through an active field in a User entity, and only users where active was either 2 or 33 could be allowed to get the game. Of course the numbers must represent membership levels, and either 2 or 33 indicated Premium membership, and since I was getting the message: ‘Sorry! This game is only available to our premium members.’, my active field most definitely wasn’t 2 or 33.

Futher down the line in the DevTools request tab, I saw the actual GET /user API request, and I found this response:

Response for GET https://theseus-games.co.uk/application/user

{
    "application": "182718048",
    "active":22,
    "userId":60088
}

At this point, I realized that the gates of heaven would need just a little prodding before they were opened wide for me and I thought, there should be something I can do to tamper with this baby.

Immediately, I fired up Postman in order to make API requests to update my active field. From DevTools, I copied out my current session cookie and the ‘X-XSRF-TOKEN’ header value as well into Postman, and I tried to make a PUT request to __ with payload:

Payload to PUT https://theseus-games.co.uk/application/user

{
    "application": "182718048",
    "active":2,
    "userId":60088
}

But I immediately received a 405 (Method Not Allowed) error. Apparently, users are updated in a different route. In turn, I tried POST /application/updateUser, POST /application/updateUserInfo and got 404s for both. At this point, I decided to abandon this approach entirely and try something else. (If I wanted to continue, I’d have inspected the other *.js files that were downloaded via AJAX to get the specific route for updating user, or just go to my account page and try to update my account, then retrieve the route; but there are many ways to skin an elephant haha.)

I have this cool Chrome extension called Tamper Chrome which is used to (surprise!) tamper with requests in the browser, and modify request headers, payloads, etc. The next approach I tried involved me tampering with the actual API requests, and there were two things I could easily try:

1. Download a copy of _, and make no changes except in the if condition checking the active field and then place it in my local server (e.g. _/var/www/html/web/purchase.js), then modify the outgoing request to __ and change the request path to __, so it serves my local version instead.
For my local modification, I could simply change:

if (this.state.status == 2 || this.state.status == 33) {...}

to:

if (true) {...}

2. In a similar manner to (1), I could serve a local version of __ and simply return a user object with the active field conveniently modified, then change the request path to my local version. I mean something like:

Response for GET http://localhost/api/user.json

{
    "application": "182718048",
    "active":2,
    "userId":60088
}

I opted for (2) and did this with the beautiful json-server node package. I created a simple test.json file with contents:

{
    "user": {
        "application":"182718048",
        "active":2,
        "userId":60088
    }
}

Then I started json-server:

Start Json-Server
Started json-server

Next, I activated Tamper Chrome in order to reroute all further requests from current tab:

Activated Tamper Chrome
Activated Tamper Chrome

And I reloaded the page. Of course, the AJAX request for __ soon came up, and I simply modified that to my local json-server:

Tampering with Tamper Chrome
Tampering with Tamper Chrome

Of course, the request hit my json-server which happily responded with a few sweet bytes.

After this request, I decided to click the Purchase button again, and of course, all the frontend could see was that I’m a privileged TG member (haha!); there was a loading indicator and next thing, a popup thanking me for using Theseus Games (My Pleasure, folks), and the game was rapidly downloaded to my computer.

I swiftly abandoned all earthly concerns and dedicated the next few hours of my life to the game, and when I was mentally and psychologically exhausted, I lay on my bed in solemn contemplation of just what the unfortunate folks at TG could have done to further protect their wares.

The Golden Rule here, ladies and gentlemen, is to always do both a client-side and a server-side check of all sensitive components of your web applications. In TG’s case, on the server side, at **_, they could have simply verified my _active** field again, to ensure that I was authorized to purchase said game, which is actually a trivial check to make. Unfortunately, this is a mistake several web developers (newbies and even old-timers) still make when building web services, and frequently fail to tighten up their security. Crafting secure, high-performance web services takes a bit of craftsmanship so if you own an IT company or build web services, please pay a little bit more attention in the future.

Thanks for reading folks. Much love.


Fortunately, the many clients that I’ve worked with over the years can rest easy because their applications are well protected on several levels from this kind of stuff. So do the smart thing and Hire Me! Let me help build and improve on your various frontend and backend web applications using various technologies (React, Angular, Laravel, Symfony, Drupal, .NET Core etc).