I've followed many tutorials to make my user system as secure as possible. I don't know what else I might've missed. The website will be released to hundreds of thousands of users. I am clueless to what security flaw I missed and I'll appreciate it if any of you advanced users out there can give me feedback on how to improve.
Registration
function validateDate($date, $format = 'Y-m-d H:i:s'){
$d = DateTime::createFromFormat($format, $date);
return $d && $d->format($format) == $date;
}
if (isset($_POST['register'])) {
if (empty($_POST['date'])) {
$error = '1';
$dateError = "Please enter a birthday date.";
} else {
if ((validateDate($_POST['date'], 'm/d/Y')) == FALSE) {
$error = '1';
$dateError = "Please enter a valid birthday date.";
}
}
if (empty($_POST['username'])) {
$error = '1';
$usernameError = "Please enter an username.";
} else
if (strlen($_POST['username']) < 3) {
$error = '1';
$usernameError = "Username must have at least 3 characters.";
} else
if (!preg_match("/^[a-z0-9\d_]{3,16}$/i", $_POST['username'])) {
$error = '1';
$usernameError = "Username can only contain alphabets, underscore characters and numbers.";
} else {
$sql = "SELECT * FROM users WHERE username=?";
$stmt = $mysql->prepare($sql);
$stmt->bind_param('s', $_POST['username']);
$stmt->execute();
$result = $stmt->get_result();
$results = $result->num_rows;
if ($results >= 1) {
$error = '1';
$usernameError = "Username is already in use.";
}
}
if (!empty($_POST["name"])) {
if (!preg_match("^(\s)*[A-Za-z]+((\s)?((\'|\-|\.)?([A-Za-z])+))*(\s)*$^", $_POST['name'])) {
$error = '1';
$nameError = "Name can only contain alphabets and spaces.";
}
}
if (!filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
$error = '1';
$emailError = "Please enter a valid email address.";
} else {
$xsql = "SELECT * FROM users WHERE email=?";
$xstmt = $mysql->prepare($xsql);
$xstmt->bind_param('s', $_POST['email']);
$xstmt->execute();
$xresult = $xstmt->get_result();
$xresults = $xresult->num_rows;
if ($xresults >= 1) {
$error = '1';
$emailError = "Email is already in use.";
}
}
if (empty($_POST['password'])) {
$error = '1';
$passError = "Please enter a password.";
}
else {
if (strlen($_POST['password']) < 6) {
$error = '1';
$passError = "Password must have at least 6 characters.";
}
}
if ($error == '0') {
$name = $_POST['name'];
$name = substr($name, 0, 40);
$username = $_POST['username'];
$username = substr($username, 0, 16);
$date = $_POST['date'];
$email = $_POST['email'];
$password = $_POST['password'];
$trn_date = date("Y-m-d H:i:s");
$ip = '';
if (getenv('HTTP_CLIENT_IP')) {
$ip = getenv('HTTP_CLIENT_IP');
}
else
if (getenv('HTTP_X_FORWARDED_FOR')) {
$ip = getenv('HTTP_X_FORWARDED_FOR');
}
else
if (getenv('HTTP_X_FORWARDED')) {
$ip = getenv('HTTP_X_FORWARDED');
}
else
if (getenv('HTTP_FORWARDED_FOR')) {
$ip = getenv('HTTP_FORWARDED_FOR');
}
else
if (getenv('HTTP_FORWARDED')) {
$ip = getenv('HTTP_FORWARDED');
}
else
if (getenv('REMOTE_ADDR')) {
$ip = getenv('REMOTE_ADDR');
}
else {
$ip = '127.0.0.1';
}
$activation = md5($email . time());
$sql = "INSERT into `users` (username, name, password, email, birthday, status, activation, ip, trn_date, theme, picture, views, currency, publicprofile, showaboutyourself, showviews, changedecimal, sortby, apiscrape, badge, sortfeed, who_can_pm_me, who_can_notify_me, google_auth_code) VALUES (?, ?, ?, ?, ?, ?, '0', ?, ?, ?, 'light', 'default.png', '1', 'USD', '1', '1', '1', '0', 'latestadded', 'coinmarketcap', 'USER', 'relevance', 'all', 'all', ?)";
$options = ['cost' => 15, ];
$pdw = password_hash($password, PASSWORD_BCRYPT, $options);
$stmt = $mysql->prepare($sql);
$stmt->bind_param('sssssssss', $username, $name, $pdw, $email, $date, $activation, $ip, $trn_date, $secret);
$stmt->execute();
// log them in after registration
$sql = "SELECT id FROM `users` WHERE email=?";
$stmt = $mysql->prepare($sql);
$stmt->bind_param('s', $email);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
$userid = $row["id"];
$usersession = substr("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", mt_rand(0, 50) , 1) . substr(md5(time()) , 1) . bin2hex(openssl_random_pseudo_bytes(25));
$now = time();
$expires = time() + 86400;
date_default_timezone_set('Europe/Amsterdam');
$date = date('d-m-Y');
$sql = "INSERT INTO sessions (SESSION_ID, SESSION_USER_ID, SESSION_IP, SESSION_EXPIRES, SESSION_DATE, SESSION_ACTIVE, SESSION_AGENT) VALUES (?,?,?,?,?,'1',?)";
$stmt = $mysql->prepare($sql);
$stmt->bind_param('ssssss', $usersession, $userid, $ip, $expires, $date, $_SERVER['HTTP_USER_AGENT']);
$stmt->execute();
setcookie("MySite_Session_ID", $usersession, $expires, '/');
header("Location: /");
die();
}
Login
if (isset($_POST['login'])) {
usleep(500000); // Slow down brute-forcers
$error = '0';
$email = $_POST['email'];
$password = $_POST['password'];
if (empty($email)) {
$error = '1';
$userError = "Please enter your email.";
}
if (empty($password)) {
$error = '1';
$passError = "Please enter your password.";
}
if ($error == '0') {
$sql = "SELECT id,password,status FROM `users` WHERE email=?";
$stmt = $mysql->prepare($sql);
$stmt->bind_param('s', $email);
$stmt->execute();
$result = $stmt->get_result();
$results = $result->num_rows;
if ($results >= 1) {
$row = $result->fetch_assoc();
if ($row['status'] != 0) {
if (password_verify($password, $row["password"])) {
// Password success, log them in
$userid = $row["id"];
$useragent = $_SERVER['HTTP_USER_AGENT'];
$usersession = substr("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", mt_rand(0, 50) , 1) . substr(md5(time()) , 1) . bin2hex(openssl_random_pseudo_bytes(25));
$now = time();
$expires = time() + 86400;
if (isset($_POST['rememberme'])) {
$expires = time() + 15768000;
}
date_default_timezone_set('Europe/Amsterdam');
$date = date('d-m-Y');
$sql = "INSERT INTO sessions (SESSION_ID, SESSION_USER_ID, SESSION_IP, SESSION_EXPIRES, SESSION_DATE, SESSION_ACTIVE, SESSION_AGENT) VALUES (?,?,?,?,?,'1',?)";
$stmt = $mysql->prepare($sql);
$stmt->bind_param('ssssss', $usersession, $userid, $_SERVER['REMOTE_ADDR'], $expires, $date, $_SERVER['HTTP_USER_AGENT']);
$stmt->execute();
setcookie("MySite_Session_ID", $usersession, $expires, '/');
header("Location: /");
} else {
$passError = "Invalid password.";
}
} else {
$userError = 'Your account has not been activated. Please check your email.';
}
}
}
}
Logout
$mysql = mysqli_connect('localhost', 'root', '');
mysqli_select_db($mysql,"mysite");
$sql = "DELETE FROM sessions WHERE SESSION_ID=?";
$stmt = $mysql->prepare($sql);
$stmt->bind_param('s', $_COOKIE['MySite_Session_ID']);
$stmt->execute();
setcookie ('MySite_Session_ID', "", 1);
setcookie ('MySite_Session_ID', false);
unset($_COOKIE['MySite_Session_ID']);
header("Location: /");
exit();
Check if they're logged in
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$mysql = mysqli_connect('localhost', 'root', '');
mysqli_select_db($mysql, "mysite");
date_default_timezone_set('Europe/Amsterdam');
if (isset($_COOKIE['MySite_Session_ID'])) {
$usersession = $_COOKIE['MySite_Session_ID'];
$sql = "SELECT SESSION_USER_ID,SESSION_EXPIRES FROM sessions WHERE SESSION_ID = ? AND SESSION_ACTIVE = '1' AND SESSION_AGENT = ?";
$stmt = $mysql->prepare($sql);
$stmt->bind_param('ss', $usersession, $_SERVER['HTTP_USER_AGENT']);
$stmt->execute();
$dasdasresult = $stmt->get_result();
$results = $dasdasresult->num_rows;
if ($results >= 1) {
// Is logged in
$row = $dasdasresult->fetch_assoc();
$userid = $row["SESSION_USER_ID"];
$expiretime = $row["SESSION_EXPIRES"];
if ($expiretime > time()) {
// Session did not expire
$sql = "SELECT * from users WHERE id=?";
$stmt = $mysql->prepare($sql);
$stmt->bind_param('s', $userid);
$stmt->execute();
$dasdasresultx = $stmt->get_result();
$results = $dasdasresultx->num_rows;
if ($results >= 1) {
// User exists
$row = $dasdasresultx->fetch_assoc();
$username = $row["username"];
$name = $row["name"];
$userid = $row["id"];
$email = $row["email"];
$loggedin = 1;
} else {
// User does not exist
header("Location: /login");
exit();
}
} else {
// Session expired
$sql = "UPDATE sessions SET SESSION_ACTIVE = '0' WHERE SESSION_ID = ?";
$stmt = $mysql->prepare($sql);
$stmt->bind_param('s', $usersession);
$stmt->execute();
header("Location: /login");
exit();
}
} else {
header("Location: /login");
exit();
}
} else {
header("Location: /login");
exit();
}
-
\$\begingroup\$ Might not see it that quickly, but have you thought about CSRF? \$\endgroup\$engineercoding– engineercoding2017年07月31日 21:12:21 +00:00Commented Jul 31, 2017 at 21:12
-
\$\begingroup\$ Hmm? Where do you mean? \$\endgroup\$R Records– R Records2017年08月01日 11:17:03 +00:00Commented Aug 1, 2017 at 11:17
-
\$\begingroup\$ Read up on CSRF here: owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF). For more information or implementations use Google ;p \$\endgroup\$engineercoding– engineercoding2017年08月01日 20:42:09 +00:00Commented Aug 1, 2017 at 20:42
-
\$\begingroup\$ Thank you. Yes I did take this in consideration, but not everywhere. I only put it in pages where I use GET, but not where I use POST because I believe they can only do a POST request by submitting a form, right? I tried submitting the form via URL and it never worked. \$\endgroup\$R Records– R Records2017年08月02日 01:31:32 +00:00Commented Aug 2, 2017 at 1:31
-
\$\begingroup\$ Programmatically everyone can do a POST request \$\endgroup\$engineercoding– engineercoding2017年08月02日 06:22:12 +00:00Commented Aug 2, 2017 at 6:22
2 Answers 2
Security
- make sure your site is running over HTTPS.
- add
httponly
parameter to setcookie() - you'd like to check whether
openssl_random_pseudo_bytes()
's result was cryprographically strong, using the function's 2nd parameter
Some random nitpicks
$usersession = substr("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", mt_rand(0, 50) , 1) . substr(md5(time()) , 1)
This code is rather silly and could be safely discarded.
"Name can only contain alphabets and spaces.";
Suppose your site won't be overly popular in Ireland.
Yours, Patrick O'Neal
if (getenv('HTTP_CLIENT_IP')) {
Apart from the fact that registration IP is rather useless as is, the code is one of old PHP users' superstitions and should never be used. Any IP related stuff should be taken from REMOTE_ADDR only, not from various HTTP headers.
if (empty($email)) {
It makes no sense to use empty() for the previously defined variable. if (!$email) {
would be enough.
$sql = "SELECT id FROM `users` WHERE email=?";
mysqli_insert_id()
ini_set('display_errors', 1);
Should be 0 on a live site
"UPDATE sessions SET SESSION_ACTIVE = '0' WHERE SESSION_ID = ?";
I don't see much point in the SESSION_ACTIVE field, as it's just duplicating the timeout
setcookie ('MySite_Session_ID', "", 1); setcookie ('MySite_Session_ID', false);
There should be only one call and parameter mach ones with setting a session cookie, otherwise it won't be removed
unset($_COOKIE['MySite_Session_ID']);
this call is useless at the end of the script
$xsql = "SELECT * FROM users WHERE email=?"; $xstmt = $mysql->prepare($xsql); $xstmt->bind_param('s', $_POST['email']); $xstmt->execute(); $xresult = $xstmt->get_result(); $xresults = $xresult->num_rows;
Did you notice that you are repeating this kind of code all the time? There is one programming concept which is often underestimated by PHP users. It is called user-defined functions. What about calling a code like this:
$sql = "SELECT count(*) FROM users WHERE email=?";
$xresults = getSQLResult($mysql, $sql, $_POST['email']);
-
\$\begingroup\$ Thank you so much for the detailed reply, I appreciate your time. I did almost everything you said except the first one with the generating the usersession. It works fine and I don't see anything wrong with it? \$\endgroup\$R Records– R Records2017年07月30日 16:12:24 +00:00Commented Jul 30, 2017 at 16:12
-
\$\begingroup\$ It's just useless \$\endgroup\$Your Common Sense– Your Common Sense2017年07月30日 19:31:37 +00:00Commented Jul 30, 2017 at 19:31
-
\$\begingroup\$
REMOTE_ADDR
comes from headers as well. perhaps you should explain exactly why that header is safe as opposed to others. \$\endgroup\$I wrestled a bear once.– I wrestled a bear once.2017年07月30日 19:38:47 +00:00Commented Jul 30, 2017 at 19:38 -
\$\begingroup\$ @Iwrestledabearonce. A) Does it really come from a header? webmasters.stackexchange.com/questions/49962/… B) It can be faked but is significantally more difficult to fake as the browser doesn't send it. (See link A) stackoverflow.com/q/5092563/1310566 \$\endgroup\$Simon Forsberg– Simon Forsberg2017年07月30日 21:54:13 +00:00Commented Jul 30, 2017 at 21:54
-
\$\begingroup\$ @YourCommonSense and SimonForsberg - I used Google to find this very helpful video to explain how the server gets the IP from the packet headers. It is not difficult to fake with low level languages but its generally pointless to do so as that would cause the server to send the response to the wrong machine. youtube.com/watch?v=PpsEaqJV_A0 \$\endgroup\$I wrestled a bear once.– I wrestled a bear once.2017年07月31日 02:24:09 +00:00Commented Jul 31, 2017 at 2:24
All code must be formatted and indented properly, otherwise you scare off potential readers. There are enough other things to worry about when reviewing code, so don't distract your readers.
So instead of this:
function abc() {
if (condition) {
if (condition2) {
action();
}
}
}
The code must look like:
function abc() {
if (condition) {
if (condition2) {
action();
}
}
}
So when you say you beautified your code, did you actually do it, or did your IDE do it? It should always be the IDE for basic things like indentation and spacing. The first few lines of your code look very inconsistent in this regard.
-
\$\begingroup\$ Surely you meant this as a comment, aren't you? \$\endgroup\$Your Common Sense– Your Common Sense2017年07月30日 10:36:32 +00:00Commented Jul 30, 2017 at 10:36
-
\$\begingroup\$ No, I didn't. As a reviewer I expect a certain minimum quality of the code. I'm not willing to review obfuscated code, and I consider improper indentation to be exactly that. See also
goto fail
. \$\endgroup\$Roland Illig– Roland Illig2017年07月30日 16:50:25 +00:00Commented Jul 30, 2017 at 16:50 -
\$\begingroup\$ Obfuscated code? I beautified the code and put it in a code tag. What else needs to be done? \$\endgroup\$R Records– R Records2017年07月31日 01:35:16 +00:00Commented Jul 31, 2017 at 1:35
-
\$\begingroup\$ I updated my answer. I hope it is clearer now. \$\endgroup\$Roland Illig– Roland Illig2017年07月31日 06:51:07 +00:00Commented Jul 31, 2017 at 6:51