Post-mortem: three portal security issues from a member report (fixed 2026-10-27)
A member audited the member portal and reported five findings privately, correctly
separating what they had read from the code from what they had inferred from
behaviour. Three were exploitable; all three were fixed, deployed and verified the
same day. This is the public record of what changed. The mechanics stay in a private
repo; this note describes classes, not recipes.
1. One secret served two purposes
The portal authenticated its Open Collective webhook with a secret in the URL path,
and authenticated its /admin/* API with a header — the same secret for both. The
URL form is not a choice: Open Collective's generic webhooks cannot send headers, so
that endpoint has to carry a token in its address. Reusing it for the admin API
turned every place that URL can be seen (the collective's own admin UI, project
logs, our access logs) into a path to admin.
Fixed:/admin/* now takes a separate, header-only token. The webhook keeps its
URL token and nothing else accepts it. The app ran a short cutover window in which
the old token was still accepted so nothing broke mid-deploy; that window is now
closed and the URL token is refused on admin routes.
2. A webhook trusted the request it was given
The credit-pack path tried to resolve the purchase against Open Collective's order
API, but fell back to the amount stated in the webhook payload itself when it could
not resolve one. Nothing checked that the order existed.
Fixed: the payload is now a hint only. An order that cannot be resolved is
refused, logged loudly, and escalated for manual reconciliation rather than granted
blind. Verified by replaying a forged request against the live service: refused, no
credit granted, no ledger entry, operator alerted.
3. Lapsing a membership did not revoke access
The deactivation path marked a member inactive and returned the credential needed to
act on their gateway access — which the caller then discarded. Existing API keys kept
working, and the recurring allowance refilled each cycle. Chat access was already
gated, so this affected keys a member had taken away from the web app.
Fixed: deactivation now zeroes the member's gateway budget, which blocks every key
under it, and the two renewal paths re-apply the budget explicitly so a returning
member is not left with a dead allowance. Nothing was exposed in practice — no member
was lapsed at the time of the report.
Queued
A fourth, lower-severity finding concerns response-header hygiene and is tracked
privately; it needs a deployment window for the inference gateway. Token rotation for
the webhook URL is tracked in #35.
Credit
Reported privately by a member, who asked — reasonably — that the detail stay out of
public view until the fixes shipped. The report was accurate on all five findings,
including the one they explicitly flagged as inferred rather than observed.
# Post-mortem: three portal security issues from a member report (fixed 2026-10-27)
A member audited the member portal and reported five findings privately, correctly
separating what they had read from the code from what they had inferred from
behaviour. Three were exploitable; all three were fixed, deployed and verified the
same day. This is the public record of what changed. The mechanics stay in a private
repo; this note describes classes, not recipes.
## 1. One secret served two purposes
The portal authenticated its Open Collective webhook with a secret in the URL path,
and authenticated its `/admin/*` API with a header — the same secret for both. The
URL form is not a choice: Open Collective's generic webhooks cannot send headers, so
that endpoint has to carry a token in its address. Reusing it for the admin API
turned every place that URL can be seen (the collective's own admin UI, project
logs, our access logs) into a path to admin.
**Fixed:** `/admin/*` now takes a separate, header-only token. The webhook keeps its
URL token and nothing else accepts it. The app ran a short cutover window in which
the old token was still accepted so nothing broke mid-deploy; that window is now
closed and the URL token is refused on admin routes.
## 2. A webhook trusted the request it was given
The credit-pack path tried to resolve the purchase against Open Collective's order
API, but fell back to the amount stated in the webhook payload itself when it could
not resolve one. Nothing checked that the order existed.
**Fixed:** the payload is now a hint only. An order that cannot be resolved is
refused, logged loudly, and escalated for manual reconciliation rather than granted
blind. Verified by replaying a forged request against the live service: refused, no
credit granted, no ledger entry, operator alerted.
## 3. Lapsing a membership did not revoke access
The deactivation path marked a member inactive and returned the credential needed to
act on their gateway access — which the caller then discarded. Existing API keys kept
working, and the recurring allowance refilled each cycle. Chat access was already
gated, so this affected keys a member had taken away from the web app.
**Fixed:** deactivation now zeroes the member's gateway budget, which blocks every key
under it, and the two renewal paths re-apply the budget explicitly so a returning
member is not left with a dead allowance. Nothing was exposed in practice — no member
was lapsed at the time of the report.
## Queued
A fourth, lower-severity finding concerns response-header hygiene and is tracked
privately; it needs a deployment window for the inference gateway. Token rotation for
the webhook URL is tracked in #35.
## Credit
Reported privately by a member, who asked — reasonably — that the detail stay out of
public view until the fixes shipped. The report was accurate on all five findings,
including the one they explicitly flagged as inferred rather than observed.
Closing this out: all three issues were fixed, deployed and verified on 2026-10-27 (commit 5720a63, image 20261027-5720a63).
Two follow-ups are not finished and are tracked rather than dropped here: the response-header item in the private security repo, and rotating the Open Collective webhook URL token in #35.
Closing this out: all three issues were fixed, deployed and verified on 2026-10-27 (commit `5720a63`, image `20261027-5720a63`).
Two follow-ups are not finished and are tracked rather than dropped here: the response-header item in the private security repo, and rotating the Open Collective webhook URL token in #35.
Follow-up: the queued response-header item has shipped and is verified.
The gateway no longer forwards provider response headers to callers. Measured on the
live service: the prefixed header count went from 27 to 0 on one provider and 8 to 0
on another, and the co-op's remaining provider balance is no longer among them. Both
models still answer normally. The private tracker issue is closed; the durable guard
is a daily check that fails loudly if a future gateway upgrade reintroduces the
behaviour.
Follow-up: the queued response-header item has shipped and is verified.
The gateway no longer forwards provider response headers to callers. Measured on the
live service: the prefixed header count went from 27 to 0 on one provider and 8 to 0
on another, and the co-op's remaining provider balance is no longer among them. Both
models still answer normally. The private tracker issue is closed; the durable guard
is a daily check that fails loudly if a future gateway upgrade reintroduces the
behaviour.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Post-mortem: three portal security issues from a member report (fixed 2026-10-27)
A member audited the member portal and reported five findings privately, correctly
separating what they had read from the code from what they had inferred from
behaviour. Three were exploitable; all three were fixed, deployed and verified the
same day. This is the public record of what changed. The mechanics stay in a private
repo; this note describes classes, not recipes.
1. One secret served two purposes
The portal authenticated its Open Collective webhook with a secret in the URL path,
and authenticated its
/admin/*API with a header — the same secret for both. TheURL form is not a choice: Open Collective's generic webhooks cannot send headers, so
that endpoint has to carry a token in its address. Reusing it for the admin API
turned every place that URL can be seen (the collective's own admin UI, project
logs, our access logs) into a path to admin.
Fixed:
/admin/*now takes a separate, header-only token. The webhook keeps itsURL token and nothing else accepts it. The app ran a short cutover window in which
the old token was still accepted so nothing broke mid-deploy; that window is now
closed and the URL token is refused on admin routes.
2. A webhook trusted the request it was given
The credit-pack path tried to resolve the purchase against Open Collective's order
API, but fell back to the amount stated in the webhook payload itself when it could
not resolve one. Nothing checked that the order existed.
Fixed: the payload is now a hint only. An order that cannot be resolved is
refused, logged loudly, and escalated for manual reconciliation rather than granted
blind. Verified by replaying a forged request against the live service: refused, no
credit granted, no ledger entry, operator alerted.
3. Lapsing a membership did not revoke access
The deactivation path marked a member inactive and returned the credential needed to
act on their gateway access — which the caller then discarded. Existing API keys kept
working, and the recurring allowance refilled each cycle. Chat access was already
gated, so this affected keys a member had taken away from the web app.
Fixed: deactivation now zeroes the member's gateway budget, which blocks every key
under it, and the two renewal paths re-apply the budget explicitly so a returning
member is not left with a dead allowance. Nothing was exposed in practice — no member
was lapsed at the time of the report.
Queued
A fourth, lower-severity finding concerns response-header hygiene and is tracked
privately; it needs a deployment window for the inference gateway. Token rotation for
the webhook URL is tracked in #35.
Credit
Reported privately by a member, who asked — reasonably — that the detail stay out of
public view until the fixes shipped. The report was accurate on all five findings,
including the one they explicitly flagged as inferred rather than observed.
Closing this out: all three issues were fixed, deployed and verified on 2026-10-27 (commit
5720a63, image20261027-5720a63).Two follow-ups are not finished and are tracked rather than dropped here: the response-header item in the private security repo, and rotating the Open Collective webhook URL token in #35.
Follow-up: the queued response-header item has shipped and is verified.
The gateway no longer forwards provider response headers to callers. Measured on the
live service: the prefixed header count went from 27 to 0 on one provider and 8 to 0
on another, and the co-op's remaining provider balance is no longer among them. Both
models still answer normally. The private tracker issue is closed; the durable guard
is a daily check that fails loudly if a future gateway upgrade reintroduces the
behaviour.