Skip to content

Note Mark: Unauthenticated disclosure of soft-deleted note metadata via deleted=true on public books

Moderate severity GitHub Reviewed Published Jun 1, 2026 in enchant97/note-mark • Updated Jul 9, 2026

Package

gomod github.com/enchant97/note-mark/backend (Go)

Affected versions

< 0.0.0-20260601210758-9c9b72740f22

Patched versions

0.0.0-20260601210758-9c9b72740f22

Description

Summary

GET /api/books/{bookID}/notes is an unauthenticated endpoint that accepts a "deleted" query parameter. When the request is ?deleted=true, the
service runs the query with Unscoped() (bypassing GORM's soft-delete scope) but keeps the read-authorization clause as "owner_id = ? OR is_public
= ?". As a result, any unauthenticated caller can enumerate the metadata of soft-deleted ("trashed") notes belonging to any public book — notes
the owner explicitly deleted and expected to be removed from public view.

Affected component (code-verified)

backend/services/notes.go — GetNotesByBookID (lines 72-89):

func (s NotesService) GetNotesByBookID(currentUserID *uuid.UUID, bookID uuid.UUID, deleted bool) ([]db.Note, error) {
tx := db.DB
if deleted {
tx = tx.Unscoped() // <-- bypasses soft-delete scope
}
tx = tx.
Preload("Book").
Joins("JOIN books ON books.id = notes.book_id").
Where(
db.DB.Where("books.id = ?", bookID),
db.DB.Where("owner_id = ? OR is_public = ?", currentUserID, true), // <-- is_public still honored for trash
)
if deleted {
tx = tx.Where("notes.deleted_at IS NOT NULL")
}
var notes []db.Note
return notes, dbErrorToServiceError(tx.Find(&notes).Error)
}

Route registration confirms the endpoint has no AuthRequiredMiddleware (backend/handlers/notes.go:37), and the deleted flag is attacker-controlled
(backend/handlers/notes.go:86 — Deleted bool with query:"deleted").

Proof of concept

  1. A victim owns a public book (is_public = true), creates a note, then soft-deletes it (moves it to trash). The note still exists in the DB with
    deleted_at set.
  2. An unauthenticated attacker who knows (or enumerates) the book UUID requests: GET /api/books//notes?deleted=true
  3. The response lists the soft-deleted note(s) — id, title, slug, timestamps — even though the attacker is not authenticated and the owner
    intended the note to be deleted.

Impact

Exposure of soft-deleted note metadata (title, slug, timestamps) of public books to unauthenticated actors. The note body is not exposed — the
content endpoint (GetNoteContent) does not use Unscoped(), so its count query returns 0 for soft-deleted notes and yields 404. Impact is therefore
limited to metadata disclosure and the bypass of the intended "delete" semantics on public books.

Remediation

Restrict trash (soft-deleted) listings to the book owner only — never honor the is_public branch when deleted=true:

func (s NotesService) GetNotesByBookID(currentUserID *uuid.UUID, bookID uuid.UUID, deleted bool) ([]db.Note, error) {
tx := db.DB
if deleted {
tx = tx.Unscoped()
}

  • // Soft-deleted ("trash") notes must only ever be listed to the book owner.
    
  • authz := db.DB.Where("owner_id = ? OR is_public = ?", currentUserID, true)
    
  • if deleted {
    
  •         authz = db.DB.Where("owner_id = ?", currentUserID)
    
  • }
    tx = tx.
            Preload("Book").
            Joins("JOIN books ON books.id = notes.book_id").
            Where(
                    db.DB.Where("books.id = ?", bookID),
    
  •                 db.DB.Where("owner_id = ? OR is_public = ?", currentUserID, true),
    
  •                 authz,
            )
    if deleted {
            tx = tx.Where("notes.deleted_at IS NOT NULL")
    }
    var notes []db.Note
    return notes, dbErrorToServiceError(tx.Find(&notes).Error)
    

}

With this change, when currentUserID is nil (unauthenticated) and deleted=true, the clause becomes owner_id = NULL, which matches nothing — so
trash is never exposed to anonymous callers.

Coordinated disclosure / CVE request

We have reported this privately and are happy to assist with any further validation or testing you need. If you agree this qualifies as a security
vulnerability, we would be grateful if you could request a CVE ID for it — GitHub lets maintainers request a CVE directly from this advisory page
once it is accepted. Thank you for your time and for maintaining note-mark.

References

  • CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
  • CWE-285: Improper Authorization
  • Prior note-mark authorization fix (CVE-2026-40265) established that read paths must scope by owner_id OR is_public; this report covers the trash
    path that the scope did not fully cover.

References

@enchant97 enchant97 published to enchant97/note-mark Jun 1, 2026
Published to the GitHub Advisory Database Jul 9, 2026
Reviewed Jul 9, 2026
Last updated Jul 9, 2026

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

EPSS score

Exploit Prediction Scoring System (EPSS)

This score estimates the probability of this vulnerability being exploited within the next 30 days. Data provided by FIRST.
(16th percentile)

Weaknesses

Exposure of Sensitive Information to an Unauthorized Actor

The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. Learn more on MITRE.

Improper Authorization

The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action. Learn more on MITRE.

CVE ID

CVE-2026-50554

GHSA ID

GHSA-588f-fvcv-xhvf

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.