Jak poprawnie dodać token fałszowania żądań między witrynami (CSRF) za pomocą PHP

98

Próbuję trochę zabezpieczyć formularze w mojej witrynie. Jeden z formularzy korzysta z technologii AJAX, a drugi to prosty formularz „skontaktuj się z nami”. Próbuję dodać token CSRF. Problem, który mam, polega na tym, że token pojawia się tylko czasami w „wartości” HTML. Przez resztę czasu wartość jest pusta. Oto kod, którego używam w formularzu AJAX:

PHP:

if (!isset($_SESSION)) {
    session_start();
$_SESSION['formStarted'] = true;
}
if (!isset($_SESSION['token']))
{$token = md5(uniqid(rand(), TRUE));
$_SESSION['token'] = $token;

}

HTML

 <form>
//...
<input type="hidden" name="token" value="<?php echo $token; ?>" />
//...
</form>

Jakieś sugestie?

Rozpoznać
źródło
Po prostu ciekawy, do czego token_timesłuży?
zerkms
@zerkms, którego obecnie nie używam token_time. Zamierzałem ograniczyć czas, w którym token jest ważny, ale nie zaimplementowałem jeszcze w pełni kodu. Dla jasności usunąłem to z powyższego pytania.
Ken,
1
@Ken: aby użytkownik mógł otrzymać sprawę, gdy otworzył formularz, opublikował go i uzyskał nieprawidłowy token? (ponieważ zostało unieważnione)
zerkms
@zerkms: Dziękuję, ale jestem trochę zdezorientowany. Czy możesz podać mi przykład?
Ken
2
@Ken: jasne. Załóżmy, że token wygasa o godzinie 10:00. Teraz jest 09:59. Użytkownik otwiera formularz i otrzymuje token (który jest nadal ważny). Następnie użytkownik wypełnia formularz przez 2 minuty i wysyła go. Dopóki jest godzina 10:01 - token jest traktowany jako nieprawidłowy, przez co użytkownik otrzymuje błąd formularza.
zerkms

Odpowiedzi:

296

W przypadku kodu zabezpieczającego nie generuj swoich tokenów w ten sposób: $token = md5(uniqid(rand(), TRUE));

Wypróbuj to:

Generowanie tokena CSRF

PHP 7

session_start();
if (empty($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['token'];

Uwaga: Jeden z projektów open source mojego pracodawcy to inicjatywa dotycząca backportów random_bytes()i random_int()projektów PHP 5. Jest licencjonowany przez MIT i dostępny w Github i Composer jako paragonie / random_compat .

PHP 5.3+ (lub z ext-mcrypt)

session_start();
if (empty($_SESSION['token'])) {
    if (function_exists('mcrypt_create_iv')) {
        $_SESSION['token'] = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
    } else {
        $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(32));
    }
}
$token = $_SESSION['token'];

Weryfikacja tokena CSRF

Nie tylko używaj ==lub nawet ===używaj hash_equals()(tylko PHP 5.6+, ale dostępne we wcześniejszych wersjach z biblioteką kompatybilną z hash ).

if (!empty($_POST['token'])) {
    if (hash_equals($_SESSION['token'], $_POST['token'])) {
         // Proceed to process the form data
    } else {
         // Log this as a warning and keep an eye on these attempts
    }
}

Idąc dalej z tokenami zależnymi od formy

Możesz dodatkowo ograniczyć tokeny, aby były dostępne tylko dla określonego formularza, używając hash_hmac() . HMAC to szczególna funkcja skrótu z kluczem, która jest bezpieczna w użyciu, nawet w przypadku słabszych funkcji skrótu (np. MD5). Jednak zamiast tego zalecam używanie rodziny funkcji skrótu SHA-2.

Najpierw wygeneruj drugi token do użycia jako klucz HMAC, a następnie użyj logiki takiej jak ta, aby go wyrenderować:

<input type="hidden" name="token" value="<?php
    echo hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
?>" />

A następnie używając kongruentnej operacji podczas weryfikacji tokena:

$calc = hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
if (hash_equals($calc, $_POST['token'])) {
    // Continue...
}

Tokenów wygenerowanych dla jednego formularza nie można ponownie wykorzystać w innym kontekście bez wiedzy $_SESSION['second_token']. Ważne jest, aby użyć oddzielnego tokena jako klucza HMAC niż ten, który właśnie upuścisz na stronie.

Bonus: podejście hybrydowe + integracja Twig

Każdy, kto korzysta z silnika tworzenia szablonów Twig, może skorzystać z uproszczonej strategii dualnej, dodając ten filtr do swojego środowiska Twig:

$twigEnv->addFunction(
    new \Twig_SimpleFunction(
        'form_token',
        function($lock_to = null) {
            if (empty($_SESSION['token'])) {
                $_SESSION['token'] = bin2hex(random_bytes(32));
            }
            if (empty($_SESSION['token2'])) {
                $_SESSION['token2'] = random_bytes(32);
            }
            if (empty($lock_to)) {
                return $_SESSION['token'];
            }
            return hash_hmac('sha256', $lock_to, $_SESSION['token2']);
        }
    )
);

Dzięki tej funkcji Twig możesz używać obu tokenów ogólnego przeznaczenia w następujący sposób:

<input type="hidden" name="token" value="{{ form_token() }}" />

Lub wariant zamknięty:

<input type="hidden" name="token" value="{{ form_token('/my_form.php') }}" />

Twig zajmuje się tylko renderowaniem szablonów; nadal musisz poprawnie zweryfikować tokeny. Moim zdaniem strategia Twig oferuje większą elastyczność i prostotę, przy jednoczesnym zachowaniu możliwości maksymalnego bezpieczeństwa.


Jednorazowe tokeny CSRF

Jeśli masz wymaganie dotyczące bezpieczeństwa, aby każdy token CSRF mógł być użyty dokładnie raz, najprostsza strategia generuje go ponownie po każdej pomyślnej weryfikacji. Jednak spowoduje to unieważnienie każdego poprzedniego tokena, który nie pasuje do osób, które przeglądają wiele kart jednocześnie.

Paragon Initiative Enterprises utrzymuje bibliotekę Anti-CSRF dla tych narożnych przypadków. Działa wyłącznie z tokenami jednorazowego użytku na formularz. Gdy wystarczająca liczba tokenów jest przechowywana w danych sesji (domyślna konfiguracja: 65535), najpierw wyłączy najstarsze niewykupione tokeny.

Scott Arciszewski
źródło
fajnie, ale jak zmienić $ token po przesłaniu formularza przez użytkownika? w twoim przypadku jeden token używany do sesji użytkownika.
Akam
1
Przyjrzyj się uważnie, w jaki sposób jest zaimplementowany github.com/paragonie/anti-csrf . Tokeny są jednorazowego użytku, ale przechowuje wiele.
Scott Arciszewski
@ScottArciszewski Co myślisz o wygenerowaniu skrótu wiadomości z identyfikatora sesji z sekretem, a następnie porównaniu otrzymanego skrótu tokenu CSRF z ponownym zahaszowaniem identyfikatora sesji z moim poprzednim sekretem? Mam nadzieję, że rozumiesz co mam na myśli.
MNR
1
Mam pytanie dotyczące weryfikacji tokena CSRF. Jeśli $ _POST ['token'] jest pusty, nie powinniśmy kontynuować, ponieważ to żądanie wiadomości zostało wysłane bez tokena, prawda?
Hiroki,
1
Ponieważ zostanie to powtórzone w formularzu HTML, a chcesz, aby był nieprzewidywalny, aby atakujący nie mogli go po prostu sfałszować. Naprawdę wdrażasz tutaj uwierzytelnianie typu wyzwanie-odpowiedź, a nie tylko „tak, ten formularz jest legalny”, ponieważ osoba atakująca może po prostu to sfałszować.
Scott Arciszewski
24

Ostrzeżenie bezpieczeństwa : md5(uniqid(rand(), TRUE))nie jest bezpiecznym sposobem generowania liczb losowych. Zapoznaj się z tą odpowiedzią, aby uzyskać więcej informacji i zapoznać się z rozwiązaniem wykorzystującym generator liczb losowych zabezpieczony kryptograficznie.

Wygląda na to, że potrzebujesz innego ze swoim if.

if (!isset($_SESSION['token'])) {
    $token = md5(uniqid(rand(), TRUE));
    $_SESSION['token'] = $token;
    $_SESSION['token_time'] = time();
}
else
{
    $token = $_SESSION['token'];
}
datasage
źródło
11
Uwaga: nie ufałbym md5(uniqid(rand(), TRUE));kontekstom bezpieczeństwa.
Scott Arciszewski
2

Zmienna $tokennie jest pobierana z sesji, gdy się tam znajduje

Dani
źródło