Search code examples
phpcsrfcsrf-token

Why does my CSRF token validation fail when deleting multiple entries in PHP?


I'm encountering an issue with CSRF token validation in my PHP application. Specifically, when I try to delete multiple entries using the same CSRF token, I receive a "CSRF attack detected" error message. I believe this issue is related to the CSRF token being used multiple times within the same session without being regenerated.

    <!-- Modal -->
    <div class="modal fade" id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header bg-info">
            <h1 class="modal-title fs-5" id="staticBackdropLabel">Are you sure to delete the entry?</h1>
            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
          </div>
          <div class="modal-body">
            <form method="POST" action="">
              <div class="mb-3">
                <input type="hidden" name="auth_token" value="<?php echo htmlspecialchars(strip_tags($antiXss->xss_clean($_SESSION['auth_token']))); ?>" required>
                <input type="hidden" name="target_id" class="form-control" id="recipient-name">
              </div>
          </div>
          <div class="modal-footer">
            <button type="submit" name="delete_entry" class="btn btn-danger"><i class="fa-solid fa-trash"></i> delete </button>
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><i class="fa-solid fa-ban"></i> cancel</button>
          </div>
        </div>
        </form>
      </div>
    </div>
<?php
session_start();

// Generate a new CSRF token if one doesn't exist
if (!isset($_SESSION['auth_token'])) {
    $_SESSION['auth_token'] = bin2hex(random_bytes(20));
}

// Handle delete request
if (isset($_POST["delete_entry"])) {
    // Validate CSRF token
    if (!isset($_POST['auth_token']) || !isset($_SESSION['auth_token']) || $_SESSION['auth_token'] !== $_POST['auth_token'] || !isset($_POST['target_id'])) {
        // If the CSRF token is invalid or missing
        echo '<div class="alert alert-danger alert-dismissible fade show" role="alert">
                <i class="fas fa-bug"></i>
                CSRF attack detected. Please reload the page and try again.<button type="button" class="btn-close"
                data-bs-dismiss="alert" aria-label="Close"></button>
              </div>';
        
        // Regenerate CSRF token to prevent any further attempts with the same token
        $_SESSION['auth_token'] = bin2hex(random_bytes(20));
        exit();
    } else {
        // Proceed with delete operation if the CSRF token is valid
        $target_id = htmlspecialchars(strip_tags($antiXss->xss_clean($_POST['target_id'])));
        $stmt = $con->prepare("SELECT * FROM jentress_erp WHERE jnum = ? and company=?");
        $stmt->bind_param("ss", $target_id, $_SESSION["company"]);

        $stmt->execute();
        $result = $stmt->get_result();

        if ($result->num_rows > 0) {
            while ($row = $result->fetch_assoc()) {
                $_id = $row["id"];
                $_path = $row["position"]; 
                
                $stmt1 = $con->prepare("SELECT * FROM entress_erp_part2 WHERE contact_id = ?");
                if ($stmt1 === false) {
                    die('Prepare failed: ' . htmlspecialchars($con->error));
                }
                $stmt1->bind_param("i", $_id);
                $stmt1->execute();
                $result = $stmt1->get_result();
                if ($result->num_rows > 0) {
                    while ($row = $result->fetch_assoc()) {
                        $_account = htmlspecialchars($antiXss->xss_clean($row['s_account']), ENT_QUOTES, 'UTF-8');
                        $_acc_serial_token = htmlspecialchars($antiXss->xss_clean($row['acc_serial_token']), ENT_QUOTES, 'UTF-8');
                        $_debtor = htmlspecialchars($antiXss->xss_clean($row['s_debtor']), ENT_QUOTES, 'UTF-8');
                        $_creditor = htmlspecialchars($antiXss->xss_clean($row['s_creditor']), ENT_QUOTES, 'UTF-8');

                        $stmtu = $con->prepare("SELECT * FROM categories WHERE acc_serial =? AND acc_name=? AND company=?");
                        $stmtu->bind_param("sss", $_acc_serial_token, $_account, $_SESSION["company"]);
                        $stmtu->execute();
                        $resultu = $stmtu->get_result();
                        if ($resultu->num_rows > 0) {
                            $rowu = $resultu->fetch_assoc();
                            $balance = $rowu['ac_balanced'];
                            $new_balance = $balance - $_debtor + $_creditor;

                            $stmtu2 = $con->prepare("UPDATE categories SET ac_balanced=? WHERE acc_serial=? AND acc_name=? AND company=?");
                            $stmtu2->bind_param('ssss', $new_balance, $_acc_serial_token, $_account, $_SESSION["company"]);

                            if ($stmtu2->execute()) {
                                $stmt5 = $con->prepare("DELETE FROM jentress_erp WHERE id = ? AND company=?");
                                $stmt5->bind_param("is", $_id, $_SESSION["company"]);
                                if ($stmt5->execute()) {
                                    if (isset($_path) && file_exists($_path)) {
                                        unlink($_path);
                                    }
                                    echo '<script>$(document).ready(function(){toastr.success("Entry successfully deleted");}) </script>';
                                } else {
                                    echo '<script>$(document).ready(function(){toastr.error("Error occurred while deleting the entry");}) </script>';
                                }
                            } else {
                                echo '<script>$(document).ready(function(){toastr.error("Error occurred while updating the balance");}) </script>';
                            }
                        } else {
                            echo '<script>$(document).ready(function(){toastr.error("No data found");}) </script>';
                        }
                    }
                } else {
                    echo '<script>$(document).ready(function(){toastr.error("Entry number error");}) </script>';
                }
            }
        } else {
            echo '<script>$(document).ready(function(){toastr.error("No data found for this number");}) </script>';
        }

        // After successful deletion, generate a new token
        $_SESSION['auth_token'] = bin2hex(random_bytes(20));
    }
}
?>

When trying to delete multiple entries in a single request or making successive delete requests within the same session, the CSRF token validation fails. The error message indicates a potential CSRF attack. This suggests that the CSRF token is being reused or is not being updated properly, causing subsequent requests to fail the CSRF check.

What I Need Help With:

  • Understanding why the CSRF token validation is failing when attempting to delete multiple entries.
  • Suggestions on how to properly handle CSRF tokens in such scenarios to ensure that multiple deletions can be performed in a single session without issues.

Solution

  • Unless I'm mistaken, your script only accepts one ID for deletion at once.

    Therefore if you're doing multiple deletions, it must mean you're making multiple different HTTP requests to this script. But a CSRF token is only valid for a single request.

    In fact the last line of the script you've shown us is specifically generating a new token and placing it in the Session, overwriting the old one - although it doesn't appear to take any steps to return the token to the front-end...presumably you have another process for doing that implemented somewhere.

    You need to ensure you get a new token and place it in your HTML form to use for each new request. Or you could consider re-designing your form and the PHP script so it can accept multiple IDs in one request.