thisago's blog


How I cancelled seat selection in Gol airlines damn website

Table of Contents

Discuss in HN: https://news.ycombinator.com/item?id=47567142


I got stuck analyzing the Gol brazilian airlines check-in page1 during the whole last hour because I selected a seat. And only after selection I realized: there's no UI button to remove the selection.

Even selecting a economic seat in the 48h period before the flight has a cost2, and yes, this kind of seat is what I bought previously.

Anyway, if I extend my personal complaining about websites of airline companies, this content would be enough to fill a book LOL

I could just give up of this online check-in and do it from the airport, but the NoScript temporary trust was already conceded, I had to make it until the end.

Analyzing

The first step was analyze the network requests the website does. The seat selection was extremely slow in my qube without GPU acceleration, but this friction didn't feared me to keep devtools open.

With the intention to aggregate useful identifiers about my order/user, I copied the curl representation of some of the main requests:

  • Check-in data retrieval
  • Personal data validation. A POST that confirms phone, CPF (government ID) and email (POST https://g2s-checkin-api.voegol.com.br/api/passenger/update)
  • Seat selection

Then, I tried to infer the design of the API to bet the deletion method/parameters.

It didn't worked, so what I did was to analyze the JavaScript minified bundle files of the webpage. With Firefox Librewolf devtools global search in debugger tab (C-F) I managed to find 2 key files:

https://b2c.voegol.com.br/check-in/chunk-WC72Z7VP.js

Found with the keyword included in the selection request body, this file exposes a class with these key functions:

  • select(body: Record<string, any>)
  • cancel(body: Record<string, any>)

Both call a method from this.seatsClient.(select|cancel). Interesting.

I configured a network override for this file in devtools and duplicated the implementation of cancel(body) into select(body), with the hope to get a cancellation when selecting a seat. Didn't worked, but I realized the request wasn't POST anymore, but DELETE.

I had to analyze further. Reading it again, I saw the both functions defines the body this way:

// ...
select(e) { // `e` is the `body`
  let r = { seatSelectRequest: l(S({}, e), { returnSession: !0 }) },
// ...

I assumed the function S is doing a deep copy of e properties into a new object, a kind of a Object.assign(). So it means the meaningful part of the body is built by the caller.

https://b2c.voegol.com.br/check-in/chunk-7A2SV6CP.js

The caller. Found this by searching which files calls the cancel(body) method.

It exposes a class named o with the method onSubmitRemoveSeatMap(), which calls the cancel(body) function:

onSubmitRemoveSeatMap() {
  // ...
    this.seatsClientFacade
      .cancel({ passengerFlightIds: [this.getFlightIdFromPassenger()] })
  // ...
}

Bingo. This function is strangely not used across the bundles, but luckly it was left there.

The select(body) payload is known because devtools network tab logged it. Comparing both, it differ. Here's for setting: payload for setting the seat:

{
  "seatSelectRequest": {
    "passengerFlightId": "<flight-id>",
    "seatNumber": "<seat-id>",
    "returnSession": true
  }
}

And the DTO for deleting which this file reveals:

{
  "seatsCancelRequest": {
    "passengerFlightIds": ["<flight-id>"],
    "returnSession": true
  }
}

Now I have the:

  • Method, URL and headers to call the DELETE route.
  • Body built by a unused caller.

The final query

Finally I was able to call the DELETE to unset my selection. Because it receives a (damn) Google ReCaptcha token and a rotating bearer token, what I did was patch the select(body) method to build the body from itself, so I can call DELETE when selecting a seat. The following diff is all I did in chunk-WC72Z7VP.js bundle:

diff /tmp/chunk-WC72Z7VP.js /tmp/chunk-WC72Z7VP.new.js
95c95,100
<       let r = { seatSelectRequest: l(S({}, e), { returnSession: !0 }) },
---
>       let r = {
>           seatsCancelRequest: l(
>             S({}, { passengerFlightIds: ["<hardcoded-flight-id>"] }),
>             { returnSession: !0 },
>           ),
>         },
99c104
<         .select(
---
>         .cancel(
111c116
<               reservation: i.result.response.seatSelectResponse.reservation,
---
>               reservation: i.result.response.seatCancelResponse.reservation,

Once it worked, I reproduced it in restclient.el for clearance. I ran with my current bearer and ReCaptcha and worked as well:

DELETE https://g2s-checkin-api.voegol.com.br/api/seats/cancel
       flow=Default
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0
Accept: application/json
sessionId: :session-id
Content-Type: application/json
Authorization: Bearer :bearer
Origin: https://b2c.voegol.com.br

{
  "seatsCancelRequest": {
    "passengerFlightIds": [
      ":flight-id"
    ],
    "returnSession": true
  }
}

You can see it in my public restclient repo: https://codeberg.org/thisago/restclient/src/commit/ca487637250bb719acd412eb37ae804809ee072c/voegol.com.br/b2c/seatSelection.http#L13-L29

Outro

In the next time I'll take a increased care before clicking in any selection, these websites offer no transparency and it's pretty frustrating the user experience, forcing you to either finish the check-in paying the seat selection, or having to arrive sooner in the airport to do the check-in from there, hoping it won't take much time.

Keep aware, modern internet is dangerous :P

Footnotes: