Skip to content

fix(mediorum): fail closed when the cidstream access lookup errors - #503

Open
rickyrombo wants to merge 1 commit into
mainfrom
fix/cidstream-auth-fail-closed
Open

fix(mediorum): fail closed when the cidstream access lookup errors#503
rickyrombo wants to merge 1 commit into
mainfrom
fix/cidstream-auth-fail-closed

Conversation

@rickyrombo

Copy link
Copy Markdown
Contributor

The bug

The cidstream authorization path issued its GORM raw queries and discarded res.Error:

s.crud.DB.Raw("SELECT track_id FROM sound_recordings WHERE cid = ?", cid).Scan(&trackID)
s.crud.DB.Raw("SELECT COUNT(*) FROM management_keys WHERE track_id = ?", trackID).Scan(&managementKeyCount)

If either query errors — table missing, connection blip, permissions — trackID stays "" and managementKeyCount stays 0. requireRegisteredSignature reads that as "this track has no access authorities", skips the access-authority check entirely, and falls through to the registered-validator-signature path. A gated track becomes streamable by any registered signer.

This was reproduced empirically while working on #502: deliberately pointing one literal at a nonexistent table produced exactly that bypass rather than an error.

The fix

Every site now checks res.Error and denies.

  • The uncached lookup moves into lookupTrackAccessInfo, which returns an error rather than a zero-valued struct the caller cannot tell apart from an ungated track. Its result is cached only on success, so a transient failure is not memoized for five minutes.
  • The per-signer authority count is guarded separately, since it runs after a cache hit.
  • serveTrack and streamTrackGRPC get the same treatment. Their swallowed errors previously surfaced as track not found and signer not authorized — plausible-looking answers derived from a query that never ran.
  • Response bodies stay generic ("unable to verify track access"); the driver error goes to a Warn log rather than to an unauthenticated caller.

The track-duration lookup stays non-fatal on purpose: it only sizes the presigned-URL expiry, not the access decision, and one malformed transcode_results row fails the whole ::jsonb cast — that should not take streaming down.

Tests

Eight new tests, across all three functions. Two failure injectors:

  • failEveryQuery — a cancelled-context gorm handle, standing in for the database being unreachable.
  • hideManagementKeys — renames the table away while leaving sound_recordings intact, which is the shape hit on refactor(core): prefix chain-derived tables with core_ #502: the cid→track_id lookup still succeeds and only the authority queries error.

The central test gates a track behind an unrelated authority and registers the fixture signer as a validator, so a fail-open lookup sails straight through the no-access-authorities branch. That is the bypass; it now returns 500.

All eight fail without the source change and pass with it. serveTrack and streamTrackGRPC had no coverage at all before this.

Behavior change worth a look before merging

Failing closed turned two pre-existing tests red, for a real reason rather than a test artifact. They run against a server whose database has no sound_recordings table, so under the old code every request there silently took the ungated path.

sound_recordings and management_keys come from core's migrations (pkg/core/db/sql/migrations/00018_release_metadata.sql), not mediorum's. Same shared database in production, so they normally exist — but a node where mediorum serves before core's migrations have run now denies cidstream traffic instead of degrading to validator-signature auth. That is the correct security posture and I would keep it, but it is a genuine availability edge on fresh-node bootstrap, and it argues for watching the track access lookup failed warn line after rollout.

Conflicts

Touches the same lines #502 renames (sound_recordingscore_sound_recordings, management_keyscore_management_keys). Whichever lands second needs a rebase.

🤖 Generated with Claude Code

The cidstream authorization path issued its GORM raw queries and discarded
res.Error. A failed query left trackID empty and managementKeyCount zero,
which requireRegisteredSignature reads as "this track has no access
authorities" -- so it skipped the access-authority check entirely and fell
through to the registered-validator-signature path. Any registered signer
could stream a gated track. Reproduced by pointing one query at a
nonexistent table: the request was authorized rather than rejected.

Check res.Error at every site and deny instead. The uncached lookup moves
into lookupTrackAccessInfo, which returns an error rather than a
zero-valued struct the caller cannot distinguish from an ungated track, and
its result is only cached on success so a transient failure is not memoized
for five minutes. serveTrack and streamTrackGRPC get the same treatment;
their swallowed errors surfaced as "track not found" and "signer not
authorized" rather than as failures.

The track-duration lookup stays non-fatal: it only sizes the presigned URL
expiry, and one malformed transcode_results row fails the whole ::jsonb
cast, which should not take streaming down.

Note this is a real behavior change for a node whose database lacks core's
sound_recordings table -- it now denies cidstream requests instead of
silently treating every track as ungated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant