8
\$\begingroup\$

I would like to know if my registration system is secure. I'm not sure if it is secure, as I'm new in development, and I'm afraid that the system is flawed

Also, I'm not experienced in object oriented programming, I would like to know if my script looks a bit like OOP or is simply a set of procedural programming with object oriented

database.php

<?php

/**
 * Class responsible for connecting to the database
 * It will be inherited by the SignUp class
 */
class DatabaseConnection
{
    public function __construct(string $host, string $dbname, string $username, string $password)
    {
        try{
            $this->database = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $username, $password);
        }catch(PDOException $e){
            throw new Exception($e->getMessage());
        }
    }
}

registration.php

<?php

/**
 * This is a registration script
 * @author Anne Batch
 * @copyright 2021
 */

declare(strict_types = 1);

class SignUp extends DatabaseConnection
{
    /**
     * Database Connection
     * @var PDO
     */
    private $database;

    /**
     * @var string
     */
    private $name;

    /**
     * @var string
     */
    private $email;

    /**
     * @var string
     */
    private $passwordHash;

    /**
     * @var string
     */
    private $signUpPage;
    
    /**
     * @param string $name Assign the username
     */
    public function setName(string $name)
    {
        $this->name = $name;
    }

    /**
     * @param string $email Assign the email
     */
    public function setEmail(string $email)
    {
        $this->email = $email;
    }

    /**
     * @param string $password & $confirm Assign password and password confirmation
     */
    public function setPassword(string $password, string $confirm)
    {
        if ($password === $confirm) {
            $options = [
                'cost' => 12,
            ];
            $this->passwordHash = password_hash($password, PASSWORD_BCRYPT, $options);
        }
        else {
            $this->returnWithError('<div class="alert alert-danger" role="alert">Passwords does not match!</div>');
        }
    }

    /**
     * @param $signUpPage Redirect Page
     */
    public function signUpPage(string $signUpPage)
    {
        $this->signUpPage = $signUpPage;
    }

    /**
     * @param $errorMessage Notification Message
     */
    private function returnWithError(string $errorMessage)
    {
        $_SESSION['error'] = $errorMessage;
        header('Location: ' . $this->signUpPage);
        exit();
    }

    /**
     * Checks whether a name exists
     */
    public function existingName(): void
    {
        $statement = $this->database->prepare("SELECT 1 FROM users WHERE username = :username");
        $statement->execute([
            ":username" => $this->name
        ]);
        $exists = $statement->fetchColumn();

        if(!$exists) {
            $this->returnWithError('<div class="alert alert-danger" role="alert">Name already exists!</div>');
        }
    }

    /**
     * Checks whether an email exists
     */
    public function existingEmail(): void
    {
        $statement = $this->database->prepare("SELECT email FROM users WHERE email = :email");
        $statement->execute([
            ":email" => $this->email
        ]);

        $result = $statement->rowCount();

        if($result != 0) {
            $this->returnWithError('<div class="alert alert-danger" role="alert">Email already exists!</div>');
        }
    }

    /**
     * Checks if the fields have been filled
     */
    public function isDefinedFields(): void
    {
        if(!$this->name || !$this->email) {
            $this->returnWithError('<div class="alert alert-danger" role="alert">Please, fill out all the fields!</div>');
        }
    }

    /**
     * Checks if the username is less than 3 or greater than 50
     */
    public function ValidateNameLength(): void
    {
        if(mb_strlen($this->name) < 3) {
            $this->returnWithError('<div class="alert alert-danger" role="alert">Can only use 3 or more characters as name!</div>');
        }

        if(mb_strlen($this->name) > 50) {
            $this->returnWithError('<div class="alert alert-danger" role="alert">Can only use a maximum of 50 characters as name!</div>');
        }
    }

    /**
     * Checks if the email is semantically correct
     */
    public function ValidateEmail(): void
    {
        if(!filter_input(INPUT_POST, $this->email, FILTER_VALIDATE_EMAIL)) {
            $this->returnWithError('<div class="alert alert-danger" role="alert">Incorrect email!</div>');
        }
    }
    
    /**
     * Checks if the username matches the regular expression
     * The purpose of regular expression is to prevent JavaScript attacks
     * The user will not be able to use certain characters, such as <> \/
     */
    public function addUser(): void
    {
        if(preg_match("/^[\w&.\-]*$/", $this->name)) {
                $statement = $this->database->prepare("INSERT INTO users VALUES(?, ?, ?, ?, ?, ?, ?)");
                $statement->execute([
                    $this->name,
                    $this->email,
                    $this->passwordHash,
                    $_SERVER['REMOTE_ADDR'],
                    $_SERVER['HTTP_CLIENT_IP'],
                    $_SERVER['HTTP_X_FORWARDED_FOR'],
                    $_SERVER['HTTP_USER_AGENT']
                ]);
                if($statement) {
                    $this->returnWithError('<div class="alert alert-success" role="alert">Registered successfully!<br>Login now</div>');
                }
            }  
        else {
            $this->returnWithError('<div class="alert alert-danger" role="alert">Can only use letters and numbers as name!</div>');
        }
    }
}

main.php

<?php

session_start();

require_once('database.php');
require_once('registration.php');

$users = new SignUp('localhost', 'database', 'root', 'password');
$users->setName($_POST['username']);
$users->setEmail($_POST['email']);
$users->signUpPage('signup');
$users->setPassword($_POST['password'], $_POST['password2']);
$users->existingName();
$users->existingEmail();
$users->isDefinedFields();
$users->ValidateNameLength();
$users->ValidateEmail();
$users->addUser();
\$\endgroup\$
4
  • 2
    \$\begingroup\$ You question has gained a close vote for "missing review context", which doesn't make much sense since you've posted what looks like some pretty complete code. Please can you add a short description on what the code does, otherwise your question looks fine to me. \$\endgroup\$
    – Peilonrayz
    Commented Feb 23, 2021 at 21:40
  • 3
    \$\begingroup\$ The code looks really solid to me. Up to now, the worst mistake I found is the simple typo "Passwords does not match", which should be "Passwords do not match". If there is nothing else, this can only mean that I'm too tired right now. I'll try again tomorrow. For someone "new in development" and "not experienced", the code definitely contains too few mistakes. ;) \$\endgroup\$ Commented Feb 23, 2021 at 22:28
  • \$\begingroup\$ @RolandIllig If you can check again, I'll be happier than I already am. Thanks for your feedback! \$\endgroup\$
    – Warlock
    Commented Feb 24, 2021 at 0:24
  • \$\begingroup\$ You've gone through the trouble of creating setter methods so I'm interested: what's the logic behind not incorporating the validation in those methods? \$\endgroup\$
    – Steven
    Commented Feb 25, 2021 at 10:34

3 Answers 3

6
\$\begingroup\$

This code is rather good if you look at each separate line, but rather bad from the organizational standpoint.

Each separate code snippet is indeed nearly flawless but it's the way they are glued together is wrong. SignUp is, so to say, a "Diety Object" as it's not as omniscient as a God Object, but definitely it does way too much:

  • connects to a database
  • validates input
  • directly interacts with a database
  • directly interacts with HTTP client
  • it formats the output

All these tasks must be delegated to separate classes or even components.

You need to learn about separation of concerns and some basic MVC. Some pointers to get it right:

  • a signup process is not a database connection. You never extend the former from the latter, just like you never extend a Human class from a Potato class in order to make a Human to eat a Potato. Besides, what if your page will require some other database interaction and use another class extended from database connection? It will create two database connections. And another class and so on. Which will eventually lead to the infamous "Too many connections" error. A connection has to be made only once and then passed to all classes as a constructor parameter
  • although the signup class can validate the data itself, most of the validation methods must be moved into a separate class for the code reuse. Also, it makes a little sense to call separate methods for validation. It will take less writing and lead to less errors if all validations would be performed in the set methods. Just like setPassword() for some reason does not following the other methods' example.
  • either way, the validation code should never format the HTML output. It is not only makes a designer to dig into your PHP code to change one class to another but also a severe case of a code repetition. Imagine you'd like to add some fancy icon to your flash messages. Going to go though all the PHP code changing it in hundreds lines, seriously?
  • neither should it otherwise interact with a client. the validation code should only return a user interactions error while it should be conveyed to a user by some other mechanism
  • the actual insert query can be made into a separate class but can be kept in this one. Either way, it must be fed with all the data required, not trying to get some data magically from a superglobal array. Remember that at some point you'd want to register users from a command line utility where no variable like ['HTTP_CLIENT_IP'] will ever be present
  • I am sure you already can feel uneasiness when calling signUpPage method which is absolutely alien to all the other code. There must be another class responsible for the HTTP interaction to which this method would belong
  • all the output must be formatted in a completely different component called "View". It should have predefined templates for this kind of flash messages and the job of the HTTP component is just to define the message type and text

I would strongly suggest to look at some established PHP frameworks, namely Symfony or Laravel, in order to see how the prosess is done and how the matters are usually get separated from each other.

The refactored version could be like this

<?php

session_start();

require_once 'autoload.php';

$http = new HTTPTransport();
$validator = new Validator();
$database = new DatabaseConnection('localhost', 'database', 'root', 'password');
$user = new SignUp($database, $validator);

try {
    $user->setName($_POST['username']);
    $user->setEmail($_POST['email']);
    $user->setPassword($_POST['password'], $_POST['password2']);
    $user->setUserAgent($_SERVER['HTTP_USER_AGENT']);
    // same for the IP address and related HTTP headers
} catch (SignUpValidationError $e) {
    $http->setFlashMessage('error', $e->getMessage());
    $http->redirect('signup');
}
$user->addUser();
$http->setFlashMessage('success', 'Registered successfully!');
$http->redirect('login');

which implies that your set methods validate the data like this

public function setEmail(string $email)
{
    if (!$this->validator->validateEmail($email)) {
        throw new SignUpValidationError('Incorrect email');
    }
    if ($this->findUserByEmail($email)) {
        throw new SignUpValidationError('Email already exists');
    }
    $this->email = $email;
}
\$\endgroup\$
10
  • \$\begingroup\$ Thank you so much, it looks perfect. The only thing I didn't understand was block catch, my IDE(VS Code) reports: Undefined type 'SignUpValidationError'.intelephense. What, after all, is SignUpValidationError, judging by the setEmail method, seems to be a method? I created a method with that name in the Validator class and the error continues \$\endgroup\$
    – Warlock
    Commented Feb 25, 2021 at 18:04
  • 2
    \$\begingroup\$ SignUpValidationError is a class. That's how exceptions work. You can define your own exceptions php.net/manual/en/language.exceptions.extending.php with the purpose of making catch to catch only a certain kind of exception \$\endgroup\$ Commented Feb 25, 2021 at 18:19
  • \$\begingroup\$ I'm stuck with the methods that validate the data. What is within those methods that you reference? You throw an exception, but I can't understand it, I don't know if I should do something like if (condition) { // check if email exists and do nothing because we'll use exception}. Please help me, I can't understand \$\endgroup\$
    – Warlock
    Commented Mar 2, 2021 at 15:38
  • \$\begingroup\$ @AnneBatch validateEmail is simply one line return filter_var($email, FILTER_VALIDATE_EMAIL); but I don't really understand your question. Can you post your code here so I can test it? \$\endgroup\$ Commented Mar 2, 2021 at 15:43
  • 1
    \$\begingroup\$ Like I said, validateEmail should just return the result. findUserByEmail shouldn't be a part pf the Validator class but a Signup class. SignUpValidationError should be a class, not a method \$\endgroup\$ Commented Mar 2, 2021 at 16:01
6
\$\begingroup\$

First of all, you really deserve to be commended for following best practices in PHP programming. You really want to learn how to develop websites properly and your effort shows.

The code which you have shown us already looks amazing and you are on the right track. However, there are still a lot of things that you could improve. Let's review some of them.

Database class

PDO is a built-in class for all your database needs. You don't really need another class to wrap PDO around. However, if you want to make your development easier you could use one of the popular classes that abstract PDO further, e.g. EasyDB. You definitely do not need an empty DatabaseConnection class.

I would also recommend to use fully-qualified names whenever possible. It helps to avoid accidental name clashes.

Inheriting from the database class

This is a big no. Your SignUp class is not a special instance of DatabaseConnection. It needs a database connection, but it is not a type of database connection class. Inheritance should narrow the type of object.

Error reporting

Since PHP 8.0 PDO has error reporting always enabled, but as a best practice I would recommend to enable error reporting explicitly and disable emulated prepares.

When we fix these problems, this is what is left:

try {
    $pdo = new \PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $username, $password, [
        \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
        \PDO::ATTR_EMULATE_PREPARES => false
    ]);
} catch (\PDOException $e) {
    throw new \PDOException($e->getMessage(), (int) $e->getCode());
}

Comments

This is always a controversial topic. Code comments are meant to help you or any other developer who reads the code after you understand the difficult parts of the code. But if your code has difficult non-self-explanatory code then you should refactor it. This should result in keeping comments in code to the absolute minimum. Use self-descriptive variable/class/function names. Don't make your functions/classes too complex. Keep it all simple.

This also applies to PHPDoc blocks. The code should be written in such a way that you shouldn't need them either.

Comments are bad because they often go stale. You will change the code, but you forget to update the comments. Comments often tend to make an idiot of the developer, and that isn't always the fault of the person who wrote the comment.

Consider this example:

/**
 * Database Connection
 * @var PDO
 */
private $database;

The comment tells me that this property holds a database connection and is of type PDO. This is not a lie, but we could convey exactly the same information using pure code.

private \PDO $databaseConnection;

Some of your comments are useful, but some of them are not:

Checks if the fields have been filled

What fields? The method doesn't take any arguments and it doesn't return any values. The comment fails to explain the behaviour of that method.

This is almost a good comment:

/**
 * Checks if the username matches the regular expression
 * The purpose of regular expression is to prevent JavaScript attacks
 * The user will not be able to use certain characters, such as <> \/
 */

It aims to explain a regular expression, which are always difficult to read. However, what attacks does it prevent? How does it prevent them? I assume you meant XSS, but this is not good protection against XSS. It can't be a JavaScript injection either. What is the purpose of the regex then?

The Signup class

This class does too much. It is not really object-oriented. While it is difficult to have true OOP in MVC programming, this class is especially bad at it. You call each method procedurally one after another. What we can do is split up the functionality into separate classes and then refactor this class.

Password class

Password hashing functionality should really be a separate class. In your example, you could create a simple class like this one:

class Password
{
    const ALGO = PASSWORD_BCRYPT;

    const OPTIONS = [
        'cost' => 12,
    ];

    public static function hash(string $password): string
    {
        if ($password === '') {
            throw new \RuntimeException('Password cannot be empty.');
        }

        return password_hash($password, self::ALGO, self::OPTIONS);
    }
}

The advantage of having a separate class is that you can extend it with more functionality later on. For example, checking the password strength. (Warning: do not add arbitrary restrictions, do not force people to use certain character classes. Good password should be of at least a certain length with no maximum and should not appear in any leaked password lists). I have written in the past a very similar class:

<?php

namespace Security;

use Curl\Curl;
use Curl\CurlException;

class Password
{
    const ALGO       = \PASSWORD_DEFAULT;
    const OPTIONS    = ['cost' => 12];
    const MIN_LENGTH = 8;

    public function hash(string $password): string
    {
        if ($password === '') {
            throw new \RuntimeException('Password cannot be empty.');
        }

        return \password_hash($password, self::ALGO, self::OPTIONS);
    }

    public function needsRehash(string $hash): bool
    {
        return \password_needs_rehash($hash, self::ALGO, self::OPTIONS);
    }

    public function checkIfStrongPassword(string $password): bool
    {
        try {
            $count = $this->getHashCountFromAPI($password);
        } catch (CurlException $e) {
            error_log('Could not verify password quality.');
            return \mb_strlen(\count_chars($password, 3)) >= self::MIN_LENGTH;
        }
        return \mb_strlen($password) >= self::MIN_LENGTH && $count < 1;
    }

    private function getHashCountFromAPI(string $password): int
    {
        $hash   = \strtoupper(\sha1($password));
        $first5 = \substr($hash, 0, 5);
        $rest   = \substr($hash, 5);
        $rq     = new Curl('https://api.pwnedpasswords.com/range/'.$first5);
        foreach (\explode("\r\n", $rq->exec()) as $line) {
            [$secondPartOfHash, $count] = \explode(':', $line);
            if ($rest === $secondPartOfHash) {
                return $count;
            }
        }
        return 0;
    }
}

UserAccount class

I believe that if we use classes we should strive to follow the OOP concept. In OOP, classes should represent entities that have certain properties and certain actions. Here, we are trying to create a new user account so we should have such class that represents an account. This class can have setters as actions that would validate each input. For example:

<?php

declare(strict_types=1);

class UserAccount
{
    private string $name;
    private string $email;
    private string $passwordHash;

    public function __construct(public Validator $validator)
    {
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setEmail(string $email): void
    {
        if (!$this->validator->isEmail($email)) {
            throw new \RuntimeException('Incorrect email!');
        }
        $this->email = $email;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setPassword(string $password, string $confirm): void
    {
        if ($password === $confirm) {
            $this->passwordHash = Password::hash($password);
        } else {
            throw new \RuntimeException('Passwords does not match!');
        }
    }

    public function getPasswordHash(): string
    {
        return $this->passwordHash;
    }
}

For more information on how to properly validate input and report errors see Your Common Sense's answer

The new Signup class

Ok, so now that we have split the responsibility of this class into smaller classes what is left to do in this class? As the name suggests, it should have a method that takes a user as an argument and saves it into the database.

<?php

declare(strict_types=1);

class SignUp
{

    public function __construct(private PDO $databaseConnection)
    {
    }

    public function addUser(UserAccount $user): void
    {
        $statement = $this->databaseConnection->prepare("INSERT INTO users (name, email, password, ip, client_ip, forwarded_ip, user_agent) VALUES(?, ?, ?, ?, ?, ?, ?)");
        $statement->execute([
            $user->getName(),
            $user->getEmail(),
            $user->getPasswordHash(),
            $_SERVER['REMOTE_ADDR'],
            $_SERVER['HTTP_CLIENT_IP'],
            $_SERVER['HTTP_X_FORWARDED_FOR'],
            $_SERVER['HTTP_USER_AGENT']
        ]);
    }
}

Cool, but what about checking if the username or email already exists in the database? Answer: don't! You should set the field in the database table to be unique if you want to prevent duplicate entries. I didn't include methods to check for their existence because when you try to insert a duplicate then PDO will throw an exception. You can look for that in your error handler and inform the user that this username is taken.

Main file

<?php

declare(strict_types=1);

spl_autoload_register();

try {
    $pdo = new \PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $username, $password, [
        \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
        \PDO::ATTR_EMULATE_PREPARES => false
    ]);
} catch (\PDOException $e) {
    throw new \PDOException($e->getMessage(), (int) $e->getCode());
}

$validator = new Validator(); // unspecified class

$newUser = new UserAccount($validator);
$newUser->setName(filter_input(INPUT_POST, 'name'));
$newUser->setEmail(filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL));
$newUser->setPassword(filter_input(INPUT_POST, 'password'), filter_input(INPUT_POST, 'password2'));

(new Signup($pdo))->addUser($newUser);

Other points

  • Always specify all your columns in the INSERT statement.
  • I don't understand why do you maintain IP addresses as part of your user record. Consider rethinking this approach.
  • Return early or use exceptions for erroneous situations. Do not use custom handling like you did as this is very messy and against best practices.
  • Do not check for return value of PDOStatement::execute(). If the query fails then PDO will throw an exception.
  • Use PSR-4 and autoloader. I recommend using Composer's autoloader but if you want something simpler you can write your own one in a few lines of code or use the built-in one.
  • Session handling requires a little bit more logic than session_start(), but that's a completely different topic.
\$\endgroup\$
4
\$\begingroup\$

First of all, your code looks rock-solid, obvious in what it does and very clean. This is not something that I would expect from someone "new in development". Whoever you learned all this from, you should keep them.

You got all of the major topics of a login system right:

  • Your code uses parameterized statements for all database access, to protect against SQL injections.
  • Your code saves passwords only in their hashed form, to protect against data breaches.
  • You use a decent password hashing algorithm. Good that PHP provides a sane default here and makes it easy to use. Throughout its long history, PHP had not always made it easy to write correct and secure code. For password hashing it succeeded to provide a good API, and you use it perfectly as well.
  • You use the full available character set from Unicode by connecting to the MySQL database using the utf8mb4 encoding. This allows emojis in user names 🙂 to be saved correctly.
  • All HTML that is printed carefully avoids to reflect any user-provided data. This prevents cross-site scripting.

Next, I'm looking at the details of the code, from top to bottom, see if there are any unnecessary stylistic variations.

database.php

For me, the comment on the DatabaseConnection is redundant. The name of the class already expresses clearly that the DatabaseConnection connects to the database. Therefore I would not need that comment. Since you are still new in development, it's perfectly fine to keep that comment until you (and whoever else works on your project) don't need it anymore because it's obvious.

Same for the will be inherited part in the comment. My IDE lets me quickly see the type hierarchy of classes, which makes this comment redundant as well. Plus, will be refers to the future and is probably outdated by now.

The constructor of DatabaseConnection initializes the field $this->database, but when looking at the class DatabaseConnection alone, that field is not used anywhere. This feels strange at first. I'm somehow missing the private $database declaration, like you wrote it in the class SignUp.

Still in the constructor of DatabaseConnection, you wrote try{ without a space in between. It is more common to write try { with a space in between, just like there is a space after if and catch. If you are using an IDE, it has a command to "format the source code", so you don't have to do that yourself. There are many different styles of formatting the code, it doesn't matter which one you choose. Just choose one that you like and apply it consistently to all of the code.

Still in the constructor of DatabaseConnection, when I first saw the code, I didn't see a reason for the try catch block at all. If you remove that block and only keep the single line $this->database = ... instead, the original stack trace will be preserved, including all information about the actual cause of the exception. By wrapping the PDOException in a plain Exception and only copying the message, you give away this context information. So far for my first impression.

The original stack trace contains a lot of information that is useful for tracking down errors. However, it also contains the database credentials, and these must not end up in the log files of the server. Therefore stripping the exception from all unnecessary information is essential in this case. To alert future readers of the code, I would add a comment explaining this in a single line, or just referring to a Stack Overflow answer with all the details.

registration.php

The comment This is a registration script is redundant. Keeping the author information is good if you plan on passing the project to someone else later, so that they have someone to contact. Whether or not the copyright 2021 is necessary or helpful is something I cannot decide for you.

Declaring strict_types=1 is good style, you should add it to database.php as well.

The class SignUp has no comment above it. This leaves a reader a bit unclear about what a SignUp really is. It might be "all data that is collected on the sign-up page", or it might be "the data that is actually required to perform a sign-up operation". The difference between these two is that the former would include the password confirmation field, but the latter would not.

You declare SignUp extends DatabaseConnection, which sounds strange. It's not that "every sign-up is a specialized database connection", but that's what the keyword extends usually means. Instead, it's more likely that the class SignUpPage would use a database connection. Therefore I would drop the extends DatabaseConnection from the class SignUp.

It's a nice trick that by declaring private $database in the class SignUp, you make that previously undeclared field of the class DatabaseConnection visible. Having tricks like these in the code may be confusing for readers, or they may even expect them. Instead of this trick, I would rather declare a field private $database of type DatabaseConnection and initialize this field in the constructor.

I like the field name $passwordHash since it makes it obvious that the password is never stored in its plain text form. I hope that you use that field in exactly this way, I haven't looked at the actual code yet. But even if at some place there were some code that read $passwordHash = $_POST['password'], this would immediately alert any careful reader that something is wrong here, which is a good second-line defense.

I'm not sure what the purpose of the field $signUpPage is. It could be a complete URL like https://localhost:3000/signup, it could be the HTML that makes up the sign-up page, or it could be the simple word signup, as in your code. A more precise name would be $relativeSignUpPageUrl, but that name is rather long, so it's understandable you chose a shorter name. In that case it might help the reader to add a small comment describing an example value for the variable. From this example value it is usually simple enough to infer all allowed values.

The function setName has the comment Assign the username. First, when I let my IDE render that comment, it looks like this:

Screenshot of rendered comment

The word Assign is listed in the section of the parameter $name while it really describes the function, not the parameter. The comment should rather be:

    /**
     * Assign the username
     * @param string $name
     */

If you were to rename the function to setUsername, each word of the comment would be redundant, allowing you to leave out the comment completely.

In the function setPassword, the comment contains the words & $confirm. The & should be written as &amp; since the comments contain HTML, just like Javadoc (different programming language, same idea). When you search for @param in that document, you will find in match 16/36 that each parameter has a @param of its own. It's the same for PHP.

In the comment for signUpPage, I don't see a need to write Page with a capital P. Same for returnWithError.

The comment for existingName says "Checks whether a name exists". The return type is void, which is unexpected to me. I would have expected bool here since that's the correct type for "whether". Instead, the function exits if the name does not exist. Therefore I would rename that function to checkNameExists. Function names starting with check usually behave this way, throwing either an exception on failure or exiting plainly.

In the function ValidateNameLength you started the name with a capital V, that is unusual. The other function names start with a lowercase letter.

In ValidateNameLength you check the length of the name using mb_strlen. This is very careful, many other programmers would have used the wrong strlen instead.

In addUser, you use a \w in the regular expression. The documentation for the \w was a bit hard to find since it is not directly referenced in the documentation of preg_match, but anyways. The \w includes all "word" characters, even outside [A-Za-z]. This may include Cyrillic, Hangul and several other alphabets. If you want to avoid usernames that look equal but are not (the Latin a and the Cyrillic а), you should rather restrict the allowed characters to [A-Za-z] first. You can always extend the character set later, restricting it is more work since it might affect existing usernames.

Still in addUser, the SQL statement currently depends on the order of the column names in the database. If the order of the columns should change in the future, your code will put the data in the wrong columns. To prevent this, you can list the column names explicitly, as in INSERT INTO users(name, email, password_hash, remote_addr, client_ip, forwarded_for, user_agent). This change, of course, will make your code dependent on the names of the columns instead of their position. Choose either way, I'm not sure which change is more likely to happen.

main.php

The call to new SignUp with the database connection parameters looks strange. This is because a Page object should not need to know these connection parameters as individual values, it should rather only have a single parameter of type DatabaseConnection. Assume that the database connection needs more more parameter, for whatever reason. Then you would have to adjust the call to new SignUp, and that feels wrong because a page should be independent from this low-level change.

Summary

Even after these many detailed remarks, the code still looks very solid to me. The remarks can be fixed easily, and the overall structure of the code is very nice and easy to understand. Very well done.

And to answer your initial main question: Yes, your code is secure. All user-generated data is only used in safe ways, without allowing any injection attacks.

\$\endgroup\$
3
  • 3
    \$\begingroup\$ If they remove the try block and only keep the single line $this->database = ... instead, the original stack trace will be preserved, including all information about the actual cause of the exception including database credentials, which feels at least awkward and some argue being a security breach to have database credentials revealed if not to the client but even in the sever logs anyway. \$\endgroup\$ Commented Feb 25, 2021 at 15:28
  • \$\begingroup\$ @Roland Illig Thank you so much for your answer. Clear and objective answer, thanks for the tips \$\endgroup\$
    – Warlock
    Commented Feb 25, 2021 at 18:04
  • \$\begingroup\$ @YourCommonSense Thank you for that explanation, I incorporated it into my answer. \$\endgroup\$ Commented Feb 26, 2021 at 0:53

Your Answer

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

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