Post-mortem: three portal security issues from a member report (fixed 2026-10-27) #36

Closed
opened 2026-10-27 03:44:57 +00:00 by inference-bot · 2 comments
Owner

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.
Author
Owner

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.
Author
Owner

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.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: co-op/support#36