security: production Forgejo is 20 days behind an arbitrary-file-read fix (16.0.2) #447
Etiketter
Inga etiketter
area/backups
area/ci
area/control-panel
area/identity
area/infra
area/observability
area/payments
area/security
area/storage
area/web
blocked
needs-info
needs-triage
ready-for-implementation
type
bug
type
chore
type
docs
type
epic
type
feature
type
task
wontfix
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Inget förfallodatum satt.
Beroenden
Inga beroenden satta
Referens
bitborg/bitborg-infra#447
Läser in…
Hänvisa till i nytt ärende
Ingen beskrivning angiven.
Ta bort grenen "%!s()"
Borttagning av en gren är permanent. Även om den borttagna grenen kan fortsätta existera en kort tid innan den faktiskt tas bort, kan det INTE ångras i de flesta fall. Vill du fortsätta?
Forgejo 16.0.2 is a security release. Production runs 16.0.1-rootless, shipped behind it since
2026-07-30.
The fix that matters
fix: prevent arbitrary file reads from org-mode rendering(upstream PR 13682).Org-mode rendering happens when a
.orgfile is viewed. Reaching it needs a repository holding anattacker-crafted file, which needs an account, and the portal is the only route to an account.
The consequence is not contained to the file being read. Anything readable by the Forgejo process
includes
app.ini, which holdsSECRET_KEY,INTERNAL_TOKEN, the database credentials, the mailercredentials and the OIDC client secret.
SECRET_KEYandINTERNAL_TOKENare what forge sessions anddecrypt stored values, so a read of that one file is an escalation rather than an information leak.
The same release also carries:
fix: add CGNAT (100.64.0.0/10) to 'private' hostmatcher rule(PR 13547/13567). Theprivatehostmatcher backs the webhook and migration allowlists, so an SSRF target in that range was not
being treated as internal.
fix: prevent stored XSS in Actions pre-execution warnings and errors(PR 13682). Needs Actionsaccess, which is entitlement-gated, so this is the least reachable of the three.
[SECURITY]:klauspost/compress,go-chi/chi,postcss,golang.org/x/text,google.golang.org/grpc,svgo, and others.Why it sat unshipped for 20 days
Two defects compounded, and neither logged an error.
minimumReleaseAgeBehaviourdefaults totimestamp-requiredand the docker datasourcereturned no
releaseTimestamp, so a check that needed a timestamp could never pass. The update satin the dashboard's "Pending Status Checks" with an
approvePr-branchaction and no branch, forseventeen days.
before 6am on mondaywindow.vulnerabilityAlertsexempts security work from the schedule, butonly for updates Renovate itself classifies, and there is no vulnerability feed for a container
image tag. So the security release queued behind a weekly window like an ordinary bump.
Worth recording what did not fail.
scripts/check-renovate-annotations.pyreported forgejo aswatched throughout, correctly: the annotation was present, the datasource resolved, and Renovate saw
the new version every night. The dependency was watched, tracked and listed, and nobody was told.
Coverage is not delivery.
Sequence
then closed unmerged. It was insurance against the concealment CSS being broken by 16.0.2,
which would have made the upgrade slow. Verification passed, so the upgrade was unblocked
immediately and closing sign-up would have cost two extra applies and two portal restarts to
protect a window of minutes. The branch is kept, not deleted, so it is one merge away if the
apply is ever delayed.
local/preview rather than on prod.local/Makefilepins16-rootless, a floating minor, so it pulls 16.0.2 today. ADR 0038's gateexists because CSS concealment no-ops silently when upstream restructures a settings page.
forgejo_image_tag→16.0.2-rootlessandhealth_check_forgejo_concealment_verified_tag→
16.0.2-rootlessin ONE change, per the gate's own instruction. Never pre-bump the verifiedtag, never commit a disabled gate.
assumed. Note #63: an image-tag bump can no-op without a pull and restart.
web_signups_open: "true") in the same session the tag bump lands.The registry mirror needs no separate change:
registry_mirror_imagestemplates forgejo's entry from{{ forgejo_image_tag }}, so the mirror follows the bump. That is not true of kanidm, which is ahardcoded literal (#441).
Follow-up this exposes
#446 was filed as a structural worst case of six days. It is now evidenced as a real 25-day exposure,
which should raise its priority rather than leave it in triage.
Verification result, 2026-08-19
Done in the
local/preview (which needed repairing first: it had been broken since the ADR 0039rename, fixed in #449). Preview confirmed running
forgejo version 16.0.2+gitea-1.22.0.bitborg.cssdisabledlabel:has(input[name="full_name"]).right.floated.content:has(input[name="_method"][value="PRIMARY"])Both selectors still match 16.0.2 markup and still hide. The toggle is what makes that meaningful:
"nothing visible" is also what a selector matching nothing produces. The PRIMARY row needed a second
email address added first, because with a single address there is no primary to choose, the selector
matched zero elements, and it would have passed vacuously.
Signed-out navbar also checked: "Create account" present, pointing at
www.bitborg.se/en/signup.Residual, NOT closed by this
EXTERNAL_USER_DISABLE_FEATURES = deletion,manage_passwordapplies only to external OIDC users, so thepreview's break-glass local admin cannot exercise it. If 16.0.2 renamed the key or changed its accepted
values, external users silently regain password management and self-delete. Needs one real OIDC account
checked after the apply. It is config rather than CSS, so the concealment gate does not cover it either.
This also explains a false alarm worth recording: the preview shows a "Change password" section and a
"Delete your account" heading. That is correct for a local admin, and the config comment says so
outright. It is not the concealment failing.
Upgrade PR
#450, with the dry-run accounted for task by task. Two of the six changed tasks are the tag propagating
through templated consumers (the backup-drill orchestrator and the registry-mirror wrapper), so the
mirror carries 16.0.2 and Saturday's restore drill runs
forgejo doctoragainst it, with no secondliteral to chase.
Applied to bitborg-prod, 2026-08-19. Patched.
The running container, not the tag:
podman inspect forgejoImageNamecodeberg.org/forgejo/forgejo:16.0.1-rootlesscodeberg.org/forgejo/forgejo:16.0.2-rootlessgitea --version16.0.1+gitea-1.22.016.0.2+gitea-1.22.0systemctl --user is-active forgejoactiveChecked this way deliberately. #63 records that an image-tag bump can no-op without a pull and
restart, so the declared tag is not evidence. The container is.
The apply
ok=355 changed=7 unreachable=0 failed=0, and the ADR 0038 gate passed rather than being skipped.apply-reconcile.shpredicted 6 and saw 7, and correctly identified the extra as known-blind ratherthan drift:
Useful side effect: the mirror was populated with 16.0.2 in the same run, so CI's container smoke will
find it in-instance rather than falling back upstream.
A second
--checkthen returnedchanged=0, which is the only thing that proves prod matches main.Health
www.bitborg.se/,www.bitborg.se/healthz,git.bitborg.se/,auth.bitborg.se/status.bitborg_unit_active= 1 across all five.Watchdogfiring, severitynone, as expected.Note the pull went to
codeberg.org/forgejo/forgejodirectly:forgejo_imageis the upstream ref, sothe apply never depended on the mirror holding 16.0.2. Worth recording because the reverse would have
failed the apply, and it was checked rather than assumed.
Still open, and it is NOT covered by anything above
EXTERNAL_USER_DISABLE_FEATURES = deletion,manage_passwordapplies only to external OIDC accounts.The local preview's break-glass admin cannot exercise it, and the ADR 0038 gate only compares tags, so
neither the verification nor this apply says anything about it.
Needs one real Kanidm-backed account checked on
git.bitborg.se/user/settings/account: no passwordsection, no self-delete. If 16.0.2 renamed that key or narrowed its accepted values, external users
have silently regained both, and nothing in the stack would report it.
Follow-up
#446 (no security release can bypass the weekly schedule) is no longer theoretical. This incident is
its evidence: 17 days lost to #430, then the weekly window on top. Worth promoting out of triage.