fix(forgejo): upgrade to 16.0.2 for the org-mode arbitrary file read #450

Sammanfogat
supernaut sammanfogade 1 incheckning från fix/forgejo-16.0.2-security in i main 2026-08-19 07:30:06 +00:00
Ägare

Closes #447. Security upgrade — worth merging ahead of ordinary review queue.

Why now

16.0.2 shipped 2026-07-30. Production has been behind it for 20 days, held first by #430
(timestamp-required kept it out of a branch for seventeen days) and then by the weekly schedule
(#446). Neither logged an error.

The fix that matters is fix: prevent arbitrary file reads from org-mode rendering (upstream PR
13682). The chain is complete on this deployment:

Step State
Get an account sign-up is open (web_signups_open: "true")
Reach Forgejo Kanidm account, OIDC login
Create a repo, push a .org file allowed
View it rendered arbitrary file read in the Forgejo container
Read app.ini SECRET_KEY, INTERNAL_TOKEN, DB, mailer, OIDC client secret

That last row is why this is escalation rather than an information leak: those first two forge
sessions and decrypt stored values.

Also in 16.0.2: CGNAT (100.64.0.0/10) added to the private hostmatcher, which backs the webhook
and migration allowlists; a stored XSS in Actions pre-execution messages (entitlement-gated, so least
reachable); and eight upstream dependency bumps marked [SECURITY].

ADR 0038 gate, cleared properly

Both tags move in this one change, as the gate's own instruction requires:

forgejo_image_tag                              16.0.1-rootless -> 16.0.2-rootless
health_check_forgejo_concealment_verified_tag  16.0.1-rootless -> 16.0.2-rootless

Verified in the local/ preview, not on prod. local/Makefile pins the floating 16-rootless, so
the preview runs 16.0.2 today (confirmed: forgejo version 16.0.2+gitea-1.22.0).

Selector Matches State With bitborg.css disabled
label:has(input[name="full_name"]) 1 HIDDEN VISIBLE
.right.floated.content:has(input[name="_method"][value="PRIMARY"]) 1 HIDDEN VISIBLE

The toggle is the point. "Nothing visible" is also exactly what a selector matching nothing
produces, so each was confirmed by disabling our stylesheet and watching the control appear. The
PRIMARY row additionally needed a second email address added first: with one address there is no
primary to choose, the selector matched zero elements, and it would have passed vacuously.

Also verified: the signed-out navbar still carries "Create account" → www.bitborg.se/en/signup.

What is NOT verified, stated rather than glossed

EXTERNAL_USER_DISABLE_FEATURES = deletion,manage_password applies only to external OIDC users, so
the preview'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.

That needs one real OIDC account checked after the apply. It is not covered by anything above, and it
is not CSS, so the concealment gate does not cover it either.

This also means the earlier appearance of a "Change password" section in the preview is expected,
not a regression
— the config comment says outright that a break-glass local admin keeps its
password page.

Dry-run against bitborg-prod

ok=293 changed=6 unreachable=0 failed=0, and the concealment gate passes.

Changed Why
forgejo : Install Forgejo custom CSS the provenance comment below
forgejo : Pull the pinned Forgejo image 16.0.2
forgejo : Install Forgejo container Quadlet unit tag lives in the unit
forgejo : Apply Quadlet changes before starting the service meta: flush_handlers
backup-drill : Install the drill orchestrator backup_drill_forgejo_image templates the tag
registry-mirror : Install the wrapper script the mirror map templates the tag

The last two are the tag propagating through templated consumers rather than duplicated literals,
which is the intended design and worth noting explicitly: the mirror will carry 16.0.2 with no second
edit, and Saturday's restore drill will run forgejo doctor on 16.0.2 rather than the old image. That
is not true of kanidm, whose mirror pin is a hardcoded literal (#441).

Handlers: forgejo : Restart Forgejo, plus runner-controller : Reload gitborg user systemd. The
second is forgejo's own notify. That handler name is defined in three roles and Ansible de-duplicates
to runner-controller's copy, which four separate handler comments in this repo already document.

Service impact

Forgejo restarts. Git and web are unavailable for the restart, and the image is pulled first, so the
gap is the container start rather than the download. Nothing else restarts.

Provenance comment

The CSS block claimed its selectors were "matched against Forgejo 16.0.1 markup". That is the record
the next upgrader reads, so it now says 16.0.2 and records how it was re-verified. The other 16.0.1
mentions in the tree are historical "verified against" claims about behaviours I have not
re-verified, so they are deliberately left alone rather than sweepingly bumped.

After the apply

Confirm the RUNNING image is 16.0.2 rather than assuming the tag bump took effect. #63 records that an
image-tag bump can no-op without a pull and restart; the dry-run shows both, but the running container
is the evidence.

Closes #447. **Security upgrade — worth merging ahead of ordinary review queue.** ## Why now 16.0.2 shipped 2026-07-30. Production has been behind it for 20 days, held first by #430 (`timestamp-required` kept it out of a branch for seventeen days) and then by the weekly schedule (#446). Neither logged an error. The fix that matters is **`fix: prevent arbitrary file reads from org-mode rendering`** (upstream PR 13682). The chain is complete on this deployment: | Step | State | | --- | --- | | Get an account | sign-up is open (`web_signups_open: "true"`) | | Reach Forgejo | Kanidm account, OIDC login | | Create a repo, push a `.org` file | allowed | | View it rendered | arbitrary file read in the Forgejo container | | Read `app.ini` | `SECRET_KEY`, `INTERNAL_TOKEN`, DB, mailer, OIDC client secret | That last row is why this is escalation rather than an information leak: those first two forge sessions and decrypt stored values. Also in 16.0.2: CGNAT (`100.64.0.0/10`) added to the `private` hostmatcher, which backs the webhook and migration allowlists; a stored XSS in Actions pre-execution messages (entitlement-gated, so least reachable); and eight upstream dependency bumps marked `[SECURITY]`. ## ADR 0038 gate, cleared properly Both tags move in this one change, as the gate's own instruction requires: ``` forgejo_image_tag 16.0.1-rootless -> 16.0.2-rootless health_check_forgejo_concealment_verified_tag 16.0.1-rootless -> 16.0.2-rootless ``` Verified in the `local/` preview, not on prod. `local/Makefile` pins the floating `16-rootless`, so the preview runs 16.0.2 today (confirmed: `forgejo version 16.0.2+gitea-1.22.0`). | Selector | Matches | State | With `bitborg.css` disabled | | --- | --- | --- | --- | | `label:has(input[name="full_name"])` | 1 | HIDDEN | **VISIBLE** | | `.right.floated.content:has(input[name="_method"][value="PRIMARY"])` | 1 | HIDDEN | **VISIBLE** | **The toggle is the point.** "Nothing visible" is also exactly what a selector matching nothing produces, so each was confirmed by disabling our stylesheet and watching the control appear. The PRIMARY row additionally needed a second email address added first: with one address there is no primary to choose, the selector matched zero elements, and it would have passed vacuously. Also verified: the signed-out navbar still carries "Create account" → `www.bitborg.se/en/signup`. ## What is NOT verified, stated rather than glossed `EXTERNAL_USER_DISABLE_FEATURES = deletion,manage_password` applies **only to external OIDC users**, so the preview'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. That needs one real OIDC account checked after the apply. It is not covered by anything above, and it is not CSS, so the concealment gate does not cover it either. This also means the earlier appearance of a "Change password" section in the preview is **expected, not a regression** — the config comment says outright that a break-glass local admin keeps its password page. ## Dry-run against bitborg-prod `ok=293 changed=6 unreachable=0 failed=0`, and the concealment gate passes. | Changed | Why | | --- | --- | | `forgejo : Install Forgejo custom CSS` | the provenance comment below | | `forgejo : Pull the pinned Forgejo image` | 16.0.2 | | `forgejo : Install Forgejo container Quadlet unit` | tag lives in the unit | | `forgejo : Apply Quadlet changes before starting the service` | `meta: flush_handlers` | | `backup-drill : Install the drill orchestrator` | `backup_drill_forgejo_image` templates the tag | | `registry-mirror : Install the wrapper script` | the mirror map templates the tag | The last two are the tag propagating through **templated** consumers rather than duplicated literals, which is the intended design and worth noting explicitly: the mirror will carry 16.0.2 with no second edit, and Saturday's restore drill will run `forgejo doctor` on 16.0.2 rather than the old image. That is not true of kanidm, whose mirror pin is a hardcoded literal (#441). Handlers: `forgejo : Restart Forgejo`, plus `runner-controller : Reload gitborg user systemd`. The second is forgejo's own notify. That handler name is defined in three roles and Ansible de-duplicates to runner-controller's copy, which four separate handler comments in this repo already document. ## Service impact Forgejo restarts. Git and web are unavailable for the restart, and the image is pulled first, so the gap is the container start rather than the download. Nothing else restarts. ## Provenance comment The CSS block claimed its selectors were "matched against Forgejo 16.0.1 markup". That is the record the next upgrader reads, so it now says 16.0.2 and records how it was re-verified. The other 16.0.1 mentions in the tree are historical "verified against" claims about behaviours I have **not** re-verified, so they are deliberately left alone rather than sweepingly bumped. ## After the apply Confirm the RUNNING image is 16.0.2 rather than assuming the tag bump took effect. #63 records that an image-tag bump can no-op without a pull and restart; the dry-run shows both, but the running container is the evidence.
supernaut lade till 1 incheckning 2026-08-19 07:25:26 +00:00
fix(forgejo): upgrade to 16.0.2 for the org-mode arbitrary file read
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m50s
0117852be5
Security upgrade. 16.0.2 shipped 2026-07-30 and prod has been behind it for 20
days, held first by the #430 timestamp-required bug and then by the weekly
schedule (#446).

The fix that matters is an arbitrary file read in org-mode rendering (upstream
PR 13682). Reaching it needs a repo holding an attacker-crafted .org file, so it
needs an account, and sign-up is open. What makes it more than an information
leak: anything readable by the Forgejo process includes app.ini, holding
SECRET_KEY, INTERNAL_TOKEN, the database credentials, the mailer credentials and
the OIDC client secret. The first two forge sessions and decrypt stored values.

Also in 16.0.2: CGNAT (100.64.0.0/10) added to the 'private' hostmatcher, which
backs the webhook and migration allowlists, and a stored XSS in Actions
pre-execution messages. Plus eight upstream dependency bumps marked [SECURITY].

ADR 0038 gate cleared properly, both tags in one change:

  forgejo_image_tag                             16.0.1 -> 16.0.2-rootless
  health_check_forgejo_concealment_verified_tag 16.0.1 -> 16.0.2-rootless

Concealment re-verified against 16.0.2 in the local/ preview, which pins the
floating 16-rootless and so runs 16.0.2 today. Both selectors matched one
element and hid it, each confirmed by toggling bitborg.css off and watching the
control appear:

  .user-setting-content label:has(input[name="full_name"])                  HIDDEN -> VISIBLE
  .user-setting-content .right.floated.content:has(input[name=_method] PRIMARY)  HIDDEN -> VISIBLE

The toggle is the point. "Nothing visible" is also what a selector matching
nothing produces. The PRIMARY row needed a second email address added first,
because with one address there is no primary to choose and the selector matched
zero elements, which would have passed vacuously.

Not verified here, and worth checking on a real account after the apply:
EXTERNAL_USER_DISABLE_FEATURES = deletion,manage_password applies only to
external OIDC users, so the preview'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.

Updated the CSS provenance comment, which claimed the selectors were matched
against 16.0.1 markup. That is the record the next upgrader reads. The other
16.0.1 mentions elsewhere are historical "verified against" statements that have
NOT been re-verified, so they are deliberately left alone rather than
sweepingly bumped.

Dry-run against bitborg-prod: changed=6, failed=0, and the concealment gate now
passes. Two of the six are the tag propagating through templated consumers
rather than duplicated literals, which is the intended design: backup-drill's
orchestrator (so Saturday's restore drill runs forgejo doctor on 16.0.2) and the
registry-mirror wrapper (so the mirror carries 16.0.2). No second literal to
chase, unlike kanidm's (#441).

Refs #447
supernaut sammanfogade incheckning c3ca3a0b0f till main 2026-08-19 07:30:06 +00:00
supernaut tog bort grenen fix/forgejo-16.0.2-security 2026-08-19 07:30:06 +00:00
Logga in för att delta i denna konversation.
Inga granskare
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet "åååå-mm-dd".

Inget förfallodatum satt.

Beroenden

Inga beroenden satta

Referens
bitborg/bitborg-infra!450
Ingen beskrivning angiven.