Date 2026-06-21. Target: Forgejo (hard-fork of Gitea). Result: CONFIRMED HIGH (live PoC) - Forgejo never ported Gitea's CVE-2026-24791/GHSA-wrr5 public-only-token fix (1.26.2, 2026-06-14); at Forgejo 11.0.15 + HEAD a public-only token reads private emails/keys/oauth-apps + writes CI secrets via /api/v1/user self-routes. Credit/responsible-disclosure (Forgejo has no paid bounty).
When project A publishes a security fix, any HARD-FORK B (Forgejo<-Gitea, OpenSearch<-Elasticsearch, Valkey<-Redis, Mastodon-forks, OpenTofu<-Terraform, LibreOffice<-OpenOffice lineage, etc.) is a high-EV regression target: 1. Take A's fresh advisory (the fix commit + the exact mechanism). 2. Check whether B ported it - by EXACT mechanism, not just "did B merge a sync." B may have diverged enough that A's patch does not apply cleanly, OR B's maintainers simply lag the port (B runs its own security process and release cadence). 3. If B's latest release/HEAD lacks the fix -> live vuln in B, often weeks before B ports it.
This is Rule 38 (post-audit regression hunting) generalized across the fork boundary. The fix being PUBLIC in A is what makes it cheap (you know the exact sink + the exact guard to look for); B not having it yet is the window.
Forgejo replaced Gitea's middleware-centric public-only enforcement with a repo-scoped AuthorizationReducer (services/authz/, absent in Gitea). That reducer correctly guards the REPO-typed self-routes - so a naive "does Forgejo block public-only tokens on /user/repos" check PASSES and looks fine. But the reducer is repo-typed ONLY; it does not cover the non-repo self-routes (emails/keys/gpg/oauth-apps/actions-secrets/hooks). Gitea's fix used a route-group middleware (checkTokenPublicOnly + rejectPublicOnly) that covers ALL of them. So the divergence created a partial-coverage gap: B solved the problem A's-fix-targets in a DIFFERENT layer that happens not to reach all the same routes.
Lesson: when B has diverged its security architecture from A, do NOT assume "B has its own guard => B is safe." Map B's guard's COVERAGE (which routes/resource-types) against A's fix coverage. The delta is where the port-lag bug lives. Here: reducer = repo-types; middleware = all self-routes; delta = the non-repo self-routes = the live HIGH.
Mid-drill, checkTokenPublicOnly's User-branch looked like it keyed only off the target user's PROFILE visibility (not resource privacy) - which would have made "absence of the guard" a possible NON-issue. I did NOT stage on the assumption. I fetched Gitea's ACTUAL fix (1.26.2) and found it added THREE things: contextAuthenticatedUser() (sets self as ctx user, so the guard evaluates), checkTokenPublicOnly() at group level, AND a NEW rejectPublicOnly() hard-block on the sensitive sub-routes. THEN confirmed Forgejo has none of that machinery, public-only tokens are real in Forgejo, and finally ran a LIVE PoC (200 on private reads + 201 on a secret write). The static "missing middleware" claim was necessary but NOT sufficient - the live 200/201-vs-expected-403 is what made it bulletproof (same discipline as the Flowise content-disposition save: verify the runtime, not just the static read).
Next time a major OSS project publishes an authz/RCE/SSRF fix, immediately check its hard-forks' latest release for the port: Gitea->Forgejo, Elasticsearch/Kibana->OpenSearch/OpenSearch-Dashboards, Redis->Valkey/KeyDB, Terraform->OpenTofu, Grafana forks, nextcloud<->owncloud lineage, MariaDB<->MySQL. Map guard coverage when the fork has diverged its security layer.
Tested the primitive on glitch-soc<-Mastodon (a near-mirror soft-fork, structural twin of Forgejo<-Gitea). Result: WALK - glitch-soc had merged Mastodon main the DAY BEFORE the audit (HEAD tracking 4.6.0 > all patched branches); all 6 sampled High/consent/IDOR fixes present. The Forgejo premise did NOT reproduce, and the reason is cadence: - Slow / batch-merge fork (Forgejo) = PERSISTENT window. Forgejo lagged a whole batch of Gitea's 06-14 advisories by days-to-weeks -> 4 gaps open at HEAD on any given day. High EV anytime. - Fast near-mirror fork (glitch-soc) = TRANSIENT window. It merges upstream main within ~a day, so the gap is only open in the short interval AFTER an upstream security release and BEFORE the fork's next merge-upstream PR lands. Checking at a random time -> almost always synced -> WALK.
Operational rule: before spending a cross-fork agent, FIRST check the fork's merge cadence + last-sync date vs the date of the most recent upstream security release. - If the fork's last sync PREDATES a fresh upstream security release -> window OPEN, high EV, go. - If the fork synced AFTER the latest upstream advisory (or has no recent upstream advisory to lag) -> window CLOSED, WALK, do not spend the deep audit. - For fast near-mirror forks, the trigger is EVENT-DRIVEN: watch upstream's security advisory feed; the moment upstream ships a security release, immediately check the fork before it merges. Untimed = wasted. This makes the primitive a TIMED/triggered hunt for fast forks (watch upstream advisories -> pounce in the merge gap) and an anytime hunt for slow forks (Forgejo, Gogs-class). The cheap disclosure+merge-state gate (~50k tokens) correctly fails-fast on synced forks.