Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐹 Go

GHSA-rj4g-rqgh-rx9h

MEDIUM

Ech0 comment model's Email field returned on public /api/comments endpoints

Published
May 7, 2026
Updated
May 7, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed

Blast Radius

1 pkg affected
🐹github.com/lin-snow/Ech0

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

Description

Summary

The Comment model serializes its Email field through the public comment-listing API. internal/model/comment/comment.go:33 uses json:"email", while adjacent PII fields (IPHash, UserAgent) correctly use json:"-". The public endpoints GET /api/comments?echo_id=X and GET /api/comments/public?limit=N both live on PublicRouterGroup with no authentication. Alice retrieves every guest commenter's email address on the instance with a few unauthenticated HTTP calls.

Details

The Comment model at internal/model/comment/comment.go:33:

type Comment struct {
    // ... 
    Email     string     `gorm:"size:255;not null;index" json:"email"`
    IPHash    string     `gorm:"size:128;index"          json:"-"`
    UserAgent string     `gorm:"size:512"                json:"-"`
    // ...
}

The json:"-" on IPHash and UserAgent shows the developer's intent: hide server-side PII from API responses. The Email field missed the same tag. GORM materializes the full struct and the Gin handler returns it verbatim.

Routes at internal/router/comment.go:20 and comment public-feed route:

appRouterGroup.PublicRouterGroup.GET("/comments", middleware.NoCache(), h.CommentHandler.ListCommentsByEchoID())
appRouterGroup.PublicRouterGroup.GET("/comments/public", middleware.NoCache(), h.CommentHandler.ListPublicComments())

Both handlers call ListPublicByEchoID (service at internal/service/comment/comment.go:329) or ListPublicComments (service at :340), both of which return the slice of Comment structs to ctx.JSON. No DTO projection, no field stripping.

The email field is populated for every guest comment: the submission form requires an email address so the server can later send moderation or reply notifications. The UI does not display the email, so users assume it stays server-side.

GHSA-m983-7426-5hrj (2026-03-22) closed a similar PII leak on GET /api/allusers, which exposed account-owner emails. This report covers a distinct endpoint (/api/comments and /api/comments/public) and a distinct data subject (guest commenters, not registered account owners).

Proof of Concept

Anonymous caller harvests commenter emails on the default install:

import requests
TARGET = "http://localhost:8300"

# Any echo UUID from the public feed.
pub_id = requests.get(f"{TARGET}/api/echo/page?page=1&pageSize=1").json()["data"]["items"][0]["id"]

# No auth header. The response includes the raw email field.
r = requests.get(f"{TARGET}/api/comments", params={"echo_id": pub_id})
for c in r.json()["data"]:
    print(f"  nickname={c['nickname']!r}  email={c.get('email')!r}")

# The /public variant returns recent comments across every echo.
r = requests.get(f"{TARGET}/api/comments/public", params={"limit": 100})
emails = {c.get("email") for c in r.json()["data"] if c.get("email")}
print(f"harvested {len(emails)} unique emails from /comments/public")

Observed on v4.5.6:

nickname='GuestHarvestMe'  email='[email protected]'
harvested 1 unique emails from /comments/public

The instance had one guest comment; its email returned in both endpoints. An instance with any commenter volume returns every address.

Impact

Anonymous harvest of every guest commenter's email address across the instance. Email addresses submitted for moderation or reply notifications are treated as private by user expectation; any visitor pulls the full list with a short paginated loop against /api/comments/public. Privacy-regulation exposure follows:

  • GDPR and CCPA. Email is personal data. Exposing it to any internet visitor without consent is a notifiable incident under both regimes.
  • Spam and phishing targeting. Attackers map commenter emails to nicknames and per-echo topics, then send targeted phishing that references content the victim engaged with.
  • Cross-instance aggregation. A scraper against any public-facing Ech0 instance yields a curated list of people who comment on the topics the site covers.

No authentication required. No admin role required. The /comments/public endpoint returns cross-echo aggregated data, so one call covers the whole instance.

Recommended Fix

Change the JSON tag on the Email field to match the adjacent PII fields:

Email string `gorm:"size:255;not null;index" json:"-"`

Or, if some authenticated view needs the email, introduce a PublicComment DTO that projects only non-sensitive fields:

type PublicComment struct {
    ID        string    `json:"id"`
    EchoID    string    `json:"echo_id"`
    Nickname  string    `json:"nickname"`
    Website   string    `json:"website,omitempty"`
    Content   string    `json:"content"`
    Status    string    `json:"status"`
    Hot       bool      `json:"hot"`
    Source    string    `json:"source"`
    CreatedAt int64     `json:"created_at"`
    UpdatedAt int64     `json:"updated_at"`
}

Project the handler output through this DTO. Keep the raw Comment struct internal to the service layer.


Found by aisafe.io

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/lin-snow/Ech0all versions1.4.8-0.20260503034700-cb8d7a997dd8

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/lin-snow/Ech0. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update github.com/lin-snow/Ech0 to 1.4.8-0.20260503034700-cb8d7a997dd8 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-rj4g-rqgh-rx9h is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-rj4g-rqgh-rx9h is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-rj4g-rqgh-rx9h. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## Summary The `Comment` model serializes its `Email` field through the public comment-listing API. `internal/model/comment/comment.go:33` uses `json:"email"`, while adjacent PII fields (`IPHash`, `UserAgent`) correctly use `json:"-"`. The public endpoints `GET /api/comments?echo_id=X` and `GET /api/comments/public?limit=N` both live on `PublicRouterGroup` with no authentication. Alice retrieves every guest commenter's email address on the instance with a few unauthenticated HTTP calls. ## Details The Comment model at `internal/model/comment/comment.go:33`: ```go type Comment struct {
O3 Security · Impact-Aware SCA

Is GHSA-rj4g-rqgh-rx9h in your dependencies?

O3 detects GHSA-rj4g-rqgh-rx9h across Go dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.