2
\$\begingroup\$

I'm writing a somewhat secure voting system. I'm aware that it won't be really secure because it's on the internet so I'm looking for feedback on any logic errors or glaring mistakes in the security implementation.

The backend is ExpressionEngine CMS.

So, in this system user is allowed to vote anonymously or not anonymously. And in both cases are allowed to change the vote as long as the poll is open. After the vote ends user can verify cast vote by assigned ticket (anonymously) or by name.

One thing I know I haven't taken into account is detection of tampering in the tables. I guess I should have a fingerprint hash of all cast votes in another database, but I'm not sure how to do it.

I have two tables:

pt_voters_new: voters_id (int), entry_id (int), member_id (int), change_vote (int), iv (varchar), salt (varchar)

pt_votes_new: votes_id (varchar), entry_id (int), vote (int), member_id (int), enc (varchar)

I have a FORM that submits the chosen vote and other choices via an AJAX call. Here is the main code:

<?php
function crypto_rand($min, $max) {
    $range = $max - $min;
    $length = (int) (log($range,2) / 8) + 1;
    $num = hexdec(bin2hex(openssl_random_pseudo_bytes($length,$s))) % $range;
    return $num + $min;
}
// str_makerand is used for creating random ticket, that user can use to verify vote later (note mt_rand is not cryptographically secure)
function str_makerand ($minlength, $maxlength, $useupper, $usespecial, $usenumbers) {
    $charset = "ACDEFGHJKLMNPQRTUVWXY";
    if ($useupper) $charset .= "ACDEFGHJKLMNPQSTUVWXY";
    if ($usenumbers) $charset .= "34679";
    if ($usespecial) $charset .= "~@#$%^*()_+-={}|][";
    for ($i=0; $i<$maxlength; $i++) $key .= $charset[(crypto_rand(0,(strlen($charset)-1)))];
    return $key;
}
// Check that members group are allowed to vote
if (($this->EE->session->userdata('group_id') == 1) || ($this->EE->session->userdata('group_id') == 5) || ($this->EE->session->userdata('group_id') == 6) || ($this->EE->session->userdata('group_id') == 7) || ($this->EE->session->userdata('group_id') == 8) || ($this->EE->session->userdata('group_id') == 9) || ($this->EE->session->userdata('group_id') == 10) || ($this->EE->session->userdata('group_id') == 11)) {
    // Function for logging
    function logvote ($text) {
        $handle = fopen("/dana/data/www.parlamentet.dk/votelog/logtest.txt", "a+");
        fwrite($handle, $text);
        fwrite($handle, "\r\n");
        fclose($handle);
    }
    // Get password_compat for bcrypt.
    require('/dana/data/www.parlamentet.dk/scripts/password.php');
    // Get DB connection
    require('/dana/data/www.parlamentet.dk/scripts/db.php');
    // Check of form XID token
    $member_id = (int)$this->EE->session->userdata('member_id'); // Users ID from ExpressionEngine session data
    $ip_address = $this->EE->session->userdata('ip_address'); // Users IP address, only used for blocking check and error handling
    $XID = $_POST['XID'];
    $this->EE =& get_instance();
    $this->EE->load->library('functions');
    if (ee()->security->secure_forms_check($XID) === FALSE) {
        echo '
        <p style="text-align: center"><strong>AFSTEMNING</strong></p>
        <p><strong>Fejl:</strong> Sikkerhedscheck fejlede. Forsøg igen eller kontakt os.</p>
        <p><strong>Fejlkode:</strong> 001</p>
        ';
        $log = $member_id." failed securitycheck 001 (from $ip_address)";
        logvote($log);
        exit();
    }
    $all_query_ok = TRUE; // Control variable for queries
    // $time_now = time() - 3600;
    $time_now = time();
    $entry_id = (int)$_POST['entry_id']; // Unique entry ID
    $vote = (int)$_POST['stemme']; // What user voted - 1, 2 or 3
    if ($vote == '') {
        echo '
        <p style="text-align: center"><strong>AFSTEMNING</strong></p>
        <p>Din stemme er <strong>IKKE</strong> registreret, da du ikke valgte noget at stemme. Genindlæs siden for at stemme korrekt.</p>
        <p><strong>Fejlkode:</strong> 002</p>
        ';
        $log = $member_id." did not choose voteoptions 002";
        logvote($log);
        exit();
    }
    if (($vote > 3) || ($vote < 1)) { // Allowed range of votes
        echo '
        <p style="text-align: center"><strong>AFSTEMNING</strong></p>
        <p><strong>Fejl:</strong> Sikkerhedscheck fejlede. Forsøg igen eller kontakt os.</p>
        <p><strong>Fejlkode:</strong> 003</p>
        ';
        $log = $member_id." somehow voted for something strange 003";
        logvote($log);
        exit();
    }
    $anonymous_vote = (int)$_POST['anonymous_vote']; // Is it an anonymous vote?
    $change_vote = (int)$_POST['change_vote']; // Should user be allowed to change vote later?
    $changing_vote = (int)$_POST['changing_vote']; // Is this vote a change of a previous vote?
    $password = $_POST['password']; // One time password to change the vote later
    if ($change_vote === 1) {
        // Blowfish hash of password (bcrypt)
        $hash = password_hash($password, PASSWORD_BCRYPT, array("cost" => 10)); // Kan sættes til PASSWORD_DEFAULT (cost 4 - 31)
        // Verify hash
        if (password_verify($password, $hash) === FALSE) {
            echo '
            <p style="text-align: center"><strong>AFSTEMNING</strong></p>
            <p>Din stemme er <strong>IKKE</strong> registreret, da der opstod en fejl. Forsøg venligst igen!</p>
            <p><strong>Fejlkode:</strong> 004</p>
            ';
            $log = $member_id." failed password check 004";
            logvote($log);
            exit();
        }
        // Create key from password
        $key_from_pw = hash('sha256', $password);
        // Get IV
        $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
        $iv = mcrypt_create_iv($iv_size, MCRYPT_DEV_URANDOM);
        $iv_base64 = base64_encode($iv);
        // Data about vote
        $data = json_encode(array('member_id'=>$member_id, 'entry_id'=>$entry_id, 'hash'=>$key_from_pw));
        // Encrypt data
        $enc = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key_from_pw, json_encode($data), MCRYPT_MODE_CBC, $iv);
        $enc_base64 = base64_encode($enc);
    }
    else {
        $change_vote = 0;
        $enc_base64 = '';
        $iv = '';
        $hash = '';
    }
    // Check if user already voted
    $result = mysqli_query($link, "SELECT voters_id FROM pt_voters WHERE member_id = '$member_id' AND entry_id='$entry_id'"); 
    $num_results = mysqli_num_rows($result); 
    // Check if poll is still open
    $result = mysqli_query($link, "SELECT field_id_18,field_id_44,status FROM exp_channel_data,exp_channel_titles WHERE exp_channel_titles.entry_id='$entry_id' AND exp_channel_titles.entry_id=exp_channel_data.entry_id"); 
    $row = mysqli_fetch_array($result); 
    $slut_ft = (int)$row["field_id_18"]; // Endtime for the vote in Danish Parliament
    if ($slut_ft === 0) { // Change this logic!
        $slut_ft = 9378133526;
    }
    $slut_pt = $row["field_id_44"]; // Sluttidspunkt for Parlamentets egne forslag
    $status = $row["status"];
    if ((($num_results === 0) || ($changing_vote === 1)) && (($time_now <= $slut_ft) || ($time_now <= $slut_pt)) && ($status == 'Fremsat')) { // User has not yet voted or is allowed to change vote
        if ($changing_vote === 1) { // Verify password if user is changing vote
            $result = mysqli_query($link, "SELECT iv,hash,voters_id FROM pt_voters WHERE member_id = '$member_id' AND entry_id = '$entry_id'"); 
            $row = mysqli_fetch_array($result); 
            $hash_check = $row['hash'];
            $voters_id = $row['voters_id'];
            // $hash = password_hash($password, PASSWORD_BCRYPT, array("cost" => 10, "salt" => $salt_check));
            if (password_verify($password, $hash_check) === FALSE) {
                print '
                <p style="text-align: center"><strong>AFSTEMNING</strong></p>
                <p>Forkert kodeord! Du kan genindlæse siden og forsøge igen!</p>
                <p><strong>Fejlkode:</strong> 005</p>
                ';
                $log = $member_id." failed password check 005";
                logvote($log);
                exit();
            }
            $key_from_pw = hash('sha256', $password);
            $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
            $old_iv_base64 = $row['iv'];
            $old_iv = base64_decode($old_iv_base64);
            $data = json_encode(array('member_id'=>$member_id, 'entry_id'=>$entry_id, 'hash'=>$key_from_pw));
            $enc_test = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key_from_pw, json_encode($data), MCRYPT_MODE_CBC, $old_iv);
            $enc_test_base64 = base64_encode($enc_test);
            $result = mysqli_query($link, "SELECT votes_id FROM pt_votes WHERE enc='$enc_test_base64'");
            $num_results = mysqli_num_rows($result); 
            if ($num_results === 1) { // OK - data checked out
                // Removing all data from previous vote
                $row = mysqli_fetch_array($result); 
                $votes_id = $row['votes_id'];
                mysqli_query($link, "DELETE FROM pt_votes WHERE votes_id='$votes_id'") ? null : $all_query_ok=FALSE;
                mysqli_query($link, "DELETE FROM pt_voters WHERE entry_id='$entry_id' AND member_id='$member_id'") ? null : $all_query_ok=FALSE;
            }
            else {
                print '
                <p style="text-align: center"><strong>AFSTEMNING</strong></p>
                <p>Forkert kodeord! Du kan genindlæse siden og forsøge igen!</p>
                <p><strong>Fejlkode:</strong> 007</p>
                ';
                $log = $member_id." did not enter correct password 007";
                logvote($log);
                exit();
            }
        }
        $num_results_2 = 1;
        while ($num_results_2 != 0) {
            $votes_id = str_makerand(10,10,1,0,1); // Random ID for vote
            $result_2 = mysqli_query($link, "SELECT * FROM pt_votes WHERE votes_id='$votes_id'"); // Check if ID exists
            $num_results_2 = mysqli_num_rows($result_2);  
            if ($num_results_2 === 0) {
                // ID OK - casting vote
                if ($anonymous_vote === 1) {
                    // $query = "INSERT INTO pt_votes (votes_id,entry_id,vote,vote_time,enc) VALUES ('$votes_id','$entry_id','$vote','$time_now','$enc_base64')";
                    $query = "INSERT INTO pt_votes (votes_id,entry_id,vote,enc) VALUES ('$votes_id','$entry_id','$vote','$enc_base64')";
                }
                else {
                    // $query = "INSERT INTO pt_votes (votes_id,entry_id,vote,vote_time,enc,member_id) VALUES ('$votes_id','$entry_id','$vote','$time_now','$enc_base64','$member_id')";
                    $query = "INSERT INTO pt_votes (votes_id,entry_id,vote,enc,member_id) VALUES ('$votes_id','$entry_id','$vote','$enc_base64','$member_id')";
                }
                mysqli_query($link, $query) ? null : $all_query_ok=FALSE;
                // Shuffle table
                $query = "ALTER TABLE pt_votes ORDER BY votes_id ASC";
                mysqli_query($link, $query) ? null : $all_query_ok=FALSE;
                // Getting ID for voters_id
                $num_results_3 = 1;
                while ($num_results_3 != 0) {
                    $voters_id = crypto_rand(100,1000000000000);
                    $result_3 = mysqli_query($link, "SELECT * FROM pt_voters WHERE voters_id='$voters_id'"); // Check for existance of voters_id
                    $num_results_3 = mysqli_num_rows($result_3); 
                    if ($num_results_3 == 0) {
                        // $query = "INSERT INTO pt_voters (voters_id,entry_id,member_id,ip_address,change_vote,iv,salt) VALUES ('$voters_id','$entry_id','$member_id','$ip_address','$change_vote','$iv_base64','$salt')";
                        $query = "INSERT INTO pt_voters (voters_id,entry_id,member_id,change_vote,iv,hash) VALUES ('$voters_id','$entry_id','$member_id','$change_vote','$iv_base64','$hash')";
                        mysqli_query($link, $query) ? null : $all_query_ok=FALSE;
                        // Shuffle table
                        $query = "ALTER TABLE pt_voters ORDER BY voters_id ASC";
                        mysqli_query($link, $query) ? null : $all_query_ok=FALSE;
                    }
                }
                if ($all_query_ok) {
                    mysqli_commit($link);
                }
                else {
                    mysqli_rollback($link);
                    echo '
                    <p style="text-align: center"><strong>AFSTEMNING</strong></p>
                    <p>Der opstod en fejl! Du kan genindlæse siden og forsøge igen!</p>
                    <p><strong>Fejlkode:</strong> 008</p>
                    ';
                    mysqli_close($link);
                    $log = $member_id." made the SQL fail! 008";
                    logvote($log);
                    exit();
                }
                // Everything OK. Inform user and write to logfile, print and/or insert posts in S3
                $action = ' voted';
                if ($changing_vote === 1) {
                    $action = ' changed vote';
                }
                if ($anonymous_vote === 1) {
                    $action .= ' anonymously';
                }
                if ($change_vote === 1) {
                    $action .= ' and is allowed to change vote';
                }
                $log = $member_id.$action;
                logvote($log);
                if ($anonymous_vote === 1) {
                    echo '
                    <p style="text-align: center;"><strong>AFSTEMNING</strong></p>
                    <p style="text-align: justify;">Din stemme er registreret anonymt. Du kan bruge nedenstående ID til at verificere stemmens korrekthed, når afstemningen er slut. Bemærk at
                    ID ikke er tilknyttet dig i vores system, så du skal selv huske det, hvis du ønsker at verificere. Det vises ikke igen, når du forlader denne side.</p>
                    <div id="printablereceipt">
                    <p style="text-align: center; font-family: Courier New, monospace; letter-spacing: 3px; font-size: 150%;"><strong>'.$votes_id.'</strong></p>
                    <div id="QRimage" style="text-align:center;"></div>
                    </div>
                    <p style="text-align: center;"><a class="btn btn-success" style="color: white;" href="#" onclick="printReceipt();"><i class="icon-print icon-large"></i> UDSKRIV</a></p>
                    ';
                    $QRcode = 'https://www.parlamentet.dk/parlamentet/detaljer/'.$entry_id.'#v'.$votes_id;
                    echo "
                    <script>
                        var image = qr.image({
                            background: '#f5f5f5',
                            size: 7,
                            level: 'L',
                            value: '".$QRcode."'
                        });
                        if (image) {
                            document.getElementById('QRimage').appendChild(image);
                        }

                    </script>
                    ";
                }
                else {
                    echo '
                    <p style="text-align: center;"><strong>AFSTEMNING</strong></p>
                    <p style="text-align: justify;">Din stemme er registreret uden anonymitet. Når afstemningen er slut, vil dit navn vises med din stemme, så du kan verificere stemmen. Du kan også bruge nedenstående ID til at verificere stemmen.</p>
                    <p style="text-align: center; font-family: Courier New, monospace; letter-spacing: 3px; font-size: 150%;"><strong>'.$votes_id.'</strong></p>
                    <div id="QRimage" style="text-align:center;"></div>
                    </div>
                    <p style="text-align: center;"><a class="btn btn-success" style="color: white;" href="#" onclick="printReceipt();"><i class="icon-print icon-large"></i> UDSKRIV</a></p>
                    ';
                    $QRcode = 'https://www.parlamentet.dk/parlamentet/detaljer/'.$entry_id.'#v'.$votes_id;
                    echo "
                    <script>
                        var image = qr.image({
                            background: '#f5f5f5',
                            size: 7,
                            level: 'L',
                            value: '".$QRcode."'
                        });
                        if (image) {
                            document.getElementById('QRimage').appendChild(image);
                        }

                    </script>
                    ";
                }
            }
        }
    }
    else {
        echo '
        <p style="text-align: center"><strong>AFSTEMNING</strong></p>
        <p><strong>Fejl:</strong> Du har allerede stemt i denne afstemning eller afstemningen er ikke åben.</p>
        <p><strong>Fejlkode:</strong> 009</p>
        ';
        $log = $member_id." tried to vote but failed 009";
        logvote($log);
        exit();
    }
}
else {
    echo '
    <p style="text-align: center"><strong>AFSTEMNING</strong></p>
    <p><strong>Fejl:</strong> Du har ikke rettigheder til at stemme.</p>
    <p><strong>Fejlkode:</strong> 010</p>
    ';
    $log = $member_id." does not have rights to vote 010";
    logvote($log);
    exit();
}

?>

Help much appreciated.

\$\endgroup\$
  • \$\begingroup\$ See the Electronic Voting Systems listed at: bitbucket.org/djarvis/world-politics/wiki/Related%20Links \$\endgroup\$ – Dave Jarvis Sep 24 '13 at 17:05
  • \$\begingroup\$ Thanks for the extensive list @DaveJarvis. I'll definitely look into those I don't know. However, I will be sticking with this voting system because of the specific requirements of our site. \$\endgroup\$ – huulbaek Sep 24 '13 at 19:39
2
\$\begingroup\$

Don't use mt_rand as the "random" value can be predictable:

This function does not generate cryptographically secure values, and should not be used for cryptographic purposes. If you need a cryptographically secure value, consider using openssl_random_pseudo_bytes() instead.

See openssl_random_pseudo_bytes

Also, parameterise your queries rather than using string concatenation: https://www.owasp.org/index.php/Query_Parameterization_Cheat_Sheet

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ True about mt_rand. But I don't think it needs to be secure cryptographically as it's not used in any of the crypt functions. Nevertheless, I will use the other function instead. Yes, I know, I should parameterize. But it is necessary when I force all variables to (int)? \$\endgroup\$ – huulbaek Sep 24 '13 at 19:43
  • \$\begingroup\$ It depends where you're using the token - if you don't want a malicious user to be able to guess a token (and possibly change someone else's vote), make sure it is not predictable by using a secure generator. This is good practise. I notice some of your SQL variables are not int but even if they were it is good practise to parameterise, then if your code is later updated it is less likely to be overlooked. \$\endgroup\$ – SilverlightFox Sep 25 '13 at 10:58
  • \$\begingroup\$ Of course - and no reason not to use it. I have updated my code to reflect the changes (haven't gotten around to parameterizing queries but will do it). Thanks! \$\endgroup\$ – huulbaek Sep 27 '13 at 9:32
1
\$\begingroup\$

A general note on the code - you shouldn't define a function inside a if condition

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I did to save processing time - no need to define functions that won't be used. Why is it a bad idea? \$\endgroup\$ – huulbaek Sep 27 '13 at 11:50
  • \$\begingroup\$ Processing time does not depend on the number of functions defined in the code. The function has to be executed. So you can define n number of functions in code that will not have any impact on processing time unless it is called. \$\endgroup\$ – Kinjal Sep 27 '13 at 12:18
  • \$\begingroup\$ Ok, moving them out then. \$\endgroup\$ – huulbaek Sep 30 '13 at 6:57
  • \$\begingroup\$ @huulbaek Why have you unaccepted my answer to accept this one instead? They are both valid answers, so it seems unfair to mark one as the accepted one. \$\endgroup\$ – SilverlightFox Oct 3 '13 at 7:55
  • \$\begingroup\$ @SilverlightFox Sorry, didn't understand the system. Thought it was possible to mark more than one as correct answers. \$\endgroup\$ – huulbaek Oct 3 '13 at 16:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.