GitHub Security Advisory: GHSA-g49p-4qxj-88v3
Package: github.com/enchant97/note-mark/backend (GO)
Severity: HIGH (CVSS 0.0)
CWE: CWE-20: Improper Input Validation, CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Vulnerable versions: < 0.0.0[PII_REDACTED]2243-db3f72bff780
Patched in: 0.0.0[PII_REDACTED]2243-db3f72bff780

Summary: Note Mark: Arbitrary File Write via Path Traversal in Asset Names Leads to Remote Code Execution

Details:
### Description

The Note Mark application allows authenticated users to upload assets to notes via `POST /api/notes/{noteID}/assets`, where the asset filename is provided through the `X-Name` HTTP request header. This value is stored directly in the database without any sanitization or validation - no path separator filtering, no directory traversal sequence rejection, and no use of `filepath.Base()` to strip directory components. The unsanitized name is persisted as-is in the `note_assets` table (`Name` column, `varchar(80)`).

When an administrator subsequently runs the data export CLI commands (`note-mark migrate export-v1` or `note-mark migrate export`), the stored asset name is passed directly into `filepath.Join()` and `path.Join()` calls as part of the output file path argument to `os.Create()`. Since Go's `filepath.Join()` resolves `../` sequences during path normalization, an attacker-controlled asset name containing directory traversal sequences causes the export process to write files to arbitrary locations on the filesystem, completely outside the intended export directory.

The export process typically runs as root (the default in Docker deployments and common in bare-metal setups). This means the arbitrary file write operates with root privileges, allowing an attacker to write to any writable location on the filesystem. This can be escalated to Remote Code Execution by overwriting system binaries such as `/bin/bash` with a malicious payload. Since the Go binary is statically compiled and does not shell out to external programs during the export, overwriting `/bin/bash` does not affect the running export process. However, the next time any user or administrator invokes `bash` on the system, the attacker-controlled binary executes instead, resulting in code execution as root. In environments with cron or systemd, writing to `/etc/cron.d/` or systemd unit files provides additional exploitation paths.

The data flow is: `X-Name` HTTP header > `handlers/assets.go` (no validation) > `services/assets.go` (stored to DB as-is) > `cli/migrate.go` (used in `os.Create(filepath.Join(..., asset.Name))`) > arbitrary file write.

#### Source Code Analysis

The asset upload handler at `backend/handlers/assets.go:48-51` extracts the filename directly from the `X-Name` header:

```go
type PostNoteAssetInput struct {
    NoteID  uuid.UUID `path:"noteID" format:"uuid"`
    Name    string    `header:"X-Name" required:"true"`
    RawBody []byte    `required:"true"`
}
```

The service layer at `backend/services/assets.go:39-42` stores this value without validation:

```go
noteAsset := db.NoteAsset{
    NoteID: noteID,
    Name:   name,
}
```

The V1 export function at `backend/cli/migrate.go:328` uses the unsanitized name directly:

```go
f, err := os.Create(filepath.Join(noteDir, asset.Name))
```

The non-V1 export function at `backend/cli/migrate.go:223` similarly uses it:

```go
f, err := os.Create(path.Join(assetsDir, asset.ID.String()+"."+asset.Name)