Implications of a dynamic token

Discussions of secure PHP coding. Security in software is important, so don't be afraid to ask. And when answering: be anal. Nitpick. No security vulnerability is too small.

Moderator: General Moderators

Post Reply
User avatar
Weiry
Forum Contributor
Posts: 323
Joined: Wed Sep 09, 2009 5:55 am
Location: Australia

Implications of a dynamic token

Post by Weiry »

This was mainly a proof of concept system as a way for exploring the possible ways of securing a form on a website without the form being inside a PHP file (so the form itself is in a plain .html file with no PHP executing).

Note: Yes i do realise that inherently over non-ssl connections exposing a token like this may be hazardous, however the point of this test was have the form generated without PHP.

The Structure:

Code: Select all

index.html
req.php
js/main.js
css/style.css
lib/loader.php
lib/stdOut.php
lib/token.php
Overview:
  • index.html contains a form with X number of fields plus a submit button.
  • When the user finishes filling in all of the fields, they press the submit button to process the form.
  • jQuery intercepts the form's onsubmit event and handles it manually.
  • Submit validates to make sure that data exists in the fields (plus any html5 validation patterns)
  • Submit calls the javascript function 'getToken()' and assigns the output to the variable 'nToken'
  • getToken() submits an ajax GET request to 'req.php?gettoken' to obtain a new token
  • req.php creates a 'new token;' class and requests a new token.
  • token generates a new token on __construct as there is no $_POST['token'] to check.
  • New token is also stored to $_SESSION['token'] with a variable key name and the token as the value ( array('token_random'=>'thetoken') )
  • req.php returns the generated token in the json format {response:'thetoken',status:'success'}
  • Submit then checks to see if the form contains a field with the jQuery selector $('#formModToken'), if it exists it updates with nToken, else it creates a new dom element and appends it to the form.
  • Submit (now with the token field), submits the form (via ajax) to req.php (handling the original for submission)
  • req.php finds there to be a $_POST, and when 'new token;' is reconstructed, finds a $_POST['token'] field and assigns that as the current token.
  • req.php then calls token::checkToken()
  • token checks current stored token against the session variable.
  • token returns a boolean to req.php
  • req.php deals with the actions/response
  • Submit removes the token field from the form after a response
There is 2 sections in particular I'm a little unsure about whether or not it is viable security wise to be implementing them in the current way.

1. Storing the token inside a predefined session variable as an array (mashed keyboard example below). Given that the token is always stored inside $_SESSION['token'], is there a more appropriate way to handle this? This also comes back to how i accessed the token by using array_keys() on $_SESSION['token'] to retrieve the current token.

Code: Select all

$_SESSION['token'] = array('token_asdfa87g63g83a' => 'fawbf97w9f982');
2. Retrieving the token via ajax and dynamically adding/removing the token field to the form. Would this actually stop something like a BOT from processing the form? My first thoughts are that i perhaps should not be processing the standard onsubmit event, but rather using a separate button not tied to the forms events... thoughts?

Are there any other issues that I'm just not seeing in the implementation or way in which it operates?

Feedback would be appreciated :)

Code:
Here is the actual code used in the above.

index.html

Code: Select all

<html>
<head>

    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="css/bootstrap-dialog.min.css">
    <link rel="stylesheet" href="css/pack.css"/>
</head>
<body>

<div class='container'>

    <div class='row'>
        <div class='col-md-6'>
            <h2>Pack Info</h2>
            <div id='pack-info'></div>
            <h2>Request</h2>
            <form method='post' onsubmit="return false;">
                <div class='form-group'>
                    <label for='formName'>Name</label>
                    <input type='text' name='formName' class='form-control' id='formName' placeholder='Joe Blogs' required aria-required="true" />
                </div>
                <div class='form-group'>
                    <label for='formURL'>URL</label>
                    <input type='url' pattern="https?://.+" name='formUrl' class='form-control' id='formURL' placeholder='http://myurl.com' required $
                </div>
                <div class='form-group'>
                    <label for='formDesc'>Description</label>
                    <textarea id='formDesc' name='formDesc' class='form-control' rows='5' required aria-required="true"></textarea>
                </div>
                <button type="submit" class="btn btn-default">Submit</button>
            </form>
        </div>

    </div>
    <script src="http://code.jquery.com/jquery-1.11.2.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
    <script src="js/bootstrap-dialog.min.js"></script>
    <script src="js/main.js"></script>
</body>
</html>
main.js

Code: Select all

jQuery(document).ready(function(){
    $("form").submit(function (e) {
        e.preventDefault();
        // Silly validation to make sure form isn't empty
        var n = 0;
        if( $('#formName').val() == "" ){ n++; }
        if( $('#formURL').val() == "" ){ n++; }
        if( $('#formDesc').val() == "" ){ n++; }
        if( n > 0 ){
            dialog('Form Validation', 'Please fill in all fields correctly', BootstrapDialog.TYPE_DANGER);
            return false;
        }

        var nToken = getToken();
        if( $('#formToken').length == 0 ){
            var t = $('<input>');
            t.attr({id:'formToken',name:'token',value:nToken,type:'hidden'});
            $("form").append(t);
        } else {
            $('#formToken').val(nToken);
        }

        $.ajax({method:'post',url:'req.php',data:$("form").serialize()})
            .done(function(data){
                if( data.status == "success" ){
                    dialog('Request', data.response, BootstrapDialog.TYPE_SUCCESS);
                }else if( data.status == "Token.SUCCESS" ){
                    dialog('Request', data.response, BootstrapDialog.TYPE_SUCCESS);
                    $('#formToken').remove();
                    $("form")[0].reset();
                }else if( data.status == "error" || data.status == 'Token.ERROR' ){
                    dialog('Request', data.response, BootstrapDialog.TYPE_DANGER);
                }
            })
            .fail(function(data){ dialog('Request', data.response, BootstrapDialog.TYPE_DANGER); });
    });
});

var getToken = function(){
    var token = '';
    $.ajax({method:'get',url:'req.php?gettoken',async:false}).done(function(data){token=data.response;});
    return token;
}

var dialog = function(title, body, type){
    BootstrapDialog.show({
        type: type,
        title: title,
        message: body,
        buttons: [
            { label: 'Close', action: function(dialog){ dialog.close(); } }
        ]
    });
}
req.php

Code: Select all

<?php
session_start();

define("_EXEC",true);
require_once 'lib/loader.php';
loader::load('token.php');

$token = new token;

if( isset($_GET['gettoken']) )
{
    $obj = new stdClass;
    $obj->response = $token->getToken();
    $obj->status = 'success';
}
if(isset($_POST) && !empty($_POST)){
    $obj = new stdClass;
    if(!$token->checkToken()) {
        $obj->response = "Invalid Token";
        $obj->status = 'Token.ERROR';
    } else {
        /**
         * Now we add the request to the database
         */
        if( database result ) {
            $obj->response = "Thanks for adding your request for a modpack!";
            $obj->status = 'Token.SUCCESS';
        } else {
            $obj->response = "There was a problem processing your request.";
            $obj->response = 'Token.ERROR';
        }
    }
}

if( empty($obj) ){ die(); }
header('Content-Type: application/json');
echo json_encode($obj);
token.php

Code: Select all

<?php
if(!defined("_EXEC")){ die("No Access"); }

class token {

    private $token;
    private $stName;

    public function __construct( )
    {
        if( !isset( $_POST['token']) ){
            $this->createToken();
            $newToken = $this->getToken();
        }
        $this->token = ( isset( $_POST['token']) ) ? $_POST['token'] : $newToken;
    }

    private function createToken()
    {
        $token = md5(uniqid(rand(), TRUE));
        $this->stName = 'token_'.md5(session_id().$token);

        unset($_SESSION['token']);
        $this->token = $_SESSION['token'][$this->stName] = $token;
        return $token;
    }

    public function checkToken()
    {
        $keys = array_keys($_SESSION['token']);
        $key = $keys[0];
        $return = ($this->token == $_SESSION['token'][$key]) ? true : false;

        // Create a new token
        $this->createToken();
        return $return;
    }

    public function getToken()
    {
        $useToken = (empty($_POST['token'])) ?  $this->token : $_POST['token'];
        return $useToken;
    }

}
Post Reply