SwiftSearch Hotels
SwiftSearch's hotel API accepts a JSON filter body that's merged straight into a MongoDB-style query. Ordinary users filter by city and price; operators slip in just as easily.
Room Description
https://dashboard.webverselabs-pro.com/challenges/swiftsearch

Scenario
SwiftSearch rebuilt their hotel-search API "JSON-first" during their last refactor. The old positional-arg endpoint became a body-merged filter pass-through — readable, flexible, and utterly permissive.
Objective
SwiftSearch's hotel API accepts a JSON filter body that's merged straight into a MongoDB-style query. Ordinary users filter by city and price; operators slip in just as easily.
Initial Analysis
We have a dashboard with 23 flights, all of them are listed on the dashboard as far as I can see.

We have 3 other main endpoints, /rentals, /support and /signin.
For the listings to show up, we make a POST request to an /api/flights/search endpoint.

Using the filter to narrow down the search results also sends a POST request to the same endpoint with more parameters in the body.

Finding the bug
/rentals

This page is very similar in function to the main flight dashboard, but instead of a regular JSON payload in the POST request body, we have NoSQL syntax in the JSON body this time for /api/rentals/search.

Exploitation
Example output for a rental place.
{"_id":"7df33888-3e51-11f1-ba78-a2814f417364","beds":2,"city":"New York","description":"Brownstone floor-through with bay windows over the promenade.","hidden":false,"id":"R-NYC-01","image":"https://images.unsplash.com/photo-1542042238232-3a0b14425b71?w=600&q=75&auto=format&fit=crop","max_guests":4,"min_nights":2,"name":"Brooklyn Heights Two","price_per_night":310,"rating":4.4,"total":1240}
The "hidden": false field is there for all of the rentals we can see in the output.

and the standard request we send to /api/rentals/search is:
{"min_nights":{"$lte":4},"nights":4}
Frontend Code Analysis
function readBody() {
const fd = new FormData(rf);
const city = (fd.get('city') || '').trim();
const guestsStr = fd.get('guests');
const nights = Number(fd.get('nights') || 4);
// Body is merged into the server's Mongo filter. Use raw operators
// so the filter expresses exactly what the UI wants.
const body = {};
if (city) body.city = { "$regex": city, "$options": "i" };
if (guestsStr) body.max_guests = { "$gte": Number(guestsStr) };
// Every rental has a min_nights constraint — keep rentals whose
// minimum stay is <= the user's nights choice.
body.min_nights = { "$lte": nights };
// `nights` itself is NOT a filter field — the server uses it only to
// compute the total column on each card.
body.nights = nights;
return body;
}
Two things stand out immediately:
- The comments explicitly say "Body is merged into the server's Mongo filter. Use raw operators", confirming the JSON body is passed directly to MongoDB with no sanitisation
- MongoDB
$operators ($regex,$gte,$lte) are used in the client, meaning the server accepts and executes them as-is. There is no server-side operator stripping.
We also now know that the "hidden" field is rendered server-side and is being enforced there, since we aren't the ones sending it.
Controlling the "hidden" value
Knowing that we see all those fields in the output of any of the rentals, we need to test whether we have control over all of the fields, so let's try adding "hidden" as a parameter with the value of true.

Oh, that worked, another way to make this work for example if the trigger wasn't a simple true/false flip would be to send the payload:
{"nights": 4, "hidden": {"$exists": true}}
This only checks whether the field exists, no matter the data inside.


https://codehooks.io/docs/nosql-database-query-language
https://www.mongodb.com/docs/manual/reference/operator/query/exists/