Skip to content

Commit 6116ab5

Browse files
authored
Merge pull request from GHSA-p3q9-qff5-97p7
* Implement checksums for POST requests Previously, the checksum logic always looked at the query string, even on POST requests where the parameters are in the body. Add checksum support for POST parameters (this is somewhat complicated by the fact that scalelite's internal API uses Rails nested parameters), and match BigBlueButton's behaviour of rejecting a request if parameters are present in both the query string and POST request body. * Validate BigBlueButton API request content-type * Check 'GET' checksum on 'POST' request with json content type The checksum helper is also used on the Scalelite administration API, which is designed to be used with POST requests with json request body. The checker designed for the BigBlueButton APIs rejected this as an unsupported content type. The expected behaviour of these requests is to have the checksum in the query string, and the body isn't covered by the checksum. Adapt the code to support this behaviour. * Use right content type in Admin API tests, reject form data * Remove checksum validation workaround for admin api * Update supported request methods to match BigBlueButton * Remove incorrect POST req checksumming * Update tests for supported methods and content types * Remove unused GET_CHECKSUM_MIME_TYPES * Ensure create call does not pass parameters in POST body as-is to BBB * Fix formatting issues reported by rubocop * Correct list of endpoints supporting GET for content type check
1 parent f2deb3e commit 6116ab5

12 files changed

+286
-162
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
module Api
4+
class ScaleliteApiController < ApplicationController
5+
include ApiHelper
6+
7+
skip_before_action :verify_authenticity_token
8+
9+
before_action :verify_content_type
10+
before_action -> { verify_checksum(true) }
11+
12+
def verify_content_type
13+
return unless request.post? && request.content_length.positive?
14+
15+
raise UnsupportedContentType unless request.content_mime_type == Mime[:json]
16+
end
17+
end
18+
end

app/controllers/api/servers_controller.rb

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
# frozen_string_literal: true
22

33
module Api
4-
class ServersController < ApplicationController
5-
include ApiHelper
6-
7-
skip_before_action :verify_authenticity_token
8-
9-
before_action -> { verify_checksum(true) }
4+
class ServersController < ScaleliteApiController
105
before_action :set_server, only: [:get_server_info, :update_server, :delete_server, :panic_server]
116

127
# Return a list of the configured BigBlueButton servers

app/controllers/api/tenants_controller.rb

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
# frozen_string_literal: true
22

33
module Api
4-
class TenantsController < ApplicationController
5-
include ApiHelper
6-
7-
skip_before_action :verify_authenticity_token
8-
9-
before_action -> { verify_checksum(true) }
4+
class TenantsController < ScaleliteApiController
105
before_action :check_multitenancy
116
before_action :set_tenant, only: [:get_tenant_info, :update_tenant, :delete_tenant]
127

app/controllers/bigbluebutton_api_controller.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ class BigBlueButtonApiController < ApplicationController
55

66
skip_before_action :verify_authenticity_token
77

8+
# Check content types on endpoints that accept POST requests. For most endpoints, form data is permitted.
9+
before_action :verify_content_type, except: [:create, :insert_document, :join, :publish_recordings, :delete_recordings]
10+
# create allows either form data or XML
11+
before_action :verify_create_content_type, only: [:create]
12+
# insertDocument only allows XML
13+
before_action :verify_insert_document_content_type, only: [:insert_document]
14+
815
before_action :verify_checksum, except: [:index, :get_recordings_disabled, :recordings_disabled, :get_meetings_disabled,
916
:analytics_callback,]
1017

@@ -198,6 +205,8 @@ def create
198205

199206
params[:voiceBridge] = meeting.voice_bridge
200207

208+
have_preuploaded_slide = request.post? && request.content_mime_type == Mime[:xml]
209+
201210
logger.debug("Creating meeting #{params[:meetingID]} on BigBlueButton server #{server.id}")
202211
params_hash = params
203212

@@ -209,8 +218,8 @@ def create
209218
uri = encode_bbb_uri('create', server.url, server.secret, pass_through_params(excluded_params))
210219

211220
begin
212-
# Read the body if POST
213-
body = request.post? ? request.body.read : ''
221+
# Read the body if preuploaded slide XML is present
222+
body = have_preuploaded_slide ? request.raw_post : ''
214223

215224
# Send a GET/POST request to the server
216225
response = get_post_req(uri, body, **bbb_req_timeout(server))

app/controllers/concerns/api_helper.rb

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,39 @@ module ApiHelper
1919
# which should be accessed only by superadmins.
2020
def verify_checksum(force_loadbalancer_secret = false)
2121
secrets = fetch_secrets(force_loadbalancer_secret: force_loadbalancer_secret)
22-
23-
raise ChecksumError if params[:checksum].blank?
2422
raise ChecksumError if secrets.empty?
2523

26-
algorithm = case params[:checksum].length
27-
when CHECKSUM_LENGTH_SHA1
28-
'SHA1'
29-
when CHECKSUM_LENGTH_SHA256
30-
'SHA256'
31-
when CHECKSUM_LENGTH_SHA512
32-
'SHA512'
33-
else
34-
raise ChecksumError
35-
end
36-
37-
# Camel case (ex) get_meetings to getMeetings to match BBB server
38-
check_string = action_name.camelcase(:lower)
39-
check_string += request.query_string.gsub(
40-
/&checksum=#{params[:checksum]}|checksum=#{params[:checksum]}&|checksum=#{params[:checksum]}/, ''
41-
)
24+
checksum = request.params[:checksum]
25+
raise ChecksumError if checksum.blank?
4226

27+
algorithm = guess_checksum_digest_algorithm(checksum)
4328
allowed_checksum_algorithms = Rails.configuration.x.loadbalancer_checksum_algorithms
44-
raise ChecksumError unless allowed_checksum_algorithms.include? algorithm
29+
raise ChecksumError unless allowed_checksum_algorithms.include?(algorithm)
4530

46-
secrets.each do |secret|
47-
return true if ActiveSupport::SecurityUtils.secure_compare(get_checksum(check_string + secret, algorithm),
48-
params[:checksum])
31+
check_string = action_name.camelcase(:lower) + query_string_remove_checksum(checksum)
32+
return true if secrets.any? do |secret|
33+
ActiveSupport::SecurityUtils.secure_compare(get_checksum(check_string + secret, algorithm), checksum)
4934
end
5035

5136
raise ChecksumError
5237
end
5338

39+
# Remove the checksum from the request query string. This is done as string manipulation, rather than decoding then re-encoding the parameters,
40+
# since there's multiple possible valid encodings for query parameters, and the one used by Ruby might not match.
41+
def query_string_remove_checksum(checksum)
42+
checksum = Regexp.escape(checksum)
43+
request.query_string.gsub(/&checksum=#{checksum}|checksum=#{checksum}&|checksum=#{checksum}/, '')
44+
end
45+
46+
def guess_checksum_digest_algorithm(checksum)
47+
case checksum.length
48+
when CHECKSUM_LENGTH_SHA1 then 'SHA1'
49+
when CHECKSUM_LENGTH_SHA256 then 'SHA256'
50+
when CHECKSUM_LENGTH_SHA512 then 'SHA512'
51+
else raise ChecksumError
52+
end
53+
end
54+
5455
def fetch_secrets(tenant_name: nil, force_loadbalancer_secret: false)
5556
return Rails.configuration.x.loadbalancer_secrets if force_loadbalancer_secret || !Rails.configuration.x.multitenancy_enabled
5657

@@ -98,6 +99,27 @@ def checksum_algorithm
9899
end
99100
end
100101

102+
# Verify that the Content-Type of POST requests is a "form data" type (applies to most APIs)
103+
def verify_content_type
104+
return unless request.post? && request.content_length.positive?
105+
raise UnsupportedContentType unless request.form_data?
106+
end
107+
108+
# Verify that the Content-Type of a POST request is a format permitted by the create API.
109+
# This can either be form data containing params, or an XML document for pre-uploaded slides
110+
CREATE_PERMITTED_CONTENT_TYPES = Set.new([Mime[:url_encoded_form], Mime[:multipart_form], Mime[:xml]]).freeze
111+
def verify_create_content_type
112+
return unless request.post? && request.content_length.positive?
113+
raise UnsupportedContentType unless CREATE_PERMITTED_CONTENT_TYPES.include?(request.content_mime_type)
114+
end
115+
116+
# Verify that the Content-Type of a POST request is a format permitted by the insertDocument API.
117+
# Only an XML document for pre-uploaded slides (same format as create) is permitted.
118+
def verify_insert_document_content_type
119+
return unless request.post? && request.content_length.positive?
120+
raise UnsupportedContentType unless request.content_mime_type == Mime[:xml]
121+
end
122+
101123
# Encode URI and append checksum
102124
def encode_bbb_uri(action, base_uri, secret, bbb_params = {})
103125
# Add slash at the end if its not there

app/controllers/concerns/bbb_errors.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ def initialize
4040
end
4141
end
4242

43+
class UnsupportedContentType < BBBErrors::BBBError
44+
def initialize
45+
super('unsupportedContentType', 'POST request Content-Type is missing or unsupported')
46+
end
47+
end
48+
4349
class InternalError < BBBError
4450
def initialize(error)
4551
super('internalError', error)

config/routes.rb

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
Rails.application.routes.draw do
44
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
55

6-
scope 'bigbluebutton/api', as: 'bigbluebutton_api', format: false, defaults: { format: 'xml' } do
6+
scope 'bigbluebutton/api', as: :bigbluebutton_api, format: false, defaults: { format: 'xml' } do
7+
# See https://github.com/bigbluebutton/bigbluebutton/blob/main/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/UrlMappings.groovy
8+
# for the definitions of the routes in BigBlueButton itself. Note that both private (BBB internal) and public APIs are in that file.
9+
710
match '/', to: 'bigbluebutton_api#index', via: [:get, :post]
811
match 'isMeetingRunning', to: 'bigbluebutton_api#is_meeting_running', as: :is_meeting_running, via: [:get, :post]
912
match 'getMeetingInfo', to: 'bigbluebutton_api#get_meeting_info', as: :get_meeting_info, via: [:get, :post]
@@ -14,19 +17,19 @@
1417
end
1518
match 'create', to: 'bigbluebutton_api#create', via: [:get, :post]
1619
match 'end', to: 'bigbluebutton_api#end', via: [:get, :post]
17-
match 'join', to: 'bigbluebutton_api#join', via: [:get, :post]
18-
post 'analytics_callback', to: 'bigbluebutton_api#analytics_callback', as: :analytics_callback
19-
post 'insertDocument', to: 'bigbluebutton_api#insert_document'
20+
get 'join', to: 'bigbluebutton_api#join'
21+
post 'analytics_callback', to: 'bigbluebutton_api#analytics_callback'
22+
post 'insertDocument', to: 'bigbluebutton_api#insert_document', as: :insert_document
2023
if Rails.configuration.x.recording_disabled
2124
match('getRecordings', to: 'bigbluebutton_api#get_recordings_disabled', as: :get_recordings, via: [:get, :post])
22-
match('publishRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :publish_recordings, via: [:get, :post])
25+
get('publishRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :publish_recordings)
2326
match('updateRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :update_recordings, via: [:get, :post])
24-
match('deleteRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :delete_recordings, via: [:get, :post])
27+
get('deleteRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :delete_recordings)
2528
else
2629
match('getRecordings', to: 'bigbluebutton_api#get_recordings', as: :get_recordings, via: [:get, :post])
27-
match('publishRecordings', to: 'bigbluebutton_api#publish_recordings', as: :publish_recordings, via: [:get, :post])
30+
get('publishRecordings', to: 'bigbluebutton_api#publish_recordings', as: :publish_recordings)
2831
match('updateRecordings', to: 'bigbluebutton_api#update_recordings', as: :update_recordings, via: [:get, :post])
29-
match('deleteRecordings', to: 'bigbluebutton_api#delete_recordings', as: :delete_recordings, via: [:get, :post])
32+
get('deleteRecordings', to: 'bigbluebutton_api#delete_recordings', as: :delete_recordings)
3033
end
3134
end
3235

spec/controllers/concerns/api_helper_spec.rb

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
describe '.verify_checksum' do
5656
let(:query_string) { 'querystring' }
5757
let(:action_name) { 'index' }
58+
let(:method) { :get }
5859
let(:check_string) { action_name + query_string }
5960
let(:checksum_algo) { nil } # To be defined down the scope
6061
let(:secret) { 'IAmSecret' }
@@ -63,10 +64,19 @@
6364
before do
6465
controller.action_name = action_name
6566
allow(request).to receive(:query_string).and_return(query_string)
67+
allow(request).to receive(:request_method_symbol).and_return(method)
6668
Rails.configuration.x.loadbalancer_secrets = [secret]
69+
if hash.present?
70+
case method
71+
when :get then request.query_parameters[:checksum] = hash
72+
when :post then request.request_parameters[:checksum] = hash
73+
end
74+
end
6775
end
6876

6977
context 'without params[:checksum]' do
78+
let(:hash) { nil }
79+
7080
it 'throws an error' do
7181
expect {
7282
verify_checksum
@@ -78,39 +88,24 @@
7888
context 'with SHA1' do
7989
let(:checksum_algo) { 'SHA1' }
8090

81-
before do
82-
params[:checksum] = hash
83-
end
84-
8591
include_examples 'proper verify_checksum behavior'
8692
end
8793

8894
context 'with SHA256' do
8995
let(:checksum_algo) { 'SHA256' }
9096

91-
before do
92-
params[:checksum] = hash
93-
end
94-
9597
include_examples 'proper verify_checksum behavior'
9698
end
9799

98100
context 'with SHA512' do
99101
let(:checksum_algo) { 'SHA512' }
100102

101-
before do
102-
params[:checksum] = hash
103-
end
104-
105103
include_examples 'proper verify_checksum behavior'
106104
end
107105

108106
context 'with incorrect checksum' do
109107
let(:checksum_algo) { 'MD5' }
110-
111-
before do
112-
params[:checksum] = 'totallyNotAHash'
113-
end
108+
let(:hash) { 'totallyNotAHash' }
114109

115110
it 'throws an error' do
116111
expect {

0 commit comments

Comments
 (0)