Perlindungan Pemalsuan Permintaan Lintas Situs (Anti-CSRF) di PHP

oleh Vincy. Terakhir diubah pada 2 Maret 2021.

Serangan Cross-Site Request Forgery (CSRF) adalah penyalahgunaan keamanan umum yang terjadi di seluruh dunia web. Melindungi server dari serangan ini adalah mekanisme perlindungan tingkat pertama dalam melindungi situs web Anda.

Pengguna jahat melalui internet digunakan untuk mengkloning permintaan untuk menyerang server yang rentan. Kloning ini dapat terjadi dengan menyematkan tautan situs berbahaya ke halaman web pengguna.

Implementasi anti CSRF mengurangi kerentanan situs web. Dengan perlindungan ini, situs web menolak akses jahat yang mengirimkan permintaan tanpa atau salah token CSRF.

Diagram berikut menunjukkan validasi permintaan pengguna terhadap serangan CSRF. Jika pengguna asli memposting formulir dengan token yang tepat, server akan memproses permintaan tersebut. Itu menolak sebaliknya, dengan tidak adanya parameter token CSRF.

Penanganan Formulir dengan Perlindungan Anti CSRF

Kita akan melihat contoh kode Formulir kontak PHP dengan perlindungan CSRF dalam tutorial ini. Dengan perlindungan ini, ia memastikan keaslian permintaan sebelum memprosesnya.

Saya telah membuat layanan di PHP untuk menangani validasi keamanan terhadap serangan CSRF. Server akan menolak permintaan pengguna yang tanpa token atau token yang tidak valid.

Jika Anda ingin memiliki formulir kontak dengan perlindungan CSRF dan fitur keamanan lainnya, dapatkan Iris.

Apa yang ada di dalam?

  1. Tentang contoh ini
  2. Hasilkan token CSRF dan buat sesi PHP
  3. Render formulir kontak dengan token CSRF
  4. Validasi Anti Pemalsuan Permintaan Lintas Situs (CSRF) di PHP
  5. Layanan keamanan untuk menghasilkan, memasukkan, memvalidasi token CSRF
  6. Output: Respons validasi CSRF dari server

Tentang contoh ini

Kode ini mengimplementasikan perlindungan Anti CSRF dalam formulir kontak PHP. Itu membuat formulir kontak. Penangan pos formulir ini memvalidasi permintaan pengguna terhadap serangan CSRF.

Saat memuat halaman arahan, skrip PHP menghasilkan token CSRF. Footer formulir akan memiliki token ini sebagai bidang tersembunyi. Juga, ia mengelola token dalam sesi PHP.

Saat memposting bidang formulir, kode PHP akan memeriksa parameter token CSRF. Jika ditemukan, maka itu akan memvalidasi token dari sesi.

Jika pengguna mengirimkan permintaan tanpa token CSRF, maka server akan menolak permintaan tersebut. Juga, jika token tidak cocok dengan token dari sesi, maka server akan menolak permintaan tersebut.

Pada validasi token CSRF yang berhasil, itu akan mengirim email kontak ke alamat target. Diagram berikut menunjukkan struktur file dari contoh ini.

Struktur File Kode Token Anti CSRF

Hasilkan token CSRF dan buat sesi PHP

Pada halaman arahan, skrip footer formulir memanggil SecurityService. Ini adalah kelas PHP untuk menghasilkan token CSRF.

Itu menulis token ke dalam sesi PHP untuk referensi di masa mendatang. Ini akan membantu pada saat memproses validasi CSRF setelah posting formulir.

Form footer adalah file kerangka kerja yang memuat token yang dihasilkan ke dalam bidang tersembunyi.

Cuplikan kode di bawah ini berasal dari SecurityService.php untuk menghasilkan token CSRF. Kode lengkap dari kelas layanan ditampilkan di bagian selanjutnya dari artikel ini.

SecurityService.php (kode untuk menghasilkan token CSRF)

    /**
     * Generate, store, and return the CSRF token
     *
     * @return string[]
     */
    public function getCSRFToken()
    {
        if (empty($this->session[$this->sessionTokenLabel])) {
            $this->session[$this->sessionTokenLabel] = bin2hex(openssl_random_pseudo_bytes(32));
        }

        if ($this->hmac_ip !== false) {
            $token = $this->hMacWithIp($this->session[$this->sessionTokenLabel]);
        } else {
            $token = $this->session[$this->sessionTokenLabel];
        }
        return $token;
    }

Ini adalah HTML formulir kontak dengan bidang biasa nama, email, subjek dan pesan. Ditambah lagi ia memiliki bidang tersembunyi csrf-token dengan token yang dihasilkan.

Tindakan kirim memproses validasi formulir jQuery sebelum memposting parameter ke PHP.

Skrip validasi sisi klien menangani validasi dasar saat pengiriman. Ini menerapkan cek tidak kosong pada setiap bidang.

index.php (Template HTML)

<html>
<head>
<title>CSRF Protection using PHP</title>
<link rel="stylesheet" type="text/css"
	href="https://phppot.com/php/cross-site-request-forgery-anti-csrf-protection-in-php/assets/css/phppot-style.css" />
<script src="vendor/jquery/jquery-3.2.1.min.js"></script>
<style>
.error-field {
	border: 1px solid #d96557;
}

.send-button {
	cursor: pointer;
	background: #3cb73c;
	border: #36a536 1px solid;
	color: #FFF;
	font-size: 1em;
	width: 100px;
}
</style>
</head>
<body>
	<div class="phppot-container">
		<h1>CSRF Protection using PHP</h1>
		<form name="frmContact" id="cnt-frm" class="phppot-form"
			frmContact"" method="post" action="" enctype="multipart/form-data"
			onsubmit="return validateContactForm()">

			<div class="phppot-row">
				<div class="label">
					Name <span id="userName-info" class="validation-message"></span>
				</div>
				<input type="text" class="phppot-input" name="userName"
					id="userName"
					value="<?php if(!empty($_POST['userName'])&& $type == 'error'){ echo $_POST['userName'];}?>" />
			</div>
			<div class="phppot-row">
				<div class="label">
					Email <span id="userEmail-info" class="validation-message"></span>
				</div>
				<input type="text" class="phppot-input" name="userEmail"
					id="userEmail"
					value="<?php if(!empty($_POST['userEmail'])&& $type == 'error'){ echo $_POST['userEmail'];}?>" />
			</div>
			<div class="phppot-row">
				<div class="label">
					Subject <span id="subject-info" class="validation-message"></span>
				</div>
				<input type="text" class="phppot-input" name="subject" id="subject"
					value="<?php if(!empty($_POST['subject'])&& $type == 'error'){ echo $_POST['subject'];}?>" />
			</div>
			<div class="phppot-row">
				<div class="label">
					Message <span id="userMessage-info" class="validation-message"></span>
				</div>
				<textarea name="content" id="content" class="phppot-input" cols="60"
					rows="6"><?php if(!empty($_POST['content'])&& $type == 'error'){ echo $_POST['content'];}?></textarea>
			</div>
			<div class="phppot-row">
				<input type="submit" name="send" class="send-button" value="Send" />
			</div>
			
			<?php require_once __DIR__ . '/view/framework/form-footer.php';?>
			
		</form>
		<?php if(!empty($message)) { ?>
		<div id="phppot-message" class="<?php  echo $type; ?>"><?php if(isset($message)){ ?>
				    <?php echo $message; }}?>
                    </div>
	</div>
	<script src="assets/js/validate.js"></script>
</body>
</html>

Ini adalah skrip form footer yang memicu service handler untuk menghasilkan token. Itu masukkanTokenTersembunyi() menulis kode HTML untuk memuat bidang token csrf ke dalam formulir.

view/framework/form-footer.php

<?php
require_once __DIR__ . '/../../lib/SecurityService.php';
$antiCSRF = new PhppotSecurityServicesecurityService();
$antiCSRF->insertHiddenToken();

assets/js/validation.js

function validateContactForm() {
	var valid = true;
	$("#userName").removeClass("error-field");
	$("#userEmail").removeClass("error-field");
	$("#subject").removeClass("error-field");
	$("#content").removeClass("error-field");

	$("#userName-info").html("").hide();
	$("#userEmail-info").html("").hide();
	$("#subject-info").html("").hide();
	$("#content-info").html("").hide();

	$(".validation-message").html("");
	$(".phppot-input").css('border', '#e0dfdf 1px solid');

	var userName = $("#userName").val();
	var userEmail = $("#userEmail").val();
	var subject = $("#subject").val();
	var content = $("#content").val();

	if (userName.trim() == "") {
		$("#userName-info").html("required.").css("color", "#ee0000").show();
		$("#userName").css('border', '#e66262 1px solid');
		$("#userName").addClass("error-field");

		valid = false;
	}
	if (userEmail.trim() == "") {
		$("#userEmail-info").html("required.").css("color", "#ee0000").show();
		$("#userEmail").css('border', '#e66262 1px solid');
		$("#userEmail").addClass("error-field");

		valid = false;
	}
	if (!userEmail.match(/^([w-.]+@([w-]+.)+[w-]{2,4})?$/)) {
		$("#userEmail-info").html("invalid email address.").css("color",
				"#ee0000").show();

		$("#userEmail").css('border', '#e66262 1px solid');
		$("#userEmail").addClass("error-field");

		valid = false;
	}

	if (subject == "") {
		$("#subject-info").html("required.").css("color", "#ee0000").show();
		$("#subject").css('border', '#e66262 1px solid');
		$("#subject").addClass("error-field");

		valid = false;
	}
	if (content == "") {
		$("#userMessage-info").html("required.").css("color", "#ee0000").show();
		$("#content").css('border', '#e66262 1px solid');
		$("#content").addClass("error-field");

		valid = false;
	}

	if (valid == false) {
		$('.error-field').first().focus();
		valid = false;
	}
	return valid;
}

Validasi Anti Pemalsuan Permintaan Lintas Situs (CSRF) di PHP

Saat mengirimkan formulir kontak yang disematkan token, tindakan formulir menjalankan skrip berikut.

Fungsi Validasi() SecuritySercive membandingkan token yang diposting dengan token yang disimpan dalam sesi.

Jika ditemukan kecocokan, maka akan dilanjutkan dengan mengirimkan email kontak. Jika tidak, itu akan mengakui pengguna dengan pesan kesalahan.

index.php (validasi PHP CSRF dan penanganan formulir)

<?php
use PhppotMailService;

session_start();
if (! empty($_POST['send'])) {
    require_once __DIR__ . '/lib/SecurityService.php';
    $antiCSRF = new PhppotSecurityServicesecurityService();
    $csrfResponse = $antiCSRF->validate();
    if (! empty($csrfResponse)) {
        require_once __DIR__ . '/lib/MailService.php'; 
        $mailService = new MailService();
        $response = $mailService->sendContactMail($_POST);
        if (! empty($response)) {
            $message = "Hi, we have received your message. Thank you.";
            $type = "success";
        } else {
            $message = "Unable to send email.";
            $type = "error";
        }
    } else {
        $message = "Security alert: Unable to process your request.";
        $type = "error";
    }
}

?>

Layanan keamanan untuk menghasilkan, memasukkan, memvalidasi token CSRF

Kelas layanan yang dibuat dalam PHP ini menyertakan metode untuk memproses operasi terkait perlindungan CSRF.

Ini mendefinisikan properti kelas untuk mengatur nama bidang token formulir, indeks sesi.

Ini memiliki metode untuk menghasilkan token dan menulisnya ke dalam HTML dan sesi PHP.

Ini menggunakan mitigasi XSS saat menulis footer formulir dengan token.

Juga, ia memiliki opsi untuk mengecualikan beberapa URL dari proses validasi. URL yang dikecualikan melewati proses validasi CSRF.

Kode mendapatkan URL permintaan saat ini dari variabel PHP SERVER. Kemudian, ia membandingkannya dengan larik URL yang dikecualikan untuk melewati validasi.

lib/SecurityService.php

<?php
/**
 * Copyright (C) Phppot
 *
 * Distributed under 'The MIT License (MIT)'
 * In essense, you can do commercial use, modify, distribute and private use.
 * Though not mandatory, you are requested to attribute Phppot URL in your code or website.
 */
namespace PhppotSecurityService;

/**
 * Library class used for CSRF protection.
 * CSRF is abbreviation for Cross Site Request Forgery.
 * Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a
 * web application in which they're currently authenticated. Defn. by OWASP.
 *
 * User session based token is generated and hashed with their IP address.
 * There are types of operations using which the DDL are executed.
 * Submits using general HTML form and submits using AJAX.
 * We are inserting a CSRF token inside the form and it is validated against the token present in the session.
 * This ensures that the CSRF attacks are prevented.
 *
 * If you are customizing the application and creating a new form,
 * you should ensure that the CSRF prevention is in place. form-footer.php
 * is the file that should be included where the token is to be echoed.
 * After echo the validation of the token happens in controller and it is
 * the common entry point for all calls. So there is no need to do any separate code for
 * CSRF validation with respect to each functionality.
 *
 * The CSRF token is written as a hidden input type inside the html form tag with a label $formTokenLabel.
 *
 * @author Vincy
 * @version 3.5 - IP Address tracking removed as it is good for GDPR compliance.
 *         
 */
class securityService
{

    private $formTokenLabel="eg-csrf-token-label";

    private $sessionTokenLabel="EG_CSRF_TOKEN_SESS_IDX";

    private $post = [];

    private $session = [];

    private $server = [];

    private $excludeUrl = [];

    private $hashAlgo = 'sha256';

    private $hmac_ip = true;

    private $hmacData="ABCeNBHVe3kmAqvU2s7yyuJSF2gpxKLC";

    /**
     * NULL is not a valid array type
     *
     * @param array $post
     * @param array $session
     * @param array $server
     * @throws Error
     */
    public function __construct($excludeUrl = null, &$post = null, &$session = null, &$server = null)
    {
        if (! is_null($excludeUrl)) {
            $this->excludeUrl = $excludeUrl;
        }
        if (! is_null($post)) {
            $this->post = & $post;
        } else {
            $this->post = & $_POST;
        }

        if (! is_null($server)) {
            $this->server = & $server;
        } else {
            $this->server = & $_SERVER;
        }

        if (! is_null($session)) {
            $this->session = & $session;
        } elseif (! is_null($_SESSION) && isset($_SESSION)) {
            $this->session = & $_SESSION;
        } else {
            throw new Error('No session available for persistence');
        }
    }

    /**
     * Insert a CSRF token to a form
     *
     * @param string $lockTo
     *            This CSRF token is only valid for this HTTP request endpoint
     * @param bool $echo
     *            if true, echo instead of returning
     * @return string
     */
    public function insertHiddenToken()
    {
        $csrfToken = $this->getCSRFToken();

        echo "<!--n--><input type="hidden"" . " name="" . $this->xssafe($this->formTokenLabel) . """ . " value="" . $this->xssafe($csrfToken) . """ . " />";
    }

    // xss mitigation functions
    public function xssafe($data, $encoding = 'UTF-8')
    {
        return htmlspecialchars($data, ENT_QUOTES | ENT_HTML401, $encoding);
    }

    /**
     * Generate, store, and return the CSRF token
     *
     * @return string[]
     */
    public function getCSRFToken()
    {
        if (empty($this->session[$this->sessionTokenLabel])) {
            $this->session[$this->sessionTokenLabel] = bin2hex(openssl_random_pseudo_bytes(32));
        }

        if ($this->hmac_ip !== false) {
            $token = $this->hMacWithIp($this->session[$this->sessionTokenLabel]);
        } else {
            $token = $this->session[$this->sessionTokenLabel];
        }
        return $token;
    }

    /**
     * hashing with IP Address removed for GDPR compliance easiness
     * and hmacdata is used.
     *
     * @param string $token
     * @return string hashed data
     */
    private function hMacWithIp($token)
    {
        $hashHmac = hash_hmac($this->hashAlgo, $this->hmacData, $token);
        return $hashHmac;
    }

    /**
     * returns the current request URL
     *
     * @return string
     */
    private function getCurrentRequestUrl()
    {
        $protocol = "http";
        if (isset($this->server['HTTPS'])) {
            $protocol = "https";
        }
        $currentUrl = $protocol . "://" . $this->server['HTTP_HOST'] . $this->server['REQUEST_URI'];
        return $currentUrl;
    }

    /**
     * core function that validates for the CSRF attempt.
     *
     * @throws Exception
     */
    public function validate()
    {
        $currentUrl = $this->getCurrentRequestUrl();
        if (! in_array($currentUrl, $this->excludeUrl)) {
            if (! empty($this->post)) {
                $isAntiCSRF = $this->validateRequest();
                if (! $isAntiCSRF) {
                    // CSRF attack attempt
                    // CSRF attempt is detected. Need not reveal that information
                    // to the attacker, so just failing without info.
                    // Error code 1837 stands for CSRF attempt and this is for
                    // our identification purposes.
                    return false;
                }
                return true;
            }
        }
    }

    /**
     * the actual validation of CSRF happens here and returns boolean
     *
     * @return boolean
     */
    public function isValidRequest()
    {
        $isValid = false;
        $currentUrl = $this->getCurrentRequestUrl();
        if (! in_array($currentUrl, $this->excludeUrl)) {
            if (! empty($this->post)) {
                $isValid = $this->validateRequest();
            }
        }
        return $isValid;
    }

    /**
     * Validate a request based on session
     *
     * @return bool
     */
    public function validateRequest()
    {
        if (! isset($this->session[$this->sessionTokenLabel])) {
            // CSRF Token not found
            return false;
        }

        if (! empty($this->post[$this->formTokenLabel])) {
            // Let's pull the POST data
            $token = $this->post[$this->formTokenLabel];
        } else {
            return false;
        }

        if (! is_string($token)) {
            return false;
        }

        // Grab the stored token
        if ($this->hmac_ip !== false) {
            $expected = $this->hMacWithIp($this->session[$this->sessionTokenLabel]);
        } else {
            $expected = $this->session[$this->sessionTokenLabel];
        }

        return hash_equals($token, $expected);
    }

    /**
     * removes the token from the session
     */
    public function unsetToken()
    {
        if (! empty($this->session[$this->sessionTokenLabel])) {
            unset($this->session[$this->sessionTokenLabel]);
        }
    }
}

MailService.php ini menggunakan fungsi PHP core mail() untuk mengirim email kontak. Anda dapat menggantinya dengan SMTP melalui skrip pengiriman email.

lib/MailService.php

<?php
namespace Phppot;

class MailService
{

    function sendContactMail($postValues)
    {
        $name = $postValues["userName"];
        $email = $postValues["userEmail"];
        $subject = $postValues["subject"];
        $content = $postValues["content"];

        $toEmail = "ADMIN EMAIL";
        $mailHeaders = "From: " . $name . "(" . $email . ")rn";
        $response = mail($toEmail, $subject, $content, $mailHeaders);

        return $response;
    }
}

Output: Respons validasi CSRF dari server

Tangkapan layar menunjukkan formulir kontak biasa di bawah ini. Kami telah melihat output ini di banyak tutorial formulir kontak sebelumnya.

Di bawah antarmuka formulir, tangkapan layar ini menunjukkan pesan peringatan keamanan berwarna merah. Ini mengakui pengguna yang mengirim permintaan dengan token yang salah atau kosong.

Output Perlindungan Anti CSRF

Kesimpulan

Jadi kami telah menerapkan perlindungan anti CSRF dalam formulir kontak PHP.

Semoga kode contoh bermanfaat dan Anda mendapatkan proses implementasi yang kami diskusikan di sini.

Kami telah membuat kelas SecurityService di PHP untuk menangani perlindungan CSRF. Ini dapat digunakan kembali untuk beberapa aplikasi di mana pun Anda perlu mengaktifkan perlindungan CSRF.

Kode PHP yang mengembalikan pesan respons mengakui pengguna dengan benar.

Unduh

Kembali ke Atas


Source link