Subscription form token expiration does not respond with an ajax message

Hello,

For the past weeks I have been trying to solve a problem on a subscription form I am developing without success. In fact the problem is related to a csrf token generation and after its expiration when trying to subscribe the ‘Token expired’ message has to be shown from the AJAX response , but nothing happens, it is stuck on the ‘Please wait…’ message, of course, after trying to press the subscribe button. Everything is working fine including the storage in mysql up until the key expires.

When I open the developer’s tools network monitor in Firefox or Chrome and double click on the last executed file (index.php) it throws:

Warning : A non-numeric value encountered in C:\xampp\htdocs\test\newsletter_signup\csrf.php on line 50

In the console of Firefox it throws:
SyntaxError: JSON.parse: unexpected end of data at line 1 column 1 of the JSON data

And in the console of Chrome:
Uncaught SyntaxError: Unexpected end of JSON input

Will someone try to help me solve this problem what should I look for or do to make the response after token expiration to work?, below is the output of the source:

index.php:

<?php 
require 'conn.php';
$new_token = new CSRF('subscribe');
?>
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>newsletter signup ajax method</title>
	<link rel="stylesheet" href="css/style.css">
	<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
	<script src="js/ajax.js" ></script>
</head>
<style>
	form .no-js-content {
		display: none;
	}
	form.noJS .main {
		display: none;
	}
	form.noJS .no-js-content {
		display: block;
	}
</style>
<body>
	<form id="newsletter-signup" action="?action=signup" method="post" class="noJS">
		<fieldset class="main">
		    <div class="token"><?php echo $new_token->get_token();?></div>
		    <label for="signup-email">Sign up for email offers, news & events:</label>
		    <input type="text" name="signup-email" id="signup-email" class="hint" hint-class="d_field" />
		    <p class="honeypot" style="display: none;">If you see can this field, please leave it empty. Thank you. <input type="text" name="url" /></p>
		    <input type="submit" id="signup-button" value="SUBSCRIBE" class="hint" hint-class="d_field"/>
			<div class="d_field" style="display: none;">
				<label class="checkbox">
					<input type="checkbox" name="terms" value="agreed">
					<i></i>
				</label>
				<p>I agree to receive the newsletter. My data will be processed in accordance with the <a href="/cookie-policy/" target="_blank">Data Protection and Cookie Management</a> I have read and accepted. I can unsubscribe at any time.</p>
			</div>
			<p id="signup-response"></p>
		</fieldset>
		<div class='no-js-content'>
	        <h1>JavaScript is Required.</h1>
	        <p>We are sorry, but MyBrand does not work properly without JavaScript enabled...</p>
	    </div>
	</form>
</body>
</html>

conn.php:

<?php
header("Content-Type: text/html; charset=utf-8");

require 'csrf.php';


$signup_process = isset($_GET['action']) && $_GET['action'] == 'signup';

if($signup_process) {

    $con = mysqli_connect('localhost:3306','root','','mydb');

    //sanitize data
    $agreement = mysqli_real_escape_string($con, empty($_POST['terms']) || $_POST['terms'] !== 'agreed');

    // CSRF protection
    $token = (isset($_POST["token_subscribe"])) ? strip_tags(trim($_POST["token_subscribe"])) : false;

    $token = htmlspecialchars($token, ENT_QUOTES, 'UTF-8');
    $new_token = new CSRF('subscribe');
    if (!$new_token->check_token($token)) {
        $status = "error";
        $message = "Token expired."; // INSTEAD SHOWING THIS MESSAGE AFTER KEY EXPIRATION AFTER TRYING TO SUBSCRIBE IT SIMPLY STAYS ON 'PLEASE WAIT...' MESSAGE
        exit;
    }

     //validate email address - check if input was empty
    $email = mysqli_real_escape_string($con, $_POST['signup-email']);
    if(empty($email)){
        $status = "error";
        $message = "You did not enter an email address.";
    }

    else if(!filter_var($email, FILTER_VALIDATE_EMAIL)){
    //validate email address - check if is a valid email address
        $status = "error";
        $message = "You have entered an invalid email address.";
    }

    else if ($agreement){
    // validate agrement to the terms and conditions
        $status = "error";
        $message = "You must agree our terms and conditions.";
    }

    else {
        $existingSignup = mysqli_query($con, "SELECT * FROM newsletter WHERE nl_email='$email'");   
        
        if(mysqli_num_rows($existingSignup) < 1){
 
            $insertSignup = mysqli_query($con, "INSERT INTO newsletter (nl_email) VALUES ('$email')");
            if($insertSignup){
               $status = "success";
               $message = "Thank you, your sign-up request was successful!";
            }

            else {
                $status = "error";
                $message = "Ooops, Theres been a technical error.";
            }
        }

        else {
            $status = "error";
            $message = "This email address has already been registered.";
            }
    }

    //return json response 
    $data = array(
        'status' => $status,
        'message' => $message
    );
 
    echo json_encode($data);

    exit;
}

?>

csrf.php

<?php
	if (!isset($_SESSION)) session_start();

	class CSRF {

		/**
		 * Token name of the session / html form field
		 * @var string
		 */
		private $token_name;

		/**
		 * When to timeout in seconds
		 * @var number
		 */
		private $timeout = 0; // this is set to 0 purposely so token expiration can be tested without waiting it to expire.

		public function __construct($token_name) {
			$this->token_name = $token_name;
		}

		/**
		 * Builds a new token and stores it in the session
		 * @param string $token_name
		 * @return token
		 */
		public function get_token() {
			// create a token
			$token_value = hash('sha256', mt_rand(0, mt_getrandmax()) . microtime(true));

			// Stored token to the session
			$_SESSION['token_' . $this->token_name] = $token_value;
			$_SESSION['token_time_' . $this->token_name] = time();
			
			// return new token to a HTML page
			return '<input type="hidden" name="token_' . $this->token_name . '" value="' . $token_value . '">';
		}

		/**
		 * Check a token
		 * @param string $token
		 * @return bool
		 */
		public function check_token($token) {
			// get a token from session
			$session_token = $this->get_token_from_session();
			$session_token_time = $this->get_token_time_from_session();

			// Lifetime of a token
			$token_time = time() - $session_token_time; // HERE LEADS THE NON-NUMERIC VALUE ENCOUNTERED ERROR
			// check a token
			if (($token_time < $this->timeout) && $session_token == $token) {
				return true;
			}

			// Unset token variables
			unset($_SESSION['token_' . $this->token_name]);
			unset($_SESSION['token_time_' . $this->token_name]);

			return false;
		}

		/**
		 * Get token from a session
		 * @return string
		 */
		public function get_token_from_session() {
			return isset($_SESSION['token_' . $this->token_name]) ? $_SESSION['token_' . $this->token_name] : '';
		}

		/**
		 * Get token creation time from a session
		 * @return string
		 */
		public function get_token_time_from_session() {
			return isset($_SESSION['token_time_' . $this->token_name]) ? $_SESSION['token_time_' . $this->token_name] : '';
		}
	}
?>

ajax.js:

$(document).ready(function(){
    $('#newsletter-signup').submit(function(){
        //check the form is not currently submitting
        if($(this).data('formstatus') !== 'submitting'){
     
            //setup variables
            var form = $(this),
            formData = form.serialize(),
            formUrl = form.attr('action'),
            formMethod = form.attr('method'), 
            responseMsg = $('#signup-response');

            //add status data to form
            form.data('formstatus','submitting');

            //show response message - waiting
            responseMsg.hide()
            .addClass('response-waiting')
            .text('Please wait...')
            .fadeIn(200);
     
            //send data to server for validation
            $.ajax({
                url: formUrl,
                type: formMethod,
                data: formData,
                success:function(data){
                    //setup variables
                    var responseData = JSON.parse(data),
                        response = '';
                    //response conditional
                    switch(responseData.status){
                        case 'error':
                            response = 'response-error';
                        break;
                        case 'success':
                            response = 'response-success';
                        break;
                    }
                    //show reponse message
                    responseMsg.removeClass('response-waiting')
                    .addClass(response)
                    .text(responseData.message)
                    .fadeIn(200,function(){
                        responseMsg.removeClass(response);
                        form.data('formstatus','idle');
                    });
                }
            });
        }
        //prevent form from submitting
        return false;  
    });
})

Thank you

So… work backward. time() will always return an int; so $session_token_time must be not a number.

$session_token_time = $this->get_token_time_from_session(); , so your get_token_time_from_session returned something that wasnt a number.

EDIT: I can read function names. Derp.

	/**
	 * Get token creation time from a session
	 * @return string
	 */
	public function get_token_time_from_session() {
		return isset($_SESSION['token_time_' . $this->token_name]) ? $_SESSION['token_time_' . $this->token_name] : '';
	}

Why does this function return a string, if you wanted a NUMBER in the form of a time? And why is the ‘default’ value to return an empty string, rather than a default time value?

The php warning is because of no validation before using a value. The check_token() method should determine if there is a saved session token before trying to use the token values. If there is no saved token, the logic should directly return a false value and not attempt to calculate/test if the token has timed out.

Another problem with this same logic, due to no validation before using a value, is if there’s no saved session token and an empty token value is submitted from the form (think of a bot script just submitting email addresses without visiting the form page or without propagating a session id cookie), the code will return a true, because token_time will be equal to time(), which won’t be less-than the timeout value, and an empty session_token is equal to an empty submitted token value.

As to the incorrect JSON error in the browser, it’s due to the exit statement in your code. The code that produces and outputs the JSON response back to the browser is not being executed in this case. Rather than to add even more nested logic to your current form processing code, this is a case where ‘short circuiting’ the logic and repeating the code that produces and outputs the JSON response would be okay. You should actually eliminate as much of this nested logic as possible, by performing all validation tests that are not dependent on a previous validation test, within a single request.

1 Like

I was dumping $session_token_time so far and it outputs an integer.
When session is set,
$session_token = $this->get_token_from_session(); returns a random string that is is also seen in the hidden html input tag and
$session_token_time = $this->get_token_time_from_session(); is returning integers and as you already said, time() function always returns an integer so I don’t see where is the problem at this point unless I am missing something that you are trying to explain. The $token_time variable always returns the difference between time() and $session_token_time as integer.

As $_SESSION is a superglobal variable when dumping

$_SESSION['token_time_' . $this->token_name]

it returns the current time because the time() function is assigned as it’s value, so how can

public function get_token_time_from_session() {
	return isset($_SESSION['token_time_' . $this->token_name]) ? $_SESSION['token_time_' . $this->token_name] : '';
}

return a string?

Hmm… well let’s see what are the possible values that could return from this line. You used a ternary, so there must be 2… either it’s the token time… or…

Do you mean the empty string as false has to be changed to false?

return isset($_SESSION['token_time_' . $this->token_name]) ? $_SESSION['token_time_' . $this->token_name] : '';

You want the function to return a Number, in order to do some subtraction from another Number without generating an error that says “that wasnt a Number.”

‘’ is a String.
false is a Boolean.

Neither of those things is a number.

1 Like

I must admit I don’t understand what you mean.

return isset($_SESSION['token_time_' . $this->token_name]) ? $_SESSION['token_time_' . $this->token_name] : '';

returns a number if true, an empty string if false when there is no session at all.

Right. And if it returns an empty string, what happens?

$session_token_time = $this->get_token_time_from_session();
… so $session_token_time becomes an empty string…

$token_time = time() - $session_token_time;

time() returns 1575669497, and $session_token_time is ‘’, so lets substitute those…
$token_time = 1575669497 - '';
annnnnd…
Warning : A non-numeric value encountered in C:\xampp\htdocs\test\newsletter_signup\csrf.php on line 50

Your code tried to do 3 - apple and threw an error. Understandably.

1 Like

Whole time I couldn’t realize why non-numeric value was encountering because I was having simple thought that after false output the method will not try to calculate further. Thanks for pointing on this. :+1:

The problem with the json encoding still persists.

Thank you for pointing, I have finally come to a conclusion why the message was not transferred, and all because of the early exit statement. I was so simple minded. I appreciate your valuable information.