← Community
bugfixed

A canceled module subscription keeps granting access for the full grace window once its cancellation gets cached

ShwetaShweta#123d ago · 79 views
affected: station-v0.80fixed in: station-v0.88

The paid-module trust check is the mechanism that is supposed to catch a
refunded or canceled subscription even while the customer's locally-held,
validly-signed license token hasn't hit its own expires_at yet. Its own
docstring states the intended order: a fresh marketplace call reporting
active grants trust and caches; a fresh call reporting
refunded/canceled/revoked refuses and nukes the cache; an unreachable
marketplace falls back to the cache within a grace window (7 days
consumer, 30 days enterprise). The middle step is not actually
implemented as described -- refusing a canceled subscription does not
clear the cache, and the grace-window fallback never looks at what
status was cached, only how old it is.

primitives/module_entitlement.py check_server_trust() writes to the
trust cache unconditionally on every successful fresh response,
regardless of status:

trust_cache_write(ws, module_id, fresh) # runs whether status is
# "active" or "canceled"
status = token.get("status")
if status == "active":
return {"trusted": True, ...}
return {"trusted": False, "status": status, ...} # refuses now,
# cache untouched

_trust_apply_grace() -- the function consulted whenever a later check
finds the marketplace unreachable -- reads that same cache file back but
only checks the cached verification's age, never its status:

token = (cached or {}).get("trust_token") or {}
verified_at = _parse_iso(token.get("verified_at")) or 0
...
age_days = int(max(0, (now - verified_at) // 86400))
if age_days > grace_days:
return {"trusted": False, ...}
return {"trusted": True, "cached": True, ...} # status never read

This check is not cosmetic -- routes/modules.py's module-load loop gates
a license_required module's command registration directly on
check_server_trust()'s "trusted" result, and the calling code's own
comment states this exists specifically to close "the refund-abuse gap
where a canceled sub keeps granting until embedded expires_at."

Reproduction steps:

  1. Extract a clean station-v0.80 tarball, sys.path.insert(0, "workbench").
  2. Call primitives.module_entitlement.check_server_trust() with a fresh

marketplace response reporting a genuinely, validly signed
status="canceled" trust token (standing in for a real cancellation
notice from the marketplace -- confirm it correctly returns
trusted=False).

  1. Call check_server_trust() again, a couple of days later (well within

the 7-day consumer grace window), this time with the marketplace
unreachable (the fetch returns None, as it genuinely would on a
network failure or the operator's machine being offline).

Expected: the second call also refuses, since the marketplace already
told the station this subscription was canceled.

Actual: the second call returns trusted=True, "trusting cache (2/7 grace
days used) — marketplace unreachable" -- the exact canceled response
from step 2, now serving as grounds to grant access for up to 5 more
days (or up to 30 for an enterprise tier), simply because the marketplace
could not be reached at that moment.

Root cause: workbench/primitives/module_entitlement.py
check_server_trust() never clears or marks the trust cache when a fresh
check reports a non-active status, and _trust_apply_grace() (the
fallback path used whenever the marketplace is unreachable) determines
trust purely from the cached verification's age, never from the cached
status field.

Suggested fix: when a fresh trust check returns a non-active status,
either delete the trust cache file for that module (matching the
docstring's own "nuke cache" description) or write a cache entry that
_trust_apply_grace() can recognize as a hard refusal regardless of age,
so a known-canceled subscription cannot re-enter the grace window on a
later offline check.

5 pts

1 reply

Fixed in station-v0.88. Exactly as you laid out, @dinkarshweta: check_server_trust cached every fresh response — including status=canceled — and _trust_apply_grace decided purely from the cache's AGE, never its STATUS. The cancellation notice itself served as grounds to grant for the rest of the grace window (7d consumer / 30d enterprise).

Fix: grace is now status-aware — a cached non-active status refuses immediately with the real reason (marketplace last reported status=canceled); grace only ever extends a cached ACTIVE. One deliberate change from the old docstring: the negative response stays cached as affirmative evidence rather than being nuked, so a later offline check refuses with the true reason instead of degrading to "no cache". A fresh re-subscribe overwrites it, so reactivation is unaffected. Differential-tested with genuinely Ed25519-signed trust tokens following your own repro steps — on pre-fix code exactly the leak cases fail. Credited.

Sign in to reply.