CSRF-Token für Backend-Links im Frontend

Quell-ID: GitHub Discussion #15

Use Case

Aus dem Frontend soll ein Link zum Bearbeiten eines YForm-Datensatzes im Backend erstellt werden. Dabei wird der CSRF-Token benötigt, jedoch unterscheiden sich die Tokens für Frontend und Backend.

Verwendete AddOns

  • REDAXO Core
  • YForm

Problemstellung

Der folgende Code generiert einen CSRF-Token, der im Backend nicht akzeptiert wird:

if (rex_backend_login::hasSession() && $newsDataId != "") {
    $table = rex_yform_manager_table::get('rex_news');
    $_csrf_key = $table->getCSRFKey();
    $_csrf_params = rex_csrf_token::factory($_csrf_key)->getUrlParams();
    $token = $_csrf_params['_csrf_token'];

    $edit = '<a href="' . rex_url::backendPage(
        'yform/manager/data_edit',
        [
            'table_name' => 'rex_news',
            'func' => 'edit',
            'data_id' => $newsDataId,
            '_csrf_token' => $token
        ]
    ) . '">News bearbeiten</a>';
}

Lösung

Die CSRF-Tokens für das Frontend und das Backend unterscheiden sich. Die Umgebung muss temporär umgeschaltet werden, damit die CSRF-Generierung im richtigen Kontext erfolgt.

<?php
if (rex_backend_login::hasSession() && $newsDataId != "") {
    // Umgebung temporär auf Backend umschalten
    rex::setProperty('redaxo', true);

    $table = rex_yform_manager_table::get('rex_news');
    $_csrf_key = $table->getCSRFKey();
    $_csrf_params = rex_csrf_token::factory($_csrf_key)->getUrlParams();
    $token = $_csrf_params['_csrf_token'];

    // Umgebung zurücksetzen
    rex::setProperty('redaxo', false);

    $edit = '<a class="btn btn-primary" href="' . rex_url::backendPage(
        'yform/manager/data_edit',
        [
            'table_name' => 'rex_news',
            'func' => 'edit',
            'data_id' => $newsDataId,
            '_csrf_token' => $token
        ]
    ) . '" target="_blank">
        <i class="fa fa-edit"></i> News bearbeiten
    </a>';
}

Wichtiger Hinweis

Bei der CLI-Umgebung gibt es kein $_REQUEST-Objekt. Falls der Code auch in CLI-Kontexten ausgeführt werden könnte, sollte dies geprüft werden:

<?php
if (PHP_SAPI !== 'cli' && rex_backend_login::hasSession() && $newsDataId != "") {
    // ... Token-Generierung
}

Besserer Ansatz

Als wiederverwendbare Helper-Funktion

<?php
class BackendEditLink
{
    /**
     * Erstellt einen Edit-Link für einen YForm-Datensatz
     */
    public static function create(string $tableName, int $dataId, string $linkText = 'Bearbeiten'): string
    {
        if (PHP_SAPI === 'cli' || !rex_backend_login::hasSession()) {
            return '';
        }

        // Umgebung temporär umschalten
        $wasBackend = rex::isBackend();
        rex::setProperty('redaxo', true);

        try {
            $table = rex_yform_manager_table::get($tableName);
            if (!$table) {
                return '';
            }

            $csrfKey = $table->getCSRFKey();
            $csrfParams = rex_csrf_token::factory($csrfKey)->getUrlParams();

            $url = rex_url::backendPage(
                'yform/manager/data_edit',
                [
                    'table_name' => $tableName,
                    'func' => 'edit',
                    'data_id' => $dataId,
                    '_csrf_token' => $csrfParams['_csrf_token']
                ]
            );

            return sprintf(
                '<a href="%s" target="_blank" class="backend-edit-link">%s</a>',
                rex_escape($url),
                rex_escape($linkText)
            );
        } finally {
            // Umgebung zurücksetzen
            rex::setProperty('redaxo', $wasBackend);
        }
    }
}

// Verwendung im Template/Modul
echo BackendEditLink::create('rex_news', $newsDataId, 'News bearbeiten');

Mit Berechtigungsprüfung

<?php
public static function create(string $tableName, int $dataId, string $linkText = 'Bearbeiten'): string
{
    $user = rex_backend_login::createUser();
    if (!$user) {
        return '';
    }

    // Berechtigung prüfen
    $table = rex_yform_manager_table::get($tableName);
    if (!$table || !$user->hasPerm('yform[' . $tableName . ']')) {
        return '';
    }

    // ... Rest der Logik
}

Diese Lösung ist sicherer und wiederverwendbar.