VYPR
Moderate severityNVD Advisory· Published Nov 17, 2021· Updated Aug 4, 2024

Cross-Site Request Forgery allowing sending of test emails and generation of node auto-deployment keys

CVE-2021-41273

Description

Pterodactyl is an open-source game server management panel built with PHP 7, React, and Go. Due to improperly configured CSRF protections on two routes, a malicious user could execute a CSRF-based attack against the following endpoints: Sending a test email and Generating a node auto-deployment token. At no point would any data be exposed to the malicious user, this would simply trigger email spam to an administrative user, or generate a single auto-deployment token unexpectedly. This token is not revealed to the malicious user, it is simply created unexpectedly in the system. This has been addressed in release 1.6.6. Users may optionally manually apply the fixes released in v1.6.6 to patch their own systems.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
pterodactyl/panelPackagist
< 1.6.61.6.6

Affected products

1

Patches

1
bf9cbe2c6d52

Add consistent CSRF token verification to API endpoints; address security concern with non-CSRF protected endpoints

https://github.com/pterodactyl/panelDane EverittNov 17, 2021via ghsa
7 files changed · +59 14
  • app/Http/Kernel.php+2 0 modified
    @@ -75,6 +75,7 @@ class Kernel extends HttpKernel
                 ApiSubstituteBindings::class,
                 'api..key:' . ApiKey::TYPE_APPLICATION,
                 AuthenticateApplicationUser::class,
    +            VerifyCsrfToken::class,
                 AuthenticateIPAccess::class,
             ],
             'client-api' => [
    @@ -85,6 +86,7 @@ class Kernel extends HttpKernel
                 SubstituteClientApiBindings::class,
                 'api..key:' . ApiKey::TYPE_ACCOUNT,
                 AuthenticateIPAccess::class,
    +            VerifyCsrfToken::class,
                 // This is perhaps a little backwards with the Client API, but logically you'd be unable
                 // to create/get an API key without first enabling 2FA on the account, so I suppose in the
                 // end it makes sense.
    
  • app/Http/Middleware/Api/AuthenticateKey.php+2 1 modified
    @@ -8,6 +8,7 @@
     use Pterodactyl\Models\User;
     use Pterodactyl\Models\ApiKey;
     use Illuminate\Auth\AuthManager;
    +use Illuminate\Support\Facades\Session;
     use Illuminate\Contracts\Encryption\Encrypter;
     use Symfony\Component\HttpKernel\Exception\HttpException;
     use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
    @@ -55,7 +56,7 @@ public function __construct(ApiKeyRepositoryInterface $repository, AuthManager $
         public function handle(Request $request, Closure $next, int $keyType)
         {
             if (is_null($request->bearerToken()) && is_null($request->user())) {
    -            throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
    +            throw new HttpException(401, 'A bearer token or valid user session cookie must be provided to access this endpoint.', null, ['WWW-Authenticate' => 'Bearer']);
             }
     
             // This is a request coming through using cookies, we have an authenticated user
    
  • app/Http/Middleware/VerifyCsrfToken.php+34 7 modified
    @@ -2,18 +2,45 @@
     
     namespace Pterodactyl\Http\Middleware;
     
    +use Closure;
    +use Pterodactyl\Models\ApiKey;
     use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
     
     class VerifyCsrfToken extends BaseVerifier
     {
         /**
    -     * The URIs that should be excluded from CSRF verification.
    +     * The URIs that should be excluded from CSRF verification. These are
    +     * never hit by the front-end, and require specific token validation
    +     * to work.
          *
    -     * @var array
    +     * @var string[]
          */
    -    protected $except = [
    -        'remote/*',
    -        'daemon/*',
    -        'api/*',
    -    ];
    +    protected $except = ['remote/*', 'daemon/*'];
    +
    +    /**
    +     * Manually apply CSRF protection to routes depending on the authentication
    +     * mechanism being used. If the API request is using an API key that exists
    +     * in the database we can safely ignore CSRF protections, since that would be
    +     * a manually initiated request by a user or server.
    +     *
    +     * All other requests should go through the standard CSRF protections that
    +     * Laravel affords us. This code will be removed in v2 since we have switched
    +     * to using Sanctum for the API endpoints, which handles that for us automatically.
    +     *
    +     * @param \Illuminate\Http\Request $request
    +     * @param \Closure $next
    +     * @return mixed
    +     *
    +     * @throws \Illuminate\Session\TokenMismatchException
    +     */
    +    public function handle($request, Closure $next)
    +    {
    +        $key = $request->attributes->get('api_key');
    +
    +        if ($key instanceof ApiKey && $key->exists) {
    +            return $next($request);
    +        }
    +
    +        return parent::handle($request, $next);
    +    }
     }
    
  • resources/scripts/api/http.ts+12 1 modified
    @@ -7,10 +7,21 @@ const http: AxiosInstance = axios.create({
             'X-Requested-With': 'XMLHttpRequest',
             Accept: 'application/json',
             'Content-Type': 'application/json',
    -        'X-CSRF-Token': (window as any).X_CSRF_TOKEN as string || '',
         },
     });
     
    +http.interceptors.request.use(req => {
    +    const cookies = document.cookie.split(';').reduce((obj, val) => {
    +        const [ key, value ] = val.trim().split('=').map(decodeURIComponent);
    +
    +        return { ...obj, [key]: value };
    +    }, {} as Record<string, string>);
    +
    +    req.headers['X-XSRF-TOKEN'] = cookies['XSRF-TOKEN'] || 'nil';
    +
    +    return req;
    +});
    +
     http.interceptors.request.use(req => {
         if (!req.url?.endsWith('/resources') && (req.url?.indexOf('_debugbar') || -1) < 0) {
             store.getActions().progress.startContinuous();
    
  • resources/views/admin/nodes/view/configuration.blade.php+5 1 modified
    @@ -70,7 +70,11 @@
         @parent
         <script>
         $('#configTokenBtn').on('click', function (event) {
    -        $.getJSON('{{ route('admin.nodes.view.configuration.token', $node->id) }}').done(function (data) {
    +        $.ajax({
    +            method: 'POST',
    +            url: '{{ route('admin.nodes.view.configuration.token', $node->id) }}',
    +            headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
    +        }).done(function (data) {
                 swal({
                     type: 'success',
                     title: 'Token created.',
    
  • resources/views/admin/settings/mail.blade.php+2 2 modified
    @@ -145,9 +145,9 @@ function testSettings() {
                     showLoaderOnConfirm: true
                 }, function () {
                     $.ajax({
    -                    method: 'GET',
    +                    method: 'POST',
                         url: '/admin/settings/mail/test',
    -                    headers: { 'X-CSRF-Token': $('input[name="_token"]').val() }
    +                    headers: { 'X-CSRF-TOKEN': $('input[name="_token"]').val() }
                     }).fail(function (jqXHR) {
                         showErrorDialog(jqXHR, 'test');
                     }).done(function () {
    
  • routes/admin.php+2 2 modified
    @@ -66,8 +66,8 @@
     Route::group(['prefix' => 'settings'], function () {
         Route::get('/', 'Settings\IndexController@index')->name('admin.settings');
         Route::get('/mail', 'Settings\MailController@index')->name('admin.settings.mail');
    -    Route::get('/mail/test', 'Settings\MailController@test')->name('admin.settings.mail.test');
         Route::get('/advanced', 'Settings\AdvancedController@index')->name('admin.settings.advanced');
    +    Route::post('/mail/test', 'Settings\MailController@test')->name('admin.settings.mail.test');
     
         Route::patch('/', 'Settings\IndexController@update');
         Route::patch('/mail', 'Settings\MailController@update');
    @@ -153,12 +153,12 @@
         Route::get('/view/{node}/allocation', 'Nodes\NodeViewController@allocations')->name('admin.nodes.view.allocation');
         Route::get('/view/{node}/servers', 'Nodes\NodeViewController@servers')->name('admin.nodes.view.servers');
         Route::get('/view/{node}/system-information', 'Nodes\SystemInformationController');
    -    Route::get('/view/{node}/settings/token', 'NodeAutoDeployController')->name('admin.nodes.view.configuration.token');
     
         Route::post('/new', 'NodesController@store');
         Route::post('/view/{node}/allocation', 'NodesController@createAllocation');
         Route::post('/view/{node}/allocation/remove', 'NodesController@allocationRemoveBlock')->name('admin.nodes.view.allocation.removeBlock');
         Route::post('/view/{node}/allocation/alias', 'NodesController@allocationSetAlias')->name('admin.nodes.view.allocation.setAlias');
    +    Route::post('/view/{node}/settings/token', 'NodeAutoDeployController')->name('admin.nodes.view.configuration.token');
     
         Route::patch('/view/{node}/settings', 'NodesController@updateSettings');
     
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.