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.

asked Sep 24, 2013 at 6:38
\$\endgroup\$
2
  • \$\begingroup\$ See the Electronic Voting Systems listed at: bitbucket.org/djarvis/world-politics/wiki/Related%20Links \$\endgroup\$ Commented Sep 24, 2013 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\$ Commented Sep 24, 2013 at 19:39

2 Answers 2

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

answered Sep 24, 2013 at 10:34
\$\endgroup\$
3
  • \$\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\$ Commented Sep 24, 2013 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\$ Commented Sep 25, 2013 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\$ Commented Sep 27, 2013 at 9:32
1
\$\begingroup\$

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

answered Sep 27, 2013 at 10:06
\$\endgroup\$
5
  • \$\begingroup\$ I did to save processing time - no need to define functions that won't be used. Why is it a bad idea? \$\endgroup\$ Commented Sep 27, 2013 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\$ Commented Sep 27, 2013 at 12:18
  • \$\begingroup\$ Ok, moving them out then. \$\endgroup\$ Commented Sep 30, 2013 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\$ Commented Oct 3, 2013 at 7:55
  • \$\begingroup\$ @SilverlightFox Sorry, didn't understand the system. Thought it was possible to mark more than one as correct answers. \$\endgroup\$ Commented Oct 3, 2013 at 16:59

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.