---
# System prepended metadata

title: 'Discovering Undocumented Next.js Endpoints: A Deep Dive into LACTF2025''s "Old-Site"'

---

# Discovering Undocumented Next.js Endpoints in LACTF2025 "Old-Site"

## TL;DR

- **The App:** A legacy Next.js web application with a simple guestbook that only accepts ASCII messages.
- **The Vulnerability:** Undocumented Next.js endpoints, active in development mode (`next dev --turbopack`), can be misused to read arbitrary files.
- **The Exploit:** By injecting a special `//# sourceMappingURL` directive into the guestbook file, an attacker can trick the `/__nextjs_source-map` endpoint into returning the contents of sensitive files (like `/flag.txt`).
- **Outcome:** The flag is exposed, emphasizing the risk of leaving development endpoints accessible.

---

## Challenge Description

The "Old-Site" challenge is built around an old-school web app where users can sign a guestbook. Since only ASCII characters (from space to tilde) are allowed, direct code injection isn’t possible. However, the application’s configuration reveals that it runs in development mode, making several internal endpoints available.

Key technical details include:

- **Environment Setup:**  
  The Dockerfile shows the application is launched with development settings:

```Dockerfile
FROM node:23.7.0-alpine3.20
RUN printf "\"$FLAG\"" > /flag.txt  # Weird flag format
WORKDIR /app
USER nextjs
CMD ["pnpm", "run", "dev"]  # Dev mode with Turbopack
```

- **Package Configuration:**  
  The **package.json** confirms the use of Next.js v15.1.6 and Turbopack:

```json
{
  "name": "lactf-web-old-site",
  "version": "1.0.0-OLD",
  "scripts": {
    "dev": "next dev --turbopack",
    ...
  },
  "dependencies": {
    "next": "15.1.6",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}
```

- **Guestbook API:**  
  The guestbook endpoint (`pages/api/guestbook.js`) reads a message and appends it to `guestbook.txt` if it only contains ASCII characters. For example:

```js
...
if (req.method == "POST") {
    if (req.body && req.body["UR MESSAGE"]) {
      const msg = req.body["UR MESSAGE"]
      if (typeof msg === "string" && msg.length > 0 && /^[ -~]+$/.test(msg)) {
        let messages = fs.readFileSync("guestbook.txt", { encoding: "ascii" }).split("\n")
        messages.push(msg)
        fs.writeFileSync("guestbook.txt", messages.join("\n"))
        
      }
      res.redirect(303, "/")
      return
    }
} else if (req.method == "GET") {
      const messages = fs.readFileSync("guestbook.txt", { encoding: "ascii" }).split("\n")
      const list = messages.map(m => `<LI>${m.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")}</LI>`).join("")
      const html = `<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><HTML><HEAD><TITLE>arcblrost's guestbook!!!</TITLE></HEAD><BODY TEXT="white"><FONT FACE="Comic Sans MS"><UL>${list}</UL></FONT></BODY></HTML>`
      res.setHeader("Content-Type", "text/html")
      res.status(200).send(html)
      return
    } else {
      res.status(405).end()
      return
    }
```

The GET part of this code presents a vulnerability when filtering bad chars because it uses `replace` instead of `replaceAll`, hence only the first occurence will be replaced. But this will be worthless in final solution.

---

## Undocumented Endpoints

Further investigation uncovered several undocumented endpoints enabled in development mode:

- **`/__nextjs_original-stack-frames`**  
  *Purpose:* Convert minified stack traces back to their original source using source maps.  
  *Usage:* Processes POST requests from the error overlay to display readable error locations.

- **`/__nextjs_launch-editor`**  
  *Purpose:* Open the code editor at a specific error location.  
  *Usage:* A GET request with `file`, `lineNumber`, and `column` parameters triggers the configured editor (e.g., VSCode).

- **`/__nextjs_source-map`**  
  *Purpose:* Serve source maps for debugging.  
  *Usage:* A GET request with a `filename` parameter reads the file, locates the `//# sourceMappingURL` directive, resolves the path (inline or external), and returns the source map as JSON.

---

```js
const sourceUrl = getSourceMapUrl(fileContents)
...
const sourceMapFilename = path.resolve(
  path.dirname(filename),
  decodeURIComponent(sourceUrl)
)
```

This implementation suffers from two issues:
1. **Controlled Source Mapping:** An attacker can inject a custom `sourceMappingURL`.
2. **Path Traversal:** The use of `decodeURIComponent` without strict checks allows reading arbitrary files.

---

## Exploitation

The exploit chain is simple:

1. **Inject a Malicious Directive:**  
   Despite the ASCII-only input, it is possible to add the following line to `guestbook.txt`:

```
//# sourceMappingURL=/flag.txt
```

2. **Trigger the Vulnerability:**  
   By sending a GET request to:

```
GET /__nextjs_source-map?filename=/app/guestbook.txt
```

   the server reads `guestbook.txt`, detects the injected directive, and resolves it to `/flag.txt`. The endpoint then returns the contents of `/flag.txt` (the flag) as JSON.

---

## Conclusion

This challenge shows that even seemingly harmless development features can lead to serious vulnerabilities when left exposed. By carefully examining the configuration and internal endpoints of a Next.js application running in development mode, it’s possible to exploit undocumented functionality to read arbitrary files.

Remember to secure or disable debugging endpoints in production environments to avoid similar issues.

Happy testing and stay secure!
