Gitea, unauthenticated stargazers leak hidden users
Gitea 1.27.3 GetStargazers and GetWatchers omit isUserVisibleToViewerCond. A limited user who starred a public repo appears in unauth JSON while GET /users/{name} is 404. Follower lists already filter. No CVE yet.
- Name
- Gitea, unauthenticated stargazers leak hidden users
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- medium
- Disclosure Status
- public
- Vendor
- Gitea
- Affected
- Gitea through v1.27.3 (146cc3e); unpublished. Needs a public repository and at least one limited/hidden user who starred or watched it.
- Published
- 29 Sept 2026
- Updated
- 29 Sept 2026
- Tags
- 0-day, gitea, disclosure, visibility, cwe-200, unauthenticated
stars skip the visibility filter
I am @abraxas_null. The proof of concept is on GitHub: abraxas/gitea-stargazers-hidden (loopback client). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh. Authorized lab only. It talks to loopback.
This is Gitea v1.27.3 (146cc3e). No CVE yet. CWE-200. 5.3 Medium (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N).
How I found it
Visibility was a theme of the 1.27.3 GHSA wave. Limited and private users are supposed to vanish from anonymous viewers. GetInfo already 404s them. Follower lists already apply isUserVisibleToViewerCond in SQL. When a product 404s a profile on purpose, the next question is which list endpoints still join user without that condition.
GetStargazers is a join on star with no visibility filter. ListStargazers then convert.ToUsers the lot. ListSubscribers is the same shape for watchers. No isUserVisibleToViewerCond.
I planted hidstar as limited, starred and watched a public repo, and hit the instance with no token. Unauth GET /api/v1/users/hidstar is 404. Unauth stargazers JSON includes hidstar. Unauth watchers include hidstar. Unauth followers HTML does not. The profile is shy. The star table is not.
I am not printing a stargazers walk you can paste at someone else's public repo. The missing And(isUserVisibleToViewerCond(viewer)) is the useful part.
Wrong turns already recorded: unauth GET /users/hidstar returning 200 (then visibility is off entirely - lab requires 404); followers HTML listing hidstar (that path already applies the condition); treating a 200 on /stargazers with only public names as SUCCESS; a reverse shell. Theatre. The leak is the limited login in the JSON.
The list that forgot the filter
GetInfo already hides limited users from anonymous viewers:
if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
// fake ErrUserNotExist error message to not leak information about existence
ctx.APIErrorNotFound()
return
}Follower lists do the same in SQL. GetUserFollowers:
sess := db.GetEngine(ctx).
Select("`user`.*").
Join("LEFT", "follow", "`user`.id=follow.user_id").
Where("follow.follow_id=?", u.ID).
And("`user`.type=?", UserTypeIndividual).
And(isUserVisibleToViewerCond(viewer))GetStargazers does not:
func GetStargazers(ctx context.Context, repo *Repository, opts db.ListOptions) ([]*user_model.User, error) {
sess := db.GetEngine(ctx).Where("star.repo_id = ?", repo.ID).
Join("LEFT", "star", "`user`.id = star.uid")
// ...
return users, sess.Find(&users)
}What an attacker can do
Without logging in, read a public repo's stargazers and watchers and learn usernames that GET /users/{name} hides. That is recon on limited and private accounts that starred or watched something public: people who set visibility so they would not show up in search, then clicked star anyway.
It is a username, not an email, not a private-git read, not RCE. Combined with the Follow 204 vs 404 oracle, you can confirm a hidden login exists even when the profile 404s.
The lab (run this at home)
Source of truth is lab/. Image gitea/gitea:1.27.3. Port 18133. Bind it to loopback. Compose allows public,limited,private visibility and leaves stars on.
# Loopback lab image pin for gitea-stargazers-hidden. Full stack: docker-compose.yml
FROM gitea/gitea:1.27.3name: gitea-stargazers-hidden
services:
gitea:
build:
context: .
dockerfile: Dockerfile
image: gitea-stargazers-hidden:1.27.3
ports:
- "127.0.0.1:18133:3000"
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__database__DB_TYPE: sqlite3
GITEA__database__PATH: /data/gitea/gitea.db
GITEA__security__INSTALL_LOCK: "true"
GITEA__server__DOMAIN: 127.0.0.1
GITEA__server__HTTP_PORT: "3000"
GITEA__server__ROOT_URL: http://127.0.0.1:18133/
GITEA__service__DISABLE_REGISTRATION: "false"
GITEA__service__REQUIRE_SIGNIN_VIEW: "false"
GITEA__service__ALLOWED_USER_VISIBILITY_MODES: public,limited,private
GITEA__repository__DISABLE_STARS: "false"
volumes:
- gitea_stargazers_hidden_data:/data
volumes:
gitea_stargazers_hidden_data:git clone https://github.com/abraxas/gitea-stargazers-hidden
cd gitea-stargazers-hidden/lab
./run.shThe proof of concept is written for 127.0.0.1:18133. Do not publish the port off loopback.
What the tree actually consumes
Limited user stars and watches a public repo. Unauth profile GET 404s. Unauth stargazers and subscribers list the login. Followers web HTML is the control that already filters.
A few other ways to lose without learning anything:
- Unauth
GET /users/hidstarreturning 200. Then visibility is off entirely. Lab requires 404. - Followers HTML listing
hidstar. That path already applies the condition. - Treating a 200 on
/stargazerswith only public names as SUCCESS. The leak is the limited login in the JSON. - A reverse shell. Theatre. The witness is
hidstarin unauth JSON.
Last lab run, trimmed:
unauth-user-get status=404
unauth-stargazers status=200 stargazer-names=['hidstar']
unauth-watchers leak=True
unauth-followers-web limited-in-html=False
SUCCESS GITEA-STARGAZERS-HIDDENThe client that produced it is on GitHub.
What this is not
It is not "Gitea never hides limited users." Profile GET and follower lists already do. Stars and watchers do not. It is a username on a public repo, not email, not a private-git read, not RCE.
The fix
Apply isUserVisibleToViewerCond in GetStargazers / GetRepoWatchers. Re-run the loopback client against a patched build: unauth stargazers and watchers must omit hidstar the same way followers already do.
I am not going to print an unauth curl of /stargazers aimed at a live host. The missing SQL condition is the useful part. If you own the box, run the proof of concept against loopback.
Same product, other unpublished labs on this tag: hostmatcher 0.0.0.0/8 · public-only PAT org · follow existence · artifact v4 ReadAll · restore file:// · keys IDOR / git redirect.
References
- Proof of concept: abraxas/gitea-stargazers-hidden · gitea-stargazers-hidden-Abraxas-Labs.py
- Lab:
lab/· Dockerfile · docker-compose.yml · run.sh - @abraxas_null · github.com/abraxas · abraxaslabs.tech · abraxas.null@proton.me
- CWE-200
- Tree: gitea v1.27.3 ·
star.gorouter ·star.gomodel ·subscriber.go·GetInfo·GetUserFollowers - SECURITY.md
- Product: Gitea