Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Rate limiting to prevent Room-ID enumeration attacks ([#2518])

### Changed

- Internal improvements to room authentication flow ([#1409], [#2726])
Expand Down Expand Up @@ -663,6 +667,7 @@ You can find the changelog for older versions there [here](https://github.com/TH
[#2496]: https://github.com/THM-Health/PILOS/issues/2496
[#2497]: https://github.com/THM-Health/PILOS/pull/2497
[#2517]: https://github.com/THM-Health/PILOS/pull/2517
[#2518]: https://github.com/THM-Health/PILOS/pull/2518
[#2519]: https://github.com/THM-Health/PILOS/pull/2519
[#2551]: https://github.com/THM-Health/PILOS/pull/2551
[#2553]: https://github.com/THM-Health/PILOS/pull/2553
Expand Down
17 changes: 17 additions & 0 deletions app/Providers/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace App\Providers;

use App\Models\Room;
use App\Models\User;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
use Response;

class RouteServiceProvider extends ServiceProvider
{
Expand Down Expand Up @@ -71,5 +73,20 @@ protected function configureRateLimiting(): void
// If the user is not editing himself, no rate limit (use the default rate limit, see api rate limit)
return Limit::none();
});

RateLimiter::for('room-enumeration', function (Request $request) {
return Limit::perMinute(10)
->by($request->user()?->id ?: $request->ip())
->after(function (\Symfony\Component\HttpFoundation\Response $response) use ($request) {
// If the response is not a 404, do not count this request
if ($response->getStatusCode() !== 404) {
return false;
}

// Only count the request if the route parameter 'room' was not resolved to a Room model
// Prevent counting requests that are valid and return a 404 for other reasons
return ! ($request->route('room') instanceof Room);
});
});
}
}
141 changes: 74 additions & 67 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,63 +73,67 @@

Route::get('rooms', [RoomController::class, 'index'])->name('rooms.index');
Route::post('rooms', [RoomController::class, 'store'])->name('rooms.store');
Route::post('rooms/{room}/favorites', [RoomController::class, 'addToFavorites'])->name('rooms.favorites.add');
Route::delete('rooms/{room}/favorites', [RoomController::class, 'deleteFromFavorites'])->name('rooms.favorites.delete');
Route::put('rooms/{room}', [RoomController::class, 'update'])->name('rooms.update');
Route::delete('rooms/{room}', [RoomController::class, 'destroy'])->name('rooms.destroy');

Route::get('rooms/{room}/settings', [RoomController::class, 'getSettings'])->name('rooms.settings');
Route::middleware('throttle:room-enumeration')->group(function () {
Route::post('rooms/{room}/favorites', [RoomController::class, 'addToFavorites'])->name('rooms.favorites.add');
Route::delete('rooms/{room}/favorites', [RoomController::class, 'deleteFromFavorites'])->name('rooms.favorites.delete');
Route::put('rooms/{room}', [RoomController::class, 'update'])->name('rooms.update');
Route::delete('rooms/{room}', [RoomController::class, 'destroy'])->name('rooms.destroy');

Route::get('rooms/{room}/settings', [RoomController::class, 'getSettings'])->name('rooms.settings');

Route::put('rooms/{room}/description', [RoomController::class, 'updateDescription'])->name('rooms.description.update')->middleware('can:update,room');

Route::post('rooms/{room}/transfer', [RoomController::class, 'transferOwnership'])->name('rooms.transfer')->middleware('can:transfer,room');

// Membership user self add/remove
Route::post('rooms/{room}/membership', [RoomMemberController::class, 'join'])->name('rooms.membership.join');
Route::delete('rooms/{room}/membership', [RoomMemberController::class, 'leave'])->name('rooms.membership.leave');

// Membership users for mass update & delete
Route::post('rooms/{room}/member/bulk', [RoomMemberController::class, 'bulkImport'])->name('rooms.member.bulkImport')->middleware('can:manageMembers,room');
Route::put('rooms/{room}/member/bulk', [RoomMemberController::class, 'bulkUpdate'])->name('rooms.member.bulkUpdate')->middleware('can:manageMembers,room');
Route::delete('rooms/{room}/member/bulk', [RoomMemberController::class, 'bulkDestroy'])->name('rooms.member.bulkDestroy')->middleware('can:manageMembers,room');

// Membership operations by room owner
Route::get('rooms/{room}/member', [RoomMemberController::class, 'index'])->name('rooms.member.get')->middleware('can:viewMembers,room');
Route::post('rooms/{room}/member', [RoomMemberController::class, 'store'])->name('rooms.member.add')->middleware('can:manageMembers,room');
Route::put('rooms/{room}/member/{user}', [RoomMemberController::class, 'update'])->name('rooms.member.update')->middleware('can:manageMembers,room');
Route::delete('rooms/{room}/member/{user}', [RoomMemberController::class, 'destroy'])->name('rooms.member.destroy')->middleware('can:manageMembers,room');

// Recording operations
Route::middleware('can:manageRecordings,room')->scopeBindings()->group(function () {
Route::put('rooms/{room}/recordings/{recording}', [RecordingController::class, 'update'])->name('rooms.recordings.update');
Route::delete('rooms/{room}/recordings/{recording}', [RecordingController::class, 'destroy'])->name('rooms.recordings.destroy');
});

// Streaming operations
Route::middleware('can:viewStreaming,room')->scopeBindings()->group(function () {
Route::get('rooms/{room}/streaming/config', [RoomStreamingController::class, 'getConfig'])->name('rooms.streaming.config.get');
Route::get('rooms/{room}/streaming/status', [RoomStreamingController::class, 'status'])->name('rooms.streaming.status');
});

Route::middleware('can:manageStreaming,room')->scopeBindings()->group(function () {
Route::put('rooms/{room}/streaming/config', [RoomStreamingController::class, 'updateConfig'])->name('rooms.streaming.config.update');
Route::post('rooms/{room}/streaming/start', [RoomStreamingController::class, 'start'])->name('rooms.streaming.start');
Route::post('rooms/{room}/streaming/stop', [RoomStreamingController::class, 'stop'])->name('rooms.streaming.stop');
Route::post('rooms/{room}/streaming/pause', [RoomStreamingController::class, 'pause'])->name('rooms.streaming.pause');
Route::post('rooms/{room}/streaming/resume', [RoomStreamingController::class, 'resume'])->name('rooms.streaming.resume');
});

// Personalized room links
Route::get('rooms/{room}/personalizedLinks', [RoomPersonalizedLinkController::class, 'index'])->name('rooms.personalizedLinks.get')->middleware('can:viewPersonalizedLinks,room');
Route::post('rooms/{room}/personalizedLinks', [RoomPersonalizedLinkController::class, 'store'])->name('rooms.personalizedLinks.add')->middleware('can:managePersonalizedLinks,room');
Route::put('rooms/{room}/personalizedLinks/{link}', [RoomPersonalizedLinkController::class, 'update'])->name('rooms.personalizedLinks.update')->middleware('can:managePersonalizedLinks,room');
Route::delete('rooms/{room}/personalizedLinks/{link}', [RoomPersonalizedLinkController::class, 'destroy'])->name('rooms.personalizedLinks.destroy')->middleware('can:managePersonalizedLinks,room');

// File operations
Route::middleware('can:manageFiles,room')->scopeBindings()->group(function () {
Route::post('rooms/{room}/files', [RoomFileController::class, 'store'])->name('rooms.files.add');
Route::put('rooms/{room}/files/{file}', [RoomFileController::class, 'update'])->name('rooms.files.update');
Route::delete('rooms/{room}/files/{file}', [RoomFileController::class, 'destroy'])->name('rooms.files.destroy');
});

Route::put('rooms/{room}/description', [RoomController::class, 'updateDescription'])->name('rooms.description.update')->middleware('can:update,room');

Route::post('rooms/{room}/transfer', [RoomController::class, 'transferOwnership'])->name('rooms.transfer')->middleware('can:transfer,room');

// Membership user self add/remove
Route::post('rooms/{room}/membership', [RoomMemberController::class, 'join'])->name('rooms.membership.join');
Route::delete('rooms/{room}/membership', [RoomMemberController::class, 'leave'])->name('rooms.membership.leave');

// Membership users for mass update & delete
Route::post('rooms/{room}/member/bulk', [RoomMemberController::class, 'bulkImport'])->name('rooms.member.bulkImport')->middleware('can:manageMembers,room');
Route::put('rooms/{room}/member/bulk', [RoomMemberController::class, 'bulkUpdate'])->name('rooms.member.bulkUpdate')->middleware('can:manageMembers,room');
Route::delete('rooms/{room}/member/bulk', [RoomMemberController::class, 'bulkDestroy'])->name('rooms.member.bulkDestroy')->middleware('can:manageMembers,room');

// Membership operations by room owner
Route::get('rooms/{room}/member', [RoomMemberController::class, 'index'])->name('rooms.member.get')->middleware('can:viewMembers,room');
Route::post('rooms/{room}/member', [RoomMemberController::class, 'store'])->name('rooms.member.add')->middleware('can:manageMembers,room');
Route::put('rooms/{room}/member/{user}', [RoomMemberController::class, 'update'])->name('rooms.member.update')->middleware('can:manageMembers,room');
Route::delete('rooms/{room}/member/{user}', [RoomMemberController::class, 'destroy'])->name('rooms.member.destroy')->middleware('can:manageMembers,room');

// Recording operations
Route::middleware('can:manageRecordings,room')->scopeBindings()->group(function () {
Route::put('rooms/{room}/recordings/{recording}', [RecordingController::class, 'update'])->name('rooms.recordings.update');
Route::delete('rooms/{room}/recordings/{recording}', [RecordingController::class, 'destroy'])->name('rooms.recordings.destroy');
});

// Streaming operations
Route::middleware('can:viewStreaming,room')->scopeBindings()->group(function () {
Route::get('rooms/{room}/streaming/config', [RoomStreamingController::class, 'getConfig'])->name('rooms.streaming.config.get');
Route::get('rooms/{room}/streaming/status', [RoomStreamingController::class, 'status'])->name('rooms.streaming.status');
});

Route::middleware('can:manageStreaming,room')->scopeBindings()->group(function () {
Route::put('rooms/{room}/streaming/config', [RoomStreamingController::class, 'updateConfig'])->name('rooms.streaming.config.update');
Route::post('rooms/{room}/streaming/start', [RoomStreamingController::class, 'start'])->name('rooms.streaming.start');
Route::post('rooms/{room}/streaming/stop', [RoomStreamingController::class, 'stop'])->name('rooms.streaming.stop');
Route::post('rooms/{room}/streaming/pause', [RoomStreamingController::class, 'pause'])->name('rooms.streaming.pause');
Route::post('rooms/{room}/streaming/resume', [RoomStreamingController::class, 'resume'])->name('rooms.streaming.resume');
});

// Personalized room links
Route::get('rooms/{room}/personalizedLinks', [RoomPersonalizedLinkController::class, 'index'])->name('rooms.personalizedLinks.get')->middleware('can:viewPersonalizedLinks,room');
Route::post('rooms/{room}/personalizedLinks', [RoomPersonalizedLinkController::class, 'store'])->name('rooms.personalizedLinks.add')->middleware('can:managePersonalizedLinks,room');
Route::put('rooms/{room}/personalizedLinks/{link}', [RoomPersonalizedLinkController::class, 'update'])->name('rooms.personalizedLinks.update')->middleware('can:managePersonalizedLinks,room');
Route::delete('rooms/{room}/personalizedLinks/{link}', [RoomPersonalizedLinkController::class, 'destroy'])->name('rooms.personalizedLinks.destroy')->middleware('can:managePersonalizedLinks,room');

// File operations
Route::middleware('can:manageFiles,room')->scopeBindings()->group(function () {
Route::post('rooms/{room}/files', [RoomFileController::class, 'store'])->name('rooms.files.add');
Route::put('rooms/{room}/files/{file}', [RoomFileController::class, 'update'])->name('rooms.files.update');
Route::delete('rooms/{room}/files/{file}', [RoomFileController::class, 'destroy'])->name('rooms.files.destroy');
});

Route::get('users/search', [UserController::class, 'search'])->name('users.search');
Expand All @@ -156,28 +160,31 @@
Route::get('meetings/{meeting}/attendance', [MeetingController::class, 'attendance'])->name('meetings.attendance');
Route::get('meetings/{meeting}/stats', [MeetingController::class, 'stats'])->name('meetings.stats');
Route::get('meetings', [MeetingController::class, 'index'])->name('meetings.index');
Route::get('rooms/{room}/meetings', [RoomController::class, 'meetings'])->name('rooms.meetings');

Route::get('rooms/{room}/meetings', [RoomController::class, 'meetings'])->middleware('throttle:room-enumeration')->name('rooms.meetings');

Route::get('getTimezones', function () {
return response()->json(['data' => timezone_identifiers_list()]);
});
});

Route::get('rooms/{room}', [RoomController::class, 'show'])->name('rooms.show')->middleware('room.authenticate:true');
Route::post('rooms/{room}/auth', [RoomController::class, 'authenticate'])->name('rooms.authenticate');
Route::middleware('throttle:room-enumeration')->group(function () {
Route::get('rooms/{room}', [RoomController::class, 'show'])->name('rooms.show')->middleware('room.authenticate:true');
Route::post('rooms/{room}/auth', [RoomController::class, 'authenticate'])->name('rooms.authenticate');

Route::middleware('room.authenticate')->scopeBindings()->group(function () {
Route::options('rooms/{room}/start', [RoomController::class, 'getStartRequirements'])->name('rooms.start-requirements')->middleware('can:start,room');
Route::options('rooms/{room}/join', [RoomController::class, 'getJoinRequirements'])->name('rooms.join-requirements');
Route::middleware('room.authenticate')->scopeBindings()->group(function () {
Route::options('rooms/{room}/start', [RoomController::class, 'getStartRequirements'])->name('rooms.start-requirements')->middleware('can:start,room');
Route::options('rooms/{room}/join', [RoomController::class, 'getJoinRequirements'])->name('rooms.join-requirements');

Route::post('rooms/{room}/start', [RoomController::class, 'start'])->name('rooms.start')->middleware('can:start,room');
Route::post('rooms/{room}/join', [RoomController::class, 'join'])->name('rooms.join');
Route::get('rooms/{room}/files', [RoomFileController::class, 'index'])->name('rooms.files.get');
Route::get('rooms/{room}/recordings', [RecordingController::class, 'index'])->name('rooms.recordings.index');
Route::get('rooms/{room}/recordings/{recording}/formats/{format}', [RecordingFormatController::class, 'show'])->name('rooms.recordings.formats.show')->middleware('can:viewRecordingFormat,room,format');
Route::post('rooms/{room}/start', [RoomController::class, 'start'])->name('rooms.start')->middleware('can:start,room');
Route::post('rooms/{room}/join', [RoomController::class, 'join'])->name('rooms.join');
Route::get('rooms/{room}/files', [RoomFileController::class, 'index'])->name('rooms.files.get');
Route::get('rooms/{room}/recordings', [RecordingController::class, 'index'])->name('rooms.recordings.index');
Route::get('rooms/{room}/recordings/{recording}/formats/{format}', [RecordingFormatController::class, 'show'])->name('rooms.recordings.formats.show')->middleware('can:viewRecordingFormat,room,format');

});
});

});
Route::get('meetings/{meeting}/endCallback', [MeetingController::class, 'endMeetingCallback'])->name('meetings.endcallback');
});

Expand Down
2 changes: 1 addition & 1 deletion routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
|
*/
Route::get('download/file/{roomFile}/{filename?}', [RoomFileController::class, 'showPresentation'])->name('rooms.files.download.bbb')->middleware('signed');
Route::get('room/{room}/file/{file}/{filename?}', [RoomFileController::class, 'show'])->name('rooms.files.download')->middleware(['signed:room_auth_token,room_auth_token_type', 'room.authenticate'])->scopeBindings();
Route::get('room/{room}/file/{file}/{filename?}', [RoomFileController::class, 'show'])->name('rooms.files.download')->middleware(['signed:room_auth_token,room_auth_token_type', 'room.authenticate', 'throttle:room-enumeration'])->scopeBindings();
Route::get('download/attendance/{meeting}', [MeetingController::class, 'attendance'])->name('download.attendance')->middleware('auth:users,ldap');
Route::get('download/recording/{recording}', [RecordingController::class, 'download'])->middleware('auth:users,ldap')->name('recording.download');

Expand Down
Loading
Loading