Challenge

A cat-name service. GET /cat?category=m returns a male name, category=f a female one. We had the source:

1
2
3
4
5
6
7
8
app.get("/cat", (req, res) => {
  let { category } = req.query;
  console.log(category);
  if (category.length == 1) {
    const filepath = path.resolve("./names/" + category);
    ...
  }
})

The category.length == 1 guard is there to allow a single character like m or f. Anything longer is rejected, so a plain ../../../etc/passwd never makes it to path.resolve.

Approach

req.query.category is not always a string. Express parses repeated or indexed query keys into an array, and an array’s length counts elements, not characters. An array with one element passes category.length == 1. When that array is then concatenated in "./names/" + category, JavaScript coerces it back to its single element, so the traversal string survives.

Local testing confirmed the coercion. Sending category twice:

Duplicate category query parameter in the request

shows up in the server log as a two-element array, and a single repeat lands as a one-element array:

Server logging the category value as a JavaScript array

So category[0]=../flag.txt is an array of length 1 that stringifies to ../flag.txt.

Solution

https://stray.chall.pwnoh.io/cat?category[0]=../flag.txt

category.length is 1, the guard passes, and path.resolve("./names/" + "../flag.txt") walks out of names/ and reads the flag.