August 22, 2026
How a Single % Turned a Booking Lookup Into a PII Leak
Bounty Case Files #03 | From % to a Full PII Leak: How a Single Wildcard Bypassed Surname Verification and Exposed Passenger Data

By 0x-elfateh
4 min read
By 0x-elfateh | Bug Bounty Hunter
Introduction
Hey again
Hope you're doing amazing today!
I found another interesting one recently, and this time the bug was hiding in a place that looked completely harmless.
A booking lookup page.
Nothing fancy.
Just a booking reference and a surname.
The application basically said:
"Give me the booking reference and the passenger's surname, and I'll show you the booking."
Fair enough.
So I started doing what we usually do:
Open Burp Suite.
Watch the requests.
Change things that probably shouldn't be changed.
And eventually, I found something interesting.
So, let's first talk about the workflow.
The Boring Booking Lookup
The application had a booking lookup function where the request looked roughly like this:
{
"pnrNo": "XXXXXX",
"surname": "DOE",
"language": "EN"
}{
"pnrNo": "XXXXXX",
"surname": "DOE",
"language": "EN"
}The idea was simple:
Booking Reference
+
Surname
↓
Booking
↓
Passenger informationBooking Reference
+
Surname
↓
Booking
↓
Passenger informationI tested the obvious stuff first.
Correct surname?
✅ Works.
Wrong surname?
❌ Rejected.
So far, so good.
At this point I thought:
"Okay, this actually looks properly protected."
Let's Start Breaking the Surname 👀
I started playing with the surname parameter.
Different casing.
Partial values.
Special characters.
Nothing particularly exciting.
Then I had one of those classic bug-hunting moments:
"What happens if I just put % here?"
So I changed:
"surname": "DOE""surname": "DOE"to:
"surname": "%""surname": "%"Pressed Send.
And…
HTTP 200.
Hmm.
I looked at the response.
The booking was returned.
Wait.
What?
I checked the request again.
"surname": "%""surname": "%"Not the actual surname.
Not a partial surname.
Just:
%%And the server accepted it.
Okay… Let's Try That Again
Obviously, one weird response isn't enough.
Maybe the application was doing some strange frontend validation.
Maybe I accidentally had something cached.
Maybe the server had a bad day.
So I sent it again.
{
"pnrNo": "XXXXXX",
"surname": "%"
}{
"pnrNo": "XXXXXX",
"surname": "%"
}Send.
Same result.
The booking came back.
At this point I was thinking:
"Okay, that's interesting."
Then I started testing other wildcard-style inputs.
For example:
D%D%and:
%AL%ALThe behavior continued to look like pattern matching.
But then I wanted to make sure that the application wasn't simply allowing partial surnames.
So I tried ordinary partial values.
Something like:
DDRejected.
Then:
DADARejected.
That was the important part.
The application wasn't simply saying:
"Any surname that starts with D is fine."
The wildcard itself was changing the meaning of the comparison.
Enter SQL LIKE
This started making a lot more sense.
If the backend is doing something conceptually similar to:
WHERE surname LIKE :surnameWHERE surname LIKE :surnamethen % isn't just another character.
It has a special meaning.
It means:
Match zero or more characters.
So:
surname LIKE '%'surname LIKE '%'is essentially:
"Does the surname contain… absolutely anything?"
Yes.
Very helpful.
Very secure.
The application expected:
"%""%"to mean:
"The passenger's surname is %."
But the database understood it as:
"I don't care what the surname is."
And suddenly the surname verification mechanism wasn't verifying much anymore.
But Then I Looked at the Response…
This is where the finding went from:
"Interesting validation bug"
to:
"Oh… this is bad."
The endpoint wasn't returning just a booking ID or a success message.
It returned passenger information.
The response contained fields such as:
{
"passengerList": [
{
"birthDate": "[PASSENGER DOB]",
"gender": "M",
"nationalId": "[NATIONAL ID]",
"name": "[PASSENGER NAME]",
"surname": "[PASSENGER SURNAME]"
}
],
"contactPersonSummary": {
"email": "[PASSENGER EMAIL]",
"phoneNumber": {
"countryCode": "[COUNTRY CODE]",
"areaCode": "[AREA CODE]",
"number": "[PHONE NUMBER]"
}
},
"pnrInfo": {
"pnrNo": "[BOOKING REFERENCE]"
},
"ticketNumberList": [
"[TICKET NUMBER]"
]
}{
"passengerList": [
{
"birthDate": "[PASSENGER DOB]",
"gender": "M",
"nationalId": "[NATIONAL ID]",
"name": "[PASSENGER NAME]",
"surname": "[PASSENGER SURNAME]"
}
],
"contactPersonSummary": {
"email": "[PASSENGER EMAIL]",
"phoneNumber": {
"countryCode": "[COUNTRY CODE]",
"areaCode": "[AREA CODE]",
"number": "[PHONE NUMBER]"
}
},
"pnrInfo": {
"pnrNo": "[BOOKING REFERENCE]"
},
"ticketNumberList": [
"[TICKET NUMBER]"
]
}I obviously won't publish the actual passenger information.
But the response contained:
- Full name
- Surname
- Date of birth
- National ID
- Email address
- Phone number
- Booking information
- Ticket information
And I had provided:
Booking Reference + %Booking Reference + %That's it.
Coffee break. ☕
Because now I had to ask the important question:
Is this actually an authorization bypass?
The Surname Wasn't Just a Search Filter
This is the important distinction.
The surname wasn't merely being used to search for a passenger.
It was acting as a proof-of-ownership factor.
The intended flow was:
I know the booking reference
+
I know the passenger's surname
↓
I can access the bookingI know the booking reference
+
I know the passenger's surname
↓
I can access the bookingBut the wildcard changed that to:
I know the booking reference
+
I know "%"
↓
I can access the bookingI know the booking reference
+
I know "%"
↓
I can access the bookingSo the attacker doesn't need to know the passenger's actual surname anymore.
The security control is effectively gone.
And that's why this isn't just:
"The search accepts funny characters."
It's an authorization bypass leading to sensitive information disclosure.
The Interesting Part 👀
While looking around the booking functionality, I noticed that another version of the booking-search flow already had an OTP verification step.
Instead of immediately returning passenger information, it could respond with something like:
{
"verificationRequired": true,
"passengerList": []
}{
"verificationRequired": true,
"passengerList": []
}Basically:
"Nice try. Complete verification first."
But some older booking-search functionality was still returning the full passenger record without the same protection.
So the security model looked like this:
New Flow
↓
OTP
↓
Passenger PIINew Flow
↓
OTP
↓
Passenger PIIwhile the older flow was:
Booking Reference + Surname
↓
Passenger PIIBooking Reference + Surname
↓
Passenger PIIAnd with %:
Booking Reference + %
↓
Surname check bypassed
↓
Passenger PIIBooking Reference + %
↓
Surname check bypassed
↓
Passenger PIIThat made the issue much more interesting.
Final Thoughts
This started with a very simple question:
"What happens if I put % here?"
And somehow ended with:
"Why am I looking at passenger PII right now?" 😭
The lesson is simple:
Whenever user input is used as an authorization factor, don't just ask what the application expects.
Ask what the backend actually does with it.
Because sometimes…
all it takes is one character. 👀👀
%%And Yes… It Got Rewarded 🏆
The best part?
This wasn't just a "cool bug I found in Burp" situation. 😂
I reported it through the bug bounty program, and after the report was reviewed, the vulnerability was accepted as High severity and rewarded.
Thanks for reading ❤️