$ cat writeup.md…
$ cat writeup.md…
hackthebox
Task: Next.js 13.4.5 API endpoint with regex validation and path traversal checks. Solution: bypass all three checks using array query parameter — multiline regex matches first element, Array.includes differs from String.includes, and procfs symlinks pad path length to truncate .png extension via slice(0,100).
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Find the next path in your career or even some vulnerabilities along the way. Anyway, good luck on your travels!
A Next.js 13.4.5 web application with a vulnerable API endpoint at /api/team that serves team member images. The endpoint has three security checks that can all be bypassed simultaneously using a single technique.
import path from 'path'; import fs from 'fs'; const ID_REGEX = /^[0-9]+$/m; export default function handler({ query }, res) { if (!query.id) { res.status(400).end("Missing id parameter"); return; } // Check format if (!ID_REGEX.test(query.id)) { console.error("Invalid format:", query.id); res.status(400).end("Invalid format"); return; } // Prevent directory traversal if (query.id.includes("/") || query.id.includes("..")) { console.error("DIRECTORY TRAVERSAL DETECTED:", query.id); res.status(400).end("DIRECTORY TRAVERSAL DETECTED?!? This incident will be reported."); return; } try { const filepath = path.join("team", query.id + ".png"); const content = fs.readFileSync(filepath.slice(0, 100)); res.setHeader("Content-Type", "image/png"); res.status(200).end(content); } catch (e) { console.error("Not Found", e.toString()); res.status(404).end(e.toString()); } }
1. Regex Multiline Bypass (/^[0-9]+$/m)
The regex uses the m (multiline) flag, which makes ^ and $ match the start/end of each line rather than the entire string. When query.id is an array like ["1\n", "../../../../...flag.txt"], calling ID_REGEX.test(array) converts the array to a string "1\n,../../../../...flag.txt". The multiline regex matches "1" on the first line and passes.
2. Array.prototype.includes vs String.prototype.includes
When query.id is an array, id.includes("/") calls Array.prototype.includes, which checks if any element is strictly equal to "/". Since no element is exactly "/" or ".." (they contain these characters but aren't equal to them), both checks pass. This is fundamentally different from String.prototype.includes which checks for substrings.
3. filepath.slice(0, 100) truncates .png extension
...
$ grep --similar