Stray
Challenge
A cat-name service. GET /cat?category=m returns a male name, category=f a female one. We had the source:
| |
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:

shows up in the server log as a two-element array, and a single repeat lands as a one-element 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.