I think this is better than about 99% of first tries I've ever seen!
Anyway, I've tried my best to address as many things as I saw, whether major or extremely minor. If you have any questions on any points or want to clarify or defend any claims, let me know and I will expand the answer.
Assuming array indexes exist
Any array that comes from user input should never be directly accessed. $_POST['key']
is not safe since you can't know for sure whether or not the user supplied the $_POST['key']
.
With arrays that can contain null
values, you must use array_key_exists
, but since input arrays like $_POST
and $_GET
will always have either strings or arrays, you can safely use isset
.
The following is how I tend to access request driven arrays:
$username = (isset($_POST['username']) && is_string($_POST['username'])) ? $_POST['username'] : null;
if (strlen($username) < 5) {
//username's must be >= 5 charactes
} else if (strlen($username > 32)) {
//username's must be < 32 characters
}
//If you wanted to check if the user even provided a username:
if ($username === null) {
//A username element of the form was not provided. Depending on specific context, this may imply form tampering.
}
Note that the is_string
check is necessary so that the user cannot maliciously pass an array to the script. If that were to happen, then the strlen
call would complain about not expecting an array. Unfortunately you have to be rather paranoid about anything coming from users.
There's a longer discussion of this here in the last section.
It's also worth noting that some people prefer a filter_input approach.
SQL injection
There is no SQL injection possible in your code, but that may be by accident. When putting strings into queries, it's good practice to always run them through mysql_real_escape_string
. That way if you change your business rules in a way that does make SQL injection possible, you don't have to worry about the change cascading down to your queries and opening a security hole.
More information about SQL injection can be found in a post I wrote the other day, here.
(Sorry if I'm assuming that you don't know about SQL injection and you do.)
MySQL Extension
The mysql_*
extension is quite dated, and PDO offers many advantages.
- Offers a cleaner API
- Offers better error handling (the ability to have failed queries throw exceptions is wonderful)
- Handles transactions SQL-dialect agnostically (for the most part)
- Different RDBMS can be changed out relatively easily (switching from
mysql_*
to PostreSQL is extremely painful. Switching from PDO with MySQL to PDO with PostreSQL isn't pleasant, but is care is taken to stay as dialect-agnostic as possible, it's reasonably easy)
- Prepared statements
You can find more information here under the PDO section.
<pre>
pre
can be used to arrange spacing, however you should be aware that it typically uses a fixed width font, and there's a few other quirks with it (you can probably style it to use a different font though). I suppose there's technically nothing wrong with it, but pre
doesn't strike me as a good long term method for arranging elements.
ctype_alnum for a password
Why restrict users to a a-zA-Z0-9
password? I'm a firm believe that passwords should be allowed to be just about anything. Some restrictions may be in order, such as ASCII only or something of that nature, but if a user wants his password to be this is my password
, then why not? You're hashing it anyway, so the length doesn't matter. By the same logic, why not allow non alpha numeric characters? It's not like HTTP or PHP will have any issues handling things like !@#$%^&*()|_+=-
and so on.
list column fields in inserts
$query = "INSERT INTO userlogin VALUES('','$username','$password')";
Listing columns explicitly allows for better maintainability and clarity. What if in the future you add a birthday column after the username column? Suddenly you're trying to insert passwords into the birthday column. Also, if you're going to put an auto incrementing column in a query, it should be null.
I would write the query like this:
$query = "INSERT INTO userlogin (username, password) VALUES ('$username','$password')";
userlogin
This sounds like a logging table to me. This sounds like it contains records of users logging in, not of actual user records. I would consider renaming this table to something like users
or user_accounts
.
escape html where necessary
echo "Thanks ".$username.", your account has been created!";
Once again, since it's limited to alphanumeric, this is definitely harmless, however, preparing for the future never hurts. If at some point you allow users to create names that contain &
, this could output invalid html.
(Once again, sorry if I'm telling you something you already know.)
always use brackets
It's hard to see on a short script with fairly simple logic, but I'm a firm believer that braces should always be used.
if {} else { statement }
Looks clearer and is less error prone than:
if {} else statement
html element's attributes should always be in quotes
value=yes
should be value="yes"
MD5
Hashing algorithms should be computationally expensive enough that a brute force attack is infeasbile. Due to rainbow tables and other factors, a single MD5 run of an alphanumeric password is not considered secure.
Consider using something like bcrypt that is a bit heavier algorithm, or at least use something like 512 bit SHA repeated a thousand (or more!) times.
Let f
be your hashing function. Let S
= the set of all possible passwords. The absolute worst case time to brute force a password, p
, such that p
is in S
is size of S * time for f(x)
If f = MD5
then the time is very small. MD5 and even SHA are considered fairly fast hashes. If you use a an algorithm such as one of those, you should at least repeat it. This way f
becomes a much longer operation.
There's a few really good posts on StackOverflow about this, so I'll try to find some and link them.
This is a pretty good explanation (thanks to luiscubal for finding the link).
More graceful error handling
For the errors, I would probably handle them a bit better than the old or die
construct that tends to go along with the mysql_
functions.
The idiom I would follow (though I would use PDO with exceptions), would be something like:
$query = "...";
$resource = mysql_query($query);
if (!$resource) {
//log the error including all necessary details
//output a fairly vague error the user
}
trigger_error
can be useful for this.
Error handling suggestion
I like to validate everything separately and independently such that all errors can be output at once. For example, if my username is only 4 characters and my passwords don't match, it would be nice to see both errors at once instead of having to fix one error, resubmit the form, fix the second error then resubmit the form.
This is by no means perfect, but the structure I typically use for error handling is:
$errors = array();
$username = $password = null;
if ($_SERVER['REQUEST_METHOD'] === "POST") {
$username = ...;
$password = ...;
if (strlen($username) < 5) {
$errors['username'][] = "Usernames must be 5 or more characters";
}
if (strlen($username) && !ctype_alnum($username)) {
$errors['username'][] = "Usernames must be alphanumeric";
}
if (strlen($password) < 5) {
$errors['password'][] = "Passwords must be 5 or more characters";
}
//More validations
if (!count($errors)) {
//No errors, so $username and $password are valid
$insertionQuery = sprintf("INSERT INTO users (username, password) VALUES ('%s', '%s')", mysql_real_escape_string($username), mysql_real_escape_string($password));
$insertionResult = mysql_query($insertionQuery);
if (!$insertionResult) {
trigger_error("MySQL query failed. Query: [{$insertionQuery}]. Error: [" . mysql_error() . "]", E_USER_WARNING);
$errors['db'][] = "There was an unexpected error. Please try again.";
}
}
}
Then, to render the errors, I typically just create a <ul>
with each error as a <li>
:
if (count($errors)) {
echo '<ul>';
foreach ($errors as $element => $errs) {
foreach ($errs as $err) {
echo '<li>' . htmlspecialchars($err) . '</li>'
}
}
echo '</ul>';
}
The element name being the array key is useful because it means you can do things like style elements a certain way if they had an error:
<input type="text" name="username" value="<?php echo htmlspecialchars($username);"<?php if (array_key_exists('username', $errors)) { echo ' class="error"'; } ?>>
You could even take the idea farther and render errors around elements.
In general, it's a lot more flexible to aggregate the errors instead of immediately outputting them.
This philosophy touches into the area of MVC and separation of concerns. MVC isn't particularly important for now, but the general idea of it (and in part SoC) is that each area of code should be concerned with one specific thing and nothing else.
A slight extension of this is the idea that logic and presentation should be separate. Doing this allows many more degrees of freedom, and typically better code. Consider for example if you wanted to have an AJAX interface for submitting the form, but still have an HTML fallback for users with JavaScript disabled. If you were storing errors, you could do something simple like:
if (!empty($_POST['ajax'])) {
echo json_encode(array('result' => 'errors', 'errors' => $errors));
//set some kind of flag so that html output will not happen
//(or, if it's a small script, I might just use `exit;`)
}
//Then later, if it's not AJAX
//render the errors as HTML inline in the form
If you intermix your logic and presentation code, the code is forever stuck rendering in exactly one way. There's no way to reuse the same logic with a different presentation. If they are separated, however, then the same logic can be used with infinite presentations.
Variable reuse
I'm definitely in the minority on this one, but I don't like to reuse variable names. I would call the repeated query and result variables something like $existsQuery
and $insertionQuery
. Nothing wrong at all with reusing the same name, just a personal preference (though the more descriptive a name, the better [within reasonable limits]).
header.php
This is not a very descriptive name. I would consider calling it something like database.php
or connect.php
or something. When I think of headers, I think content, not code.
Also, since the database connection is required for the page to function, require
should be used instead of include
. A failed include will allow execution to continue, whereas a failed require will kill execution.
mysql_select_db
This should be with your database connection code.
login.php
No idea what "Login.php defines database log in numbers" means? I'm a bit confused though about what this could be doing. Also not sure why require_once
instead of require
?
()
after one of the mysql_error calls. Also,pre
should typically be avoided for styling. Also, everything other than the first section of this applies to your question: codereview.stackexchange.com/questions/12647/… \$\endgroup\$