Skip to main content
Press slash or control plus K to focus the search. Use the arrow keys to navigate results and press enter to open a threat.
Reconnecting to live updates…

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

0
High
Published: 07/09/2026 (07/09/2026, 13:41:35 UTC)
Source: GCVE Database
Product: github.com/enchant97/note-mark/backend

Description

## Summary Note Mark validates book and note `slug` values with the OpenAPI/huma tag `pattern:"[a-z0-9-]+"`. huma compiles this with `regexp.MustCompile(s.Pattern)` and tests it with `patternRe.MatchString(str)`, an UNANCHORED match. Because the pattern is not anchored (`^...$`), any string that merely CONTAINS one `[a-z0-9-]` substring passes validation. A slug such as `../../../../../../tmp/escape` is accepted and stored verbatim. The data-export CLI commands (`note-mark migrate export` and `note-mark migrate export-v1`) join these unsanitized slugs straight into the output path with `path.Join` / `filepath.Join`, then `os.MkdirAll` the directory and `os.Create` the note file. `path.Join` resolves the `../` segments, so the note content file is written OUTSIDE the configured export directory. The export process commonly runs as root (default in Docker / bare-metal admin usage), so this is a root-privilege arbitrary directory create + file write. This is the unguarded sibling of GHSA-g49p-4qxj-88v3 (CVE class CWE-22 in the same export sinks). That fix added `filepath.Base(asset.Name)` to sanitize the asset filename, but the adjacent path components `book.Slug` and `note.Slug` — used in the very same `path.Join` calls in the same two export functions — were left raw, and their input-side `pattern` guard is bypassable as shown above. ## Vulnerable code Slug input validation (`backend/db/types.go`, v0.19.4): ```go type CreateBook struct { Name string `json:"name" required:"true" minLength:"1" maxLength:"80"` Slug string `json:"slug" required:"true" minLength:"1" maxLength:"80" pattern:"[a-z0-9-]+"` IsPublic bool `json:"isPublic,omitempty" default:"false"` } type CreateNote struct { Name string `json:"name" required:"true" minLength:"1" maxLength:"80"` Slug string `json:"slug" required:"true" minLength:"1" maxLength:"80" pattern:"[a-z0-9-]+"` } ``` huma applies the pattern UNANCHORED (`github.com/danielgtaylor/huma/[email protected]`): ```go // schema.go if s.Pattern != "" { s.patternRe = regexp.MustCompile(s.Pattern) ``` ```go // validate.go if s.patternRe != nil { if !s.patternRe.MatchString(str) { res.Add(path, v, s.msgPattern) ``` `regexp.MatchString("[a-z0-9-]+", "../../../../tmp/escape")` is `true` (it matches the `tmp` substring), so the traversal slug passes and `BooksService.CreateBook` / `NotesService` store it verbatim. Export sinks (`backend/cli/migrate.go`, v0.19.4). The asset filename was sanitized by the GHSA-g49p fix; the sibling slug path components were not: ```go // commandMigrateExportDataV1 / commandMigrateExportData for _, book := range user.Books { bookDir := path.Join(exportDir, user.Username, book.Slug) // book.Slug raw for _, note := range book.Notes { noteDir := path.Join(bookDir, note.Slug) // note.Slug raw if err := os.MkdirAll(noteDir, os.ModePerm); err != nil { return err } f, err := os.Create(path.Join(noteDir, "_index.md")) // escapes exportDir ``` ```go // the same functions DO sanitize the sibling asset name: assetFileName := filepath.Base(asset.Name) if assetFileName == "/" || assetFileName == "." { log.Printf("disallowed asset filename found '%s', skipping\n", asset.Name) continue } f, err := os.Create(path.Join(assetsDir, asset.ID.String()+"."+assetFileName)) ``` ## Impact A low-privilege authenticated user (any registered account that can create a book/note) sets a traversing `slug`. When an administrator later runs `note-mark migrate export` or `export-v1` (a routine backup/migration operation, commonly as root in Docker), the exporter creates attacker-chosen directories and writes the note's `_index.md` to an arbitrary filesystem location outside the export directory. With root, this allows writing to `/etc/cron.d/`, systemd unit directories, or other startup paths, escalating to code execution as root. Same trust boundary and severity class as GHSA-g49p-4qxj-88v3. ## Attack scenario 1. Attacker registers / uses any normal user account. 2. Attacker `POST /api/books` (or a note) with `slug` = `../../../../../../etc/cron.d/x` (passes the unanchored `[a-z0-9-]+` pattern). Stored verbatim. 3. Admin runs `note-mark migrate export-v1 --export-dir /data/backup` (root). 4. Exporter does `path.Join("/data/backup", username, "../../../../../../etc/cron.d/x")` which yields `/etc/cron.d/x`, then `os.MkdirAll` creates it and `os.Create(path.Join(noteDir, "_index.md"))` writes attacker-influenced content outside `/data/backup`. ## Proof of concept Self-contained Go reproducer pinning `huma v2.37.3` (Note Mark's exact version) and Note Mark's exact `CreateBook` DTO + the exact export `path.Join` expression. It demonstrates (a) the traversal slug passes huma validation, (b) a negative control that genuinely violates the charset is rejected, (c) the export sink writes the file outside the export root. ```go // go.mod: module nmpoc; go 1.24; require github.com/danielgtaylor/huma/v2 v2.37.3 package main import ( "contex

CVSS v4.0

Attack Vector
Network
Attack Complexity
Low
Attack Requirements
None
Privileges Required
Low
User Interaction
Passive
Vuln. Confidentiality
High
Vuln. Integrity
High
Vuln. Availability
High
Subsq. Confidentiality
None
Subsq. Integrity
None
Subsq. Availability
None
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

Affected software

Goghsa
github.com/enchant97/note-mark/backend
Affected versions
<0.0.0-20260601210719-67b7de04308a

Run on your own infrastructure? Check whether these packages are installed with threat-finder — our free open-source scanner.

Technical Details

Gcve Source
db.gcve.eu
Osv Id
GHSA-rqrh-8wpv-x7hh
Osv Schema Version
1.4.0
Aliases
["CVE-2026-50553"]
Ecosystems
["Go"]
Database Specific Severity
HIGH
Cvss Version
4.0

Threat ID: 6a50bac868715ace435892b3

Added to database: 07/10/2026, 09:26:32 UTC

Last updated: 07/10/2026, 09:26:32 UTC

Views: 1

Community Reviews

0 reviews

Crowdsource mitigation strategies, share intel context, and vote on the most helpful responses. Sign in to add your voice and help keep defenders ahead.

Sort by
Loading community insights…

Want to contribute mitigation steps or threat intel context? Sign in or create an account to join the community discussion.

Actions

Please log in to the Console to use AI analysis features.

Need more coverage?

Upgrade to Pro Console for AI refresh and higher limits.

For incident response and remediation, OffSeq services can help resolve threats faster.

Latest Threats

Breach by OffSeqOFFSEQFRIENDS — 25% OFF

Check if your credentials are on the dark web

Instant breach scanning across billions of leaked records. Free tier available.

Scan now
OffSeq TrainingCredly Certified

Lead Pen Test Professional

Technical5-day eLearningPECB Accredited
View courses