post_ico4

(Nie)bezpieczny kod – CSRF

CSRF jest metodą ataku umożliwiającą na wykonanie akcji przeznaczonej dla zalogowanego użytkownika bez konieczności logowania się do systemu przez atakującego.

W artykule wykorzystuję kod napisany na potrzeby ataku XSS, a więc do pełnego zrozumienia niezbędne jest przeczytanie poprzedniego wpisu z serii (Nie)bezpieczny kod.

Trochę teorii

Cross-site request forgery (w skrócie CSRF lub XSRF) ma na celu skłonić zalogowanego użytkownika serwisu internetowego do tego, aby uruchomił on odnośnik, którego otwarcie wykona akcję, do której atakujący nie miałby dostępu. Podsyłając spreparowany odnośnik zalogowanemu administratorowi można na przykład „za jego pośrednictwem” usunąć informacje z bazy danych.

Przykładowy atak CSRF

Żeby umożliwić atak potrzebujemy nieco zmodyfikować plik logowanie.php:

<?php
$dbh = new PDO('mysql:host=****;dbname=****', '****', '****');
session_start();
if (!isset($_SESSION['logged'])) {
    $_SESSION['logged'] = false;
}
if (isset($_GET['action'])) {
    if ($_GET['action'] == 'logout') {
        $_SESSION['logged'] = false;
        session_destroy();
    }
}
if ($_SESSION['logged'] === false && isset($_POST['login']) && isset($_POST['password'])) {
    if ($_POST['login'] == 'demo' && $_POST['password'] == 'demo'
    ) {
        $_SESSION['logged'] = true;
    } else {
        echo '<p>Złe hasło!!!</p>';
        $_SESSION['logged'] = false;
    }
}
?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
    <meta charset="utf-8">
    <title>CSRF - logowanie</title>
</head>
<body>
<?php if ($_SESSION['logged']):
    if (isset($_GET['action'])) {
        if ($_GET['action'] == 'delete' && isset($_GET['id'])) {
            $posts = $dbh->prepare("DELETE from post WHERE id=" . (int)$_GET['id']);
            $posts->execute();
        }
    }
    ?>
    Jesteś zalogowany! <a href="?action=logout">Wyloguj mnie!</a>
<?php else: ?>
    <form method="post" action="">
        Login: <input type="text" name="login"/><br/>
        Hasło: <input type="password" name="password"/><br/><br/>
        <input type="submit" value="Zaloguj"/>
    </form>
<?php endif; ?>
</body>
</html>

W powyższym kodzie dodałem możliwość usuwania postu dla zalogowanego administratora.

Pora teraz na właściwą część ataku, a więc dodanie spreparowanego obrazka do wpisu :).

Przykładowy atak CSRF

W miejsce adresu do obrazka wpisuję url wykonujący akcję przeznaczoną tylko dla zalogowanego użytkownika – w tym wypadku usuwam post o ID równyym 1. Po dodaniu kodu wystarczy teraz tylko poczekać aż zalogowany administrator otworzy stronę…

Jak się zabezpieczyć przed CSRF?

Jednym z kanałów wszczepienia złośliwego kodu jest luka zezwalająca na XSS, a więc warto w pierwszej kolejności zadbać o zabezpieczenie się przed XSS.

Dodatkowym zabezpieczeniem jest zapisanie unikalnego tokena w sesji i doklejanie go do każdego linka prowadzącego do akcji przeznaczonych dla zalogowanych użytkowników. Przed wykonaniem akcji konieczne jest sprawdzenie, czy token z tablicy GET jest taki sam, jak zapisany w sesji.

Poprawiony plik logowanie.php wygląda następująco:

<?php
$dbh = new PDO('mysql:host=****;dbname=****', '****', '****');
session_start();
if (!isset($_SESSION['logged'])) {
    $_SESSION['logged'] = false;
}
if (isset($_GET['action'])) {
    if ($_GET['action'] == 'logout') {
        $_SESSION['logged'] = false;
        session_destroy();
    }
}
if ($_SESSION['logged'] === false && isset($_POST['login']) && isset($_POST['password'])) {
    if ($_POST['login'] == 'demo' && $_POST['password'] == 'demo'
    ) {
        $_SESSION['logged'] = true;
        $_SESSION['token'] = uniqid();
    } else {
        echo '<p>Złe hasło!!!</p>';
        $_SESSION['logged'] = false;
    }
}
?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
    <meta charset="utf-8">
    <title>CSRF - logowanie</title>
</head>
<body>
<?php if ($_SESSION['logged']):
    if (isset($_GET['action'])) {
        if ($_GET['action'] == 'delete' && isset($_GET['id'])  && isset($_GET['token']) && $_GET['token'] == $_SESSION['token']) {
            $posts = $dbh->prepare("DELETE from post WHERE id=" . (int)$_GET['id']);
            $posts->execute();
        }
    }
    ?>
    Jesteś zalogowany! <a href="?action=logout">Wyloguj mnie!</a>
    <h4>Posty</h4>
    <ul>
        <?php
        $posts=$dbh->prepare("SELECT * from post ORDER by id DESC");
        $posts->execute();
        foreach($posts->fetchAll() as $post) {
            echo '<li><a href="?action-=delete&id='.$post['id'].'&token='.$_SESSION['token'].'">Usuń wpis '.$post['id'].'</a></li>';
        }
        ?>
    </ul>
<?php else: ?>
    <form method="post" action="">
        Login: <input type="text" name="login"/><br/>
        Hasło: <input type="password" name="password"/><br/><br/>
        <input type="submit" value="Zaloguj"/>
    </form>
<?php endif; ?>
</body>
</html>

W moim przykładzie link do usuwania wpisów wygląda mniej więcej tak:

logowanie.php?action-=delete&id=2&token=54ca4c85098f9

Poza tym bezpieczniej jest przekazywać dane metodą POST. Manipulacja nimi będzie znacznie trudniejsza.

Uwaga: informacje przedstawione w artykule służą tylko celom edukacyjnym. ZABRONIONE jest wykorzystywanie informacji przedstawionych w artykule do celów niezgodnych z prawem. Autor nie ponosi odpowiedzialności za ewentualne szkody.

Co sądzisz o wpisie?
BeżnadziejnySłabyŚredniDobryBardzo dobry (1 głosów, średnia ocen: 4,00 z 5)
Loading...
  • WooDzu

    Witam. Metoda zabezpieczenia przez crsf nie jest odfiltrowanie html z tesci post ale zastosowanie specjalnego tokena sesji – unikalnego dla kazdej sesji aby spreparowany obrazek/link/iframe nic nie mogl zdzialac. Pozdrawiam

    • redeemer

      Dokładnie. CSRF i XSS to dwa zupełnie niepowiązane ze sobą błędy.

      • Racja, błędnie założyłem, że spreparowany link może być podany tylko w obrębie serwisu.

        Pośpiech przy tworzeniu wpisów niewskazany. Postaram się unikać w przyszłości tego typu błędów :)

        Dzięki za zwrócenie uwagi.

  • Dwa drobne szczegóły: dobrze byłoby zaznaczyć, że do wywołania adresu nie trzeba wcale XSSa, czasem wystarczy np. tag [img] z bbcode (w przypadku XSSa można zwyczajnie pobrać za pomocą AJAXa kod strony na której obecny jest token po czym wywołać kolejne zapytanie które usunie post, więc tak naprawdę w podanym przykładzie wcale się przed niczym nie zabezpieczyliśmy). No i przydałby się jeszcze przykład jak wyglądają linki do usuwania postów przed i po dodaniu zabezpieczenia (czyli dodanie &token= do URL) – niby oczywiste ale dobrze byłoby mieć to czarno na białym.

    • Dlatego, poza zabezpieczeniem przed XSS, wspominam w artykule o tokenie i korzystaniu z POST a nie GET.. W wolnej chwili dodam przykład URL z tokenem.

  • Tak jak mówią poprzednicy. Najbardziej obrazujący przykład to „usunWpis.php?id=13”. Wystarczy że potencjalny cracker wie o podatności CSRF, użyje socjotechniki i skróci odnośnik do tej akcji za pomocą takich serwisów jak TinyURL. Nakłaniając użytkownika do kliknięcia (obojętnie czy to na czacie, komunikatorze, facebooku) dąży do wykonania tej akcji. Specyfikacja nawet podaje kiedy używać powinno się GET a kiedy POST w swoich seriwsach: http://www.w3.org/2001/tag/doc/whenToUseGet.html#checklist punkt np: 1.3. Bardzo fajny blog. Trafiliśmy tutaj szukając ciekawych wzorców projektowych :-)

    • Zgadza się. Tego typu dane lepiej przekazywać POSTem. Na końcu artykułu napisałem „Poza tym bezpieczniej jest przekazywać dane metodą POST. Manipulacja nimi będzie znacznie trudniejsza.” :)

      • Tieman

        Czy to oznacza że przy metodzie POST w ogóle nie da się zastosować CSRF? (ponieważ nie wiem czy to dobrze zrozumiałem)

        • Można ewentualnie kombinować z AJAXem i przesyłać taką formą tablicę POST, ale szczerze mówiąc nie sprawdzałem takiej formy ataku. Aczkolwiek wydaje mi się, że złośliwy kod musiałby być umieszczony w tej samej domenie ze względu na identyfikator sesji trzymany w cookie.

  • Pascal

    A co z definiowaniem stałej w index.php i sprawdzanie w każdym pliku akcji czy stała jest zdefiniowana? Do tej pory używałem zawsze takiej metody zabezpieczania się przed wykonywaniem akcji jako administrator aplikacji przez osoby z zewnątrz. Co o tym sądzicie?

    • Moim zdaniem przed CSRF nie zabezpieczy. Przecież otwierając taki spreparowany link przejdziesz taką samą drogę przez pliki tak jakbyś odpalił link w przeglądarce.

      Z tym że bez fragmentów kodu nie jestem pewien czy dobrze rozumiem twoją koncepcję.