How Scalelite Playback Broke Tenant Isolation in BigBlueButton Multitenancy

ruby dev.to

While researching the BigBlueButton bug bounty program on YesWeHack, I found an access control issue in Scalelite’s recording playback flow.

The problem appeared when multitenancy was enabled.

Scalelite already derived tenant context from the request hostname for BigBlueButton API requests and scoped recording queries to that tenant. The playback controller did not follow the same model. It resolved recordings using only record_id and playback format.

As a result, a recording assigned to Tenant A could also be requested through Tenant B’s hostname if the same record_id was known.

I submitted the finding as High, with CVSS 3.1 score 7.5. Triage later classified it as Medium, with CVSS score 5.3. The report received a €250 bounty.

The intended tenant boundary

When Scalelite multitenancy is enabled through MULTITENANCY_ENABLED=true, tenant identity is derived from the request hostname.

The helper responsible for that behavior was:

def fetch_tenant_name_from_url
  request.host.split(".").first
end

def fetch_tenant(name: nil)
  return nil unless Rails.configuration.x.multitenancy_enabled

  tenant_name = name.presence || fetch_tenant_name_from_url
  tenant = Tenant.find_by_name(tenant_name)
  raise ChecksumError if tenant.blank?

  tenant
end
Enter fullscreen mode Exit fullscreen mode

For example, these hostnames represent different tenant contexts:

tenantA.sl.example.com
tenantB.sl.example.com
Enter fullscreen mode Exit fullscreen mode

The project documentation also states that each tenant should have access only to its own meetings and recordings.

The BigBlueButton API controller followed this model. When multitenancy was enabled, it resolved the current tenant and used that context when querying recordings.

The recording query included a metadata filter matching tenant-id to the current tenant:

query = Recording
        .includes(playback_formats: [:thumbnails], metadata: [])
        .left_joins(:metadata)
        .distinct

query = query.where(
  metadata: {
    key: "tenant-id",
    value: @tenant.id
  }
) if @tenant.present?
Enter fullscreen mode Exit fullscreen mode

So the tenant boundary already existed for recording discovery through the API.

Playback used a global lookup

The vulnerable behavior was inside PlaybackController.

Both playback actions resolved a recording using its identifier and playback format:

@playback_format = PlaybackFormat
                   .joins(:recording)
                   .find_by!(
                     format: params[:playback_format],
                     recordings: {
                       record_id: params[:record_id]
                     }
                   )

@recording = @playback_format.recording
Enter fullscreen mode Exit fullscreen mode

There was no tenant lookup from the hostname.

There was no before_action :set_tenant.

There was no condition requiring the recording metadata to contain a tenant-id matching the current request.

The query therefore answered only whether a recording with the requested identifier and format existed.

It did not verify whether that recording belonged to the tenant represented by the hostname.

Reproducing the issue

I reproduced the problem with an RSpec request test while multitenancy was enabled.

The test created a published recording and explicitly associated it with Tenant A:

create(
  :metadatum,
  recording: recording,
  key: "tenant-id",
  value: "tenantA-id"
)
Enter fullscreen mode Exit fullscreen mode

A presentation playback resource was then created for the same recording:

create(
  :playback_format,
  recording: recording,
  format: "presentation",
  url: "/presentation/#{recording.record_id}/index.html"
)
Enter fullscreen mode Exit fullscreen mode

The first request used Tenant A’s hostname and returned HTTP 200 as expected.

The second request used Tenant B’s hostname while keeping the same path and record_id.

That request also returned HTTP 200.

Both responses exposed the same X-Accel-Redirect target for the recording resource.

The recording ownership never changed. Only the request hostname changed.

The test completed successfully:

Finished in 0.19804 seconds
2 examples, 0 failures
Enter fullscreen mode Exit fullscreen mode

This demonstrated that playback authorization depended on possession of the record_id, not on tenant ownership.

Why this matters

The affected playback routes can serve recording pages and their associated resources.

Depending on the generated recording format and meeting content, those resources may include presentation material, media, captions, notes, chat content, participant related information, and other recording assets.

The attacker still needs to know or obtain the victim recording’s record_id.

The vulnerability is not that every identifier is automatically discoverable. The issue is that once an identifier is known, the playback path does not enforce the tenant boundary that multitenancy is supposed to provide.

Protected recordings also have their own token and cookie mechanism. That mechanism is separate from tenant ownership and does not replace tenant isolation.

Why this is an access control flaw

The key difference is between object existence and object ownership.

The vulnerable lookup effectively used:

record_id + format
Enter fullscreen mode Exit fullscreen mode

Under multitenancy, the authorization decision needed to include the current tenant:

tenant + record_id + format
Enter fullscreen mode Exit fullscreen mode

The API side already enforced that distinction through tenant-id metadata.

Playback did not.

That is why the issue fits CWE 284, Improper Access Control. It also matches the authorization bypass pattern described by CWE 639, where a user controlled object identifier can reach data outside the caller’s authorization boundary.

Severity

I originally submitted the issue with this CVSS 3.1 vector:

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Enter fullscreen mode Exit fullscreen mode

That produced a base score of 7.5.

During triage, the confidentiality impact was reduced from High to Low, resulting in:

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Enter fullscreen mode Exit fullscreen mode

The final score was 5.3, and the finding was classified as Medium.

The public writeup preserves both values because the first reflects the submitted assessment and the second reflects the final program classification.

Remediation

The playback path should enforce the same tenant ownership rule already used by the recording API.

When multitenancy is enabled, PlaybackController should resolve the tenant from the request hostname and scope the recording lookup to metadata where tenant-id matches that tenant.

The expected behavior should be:

Tenant A requesting Tenant A recording = HTTP 200
Tenant B requesting Tenant A recording = HTTP 404
Enter fullscreen mode Exit fullscreen mode

The cross tenant response should not receive an X-Accel-Redirect for the recording resource.

Legacy recordings without tenant metadata also need an explicit policy. The safer behavior under multitenancy is to deny access until ownership has been established or migrated.

A regression test should keep this boundary covered by creating a recording for one tenant and confirming that the same identifier cannot be served through another tenant hostname.

The broader lesson

Multitenancy failures often appear when different application surfaces reach the same object through different lookup paths.

In this case, the recordings API understood tenant ownership.

Playback reached the same underlying data without carrying that context forward.

That inconsistency was enough to break the isolation model.

The useful question when reviewing a multitenant application is:

Does every path that can return this object enforce the same tenant boundary?

For Scalelite recording playback, the answer was no.

The recording belonged to one tenant in metadata, but playback resolved it globally by record_id and format.

That was enough to allow cross tenant access to recording playback resources.

Source: dev.to

arrow_back Back to Tutorials