I am new to web development and am working on a portfolio piece, so I would like to make sure I'm not creating obvious vulnerabilities. I am wondering whether there are any clear flaws in the following account creation and login code. Also, the relevant query wrapper is included as the last code snippet, and I am wondering whether there are SQL injection possibilities.
All of the files for this LAMP site are at: https://github.com/ian-luttrell/lamp-site
Create Account (controller):
<?php
require_once('../App/Models/CreateAccount.php');
class CreateAccount
{
public function index()
{
$view = '../App/Views/CreateAccount/index.php';
View::render($view, []);
}
public function submit()
{
// pass registration credentials to model for processing
$cred = ['username' => $_POST['username'],
'password' => $_POST['password']];
$processed_cred = CreateAccountModel::processCredentials($cred);
// pass processed credentials to model for account creation
CreateAccountModel::register($processed_cred);
$username = $processed_cred['username'];
$data = ['username' => $username];
$view = '../App/Views/CreateAccount/submit.php';
View::render($view, $data);
}
}
?>
Create Account (model):
<?php
require_once('../Core/Model.php');
class CreateAccountModel extends Model
{
public static function processCredentials($cred)
{
$username = htmlspecialchars($cred['username']);
$hashed_password = password_hash($cred['password'], PASSWORD_DEFAULT);
$processed_cred = ['username' => $username,
'hashed_pass' => $hashed_password];
return $processed_cred;
}
public static function register($processed_cred)
{
$username = $processed_cred['username'];
$hashed_password = $processed_cred['hashed_pass'];
$record = ['id' => NULL,
'username' => $username,
'hashed_password' => $hashed_password,
'created_at' => NULL];
$conn = static::getConn();
$db = new QueryBuilder($conn);
$db->insert('users', $record);
}
}
?>
Login (controller):
<?php
require_once('../App/Models/Login.php');
require_once('../Core/View.php');
class Login
{
public function index()
{
$view = '../App/Views/Login/index.php';
View::render($view, []);
}
public function submit()
{
$cred = ['username' => $_POST['username'],
'password' => $_POST['password']];
// pass supplied username to model for escaping
$username = $cred['username'];
$escapedUsername = LoginModel::escapeUsername($username);
$password = $_POST['password'];
$processedCred = ['username' => $escapedUsername,
'password' => $password];
// pass processed credentials to model for authentication
$validLogin = LoginModel::authenticate($processedCred);
if ($validLogin) {
session_start();
$_SESSION['user'] = $escapedUsername;
$data = ['username' => $escapedUsername];
$view = '../App/Views/Login/successful_login.php';
View::render($view, $data);
} else {
$view = '../App/Views/Login/failed_login.php';
View::render($view, []);
}
}
public function logOut()
{
session_start();
$_SESSION = [];
session_destroy();
$view = '../App/Views/Login/index.php';
View::render($view, []);
}
}
?>
Login (model):
<?php
require_once('../Core/Model.php');
class LoginModel extends Model
{
public static function escapeUsername($username)
{
return(htmlspecialchars($username));
}
public static function authenticate($processedCred)
{
$username = $processedCred['username'];
$password = $processedCred['password'];
$conn = static::getConn();
$sql = 'SELECT * FROM users WHERE username=:username';
$stmt = $conn->prepare($sql);
$stmt->bindParam('username', $username);
$user_exists = $stmt->execute();
if ($user_exists) {
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($results as $row) {
$hashed_password = $row['hashed_password'];
if (password_verify($password, $hashed_password)) {
return True;
}
}
}
return False;
}
}
?>
Query wrapper:
<?php
// Config.php is a configuration file in the App directory containing
// sensitive database login information, so it is NOT posted on GitHub
require_once('../App/Config.php');
class QueryBuilder
{
protected $pdo;
public function __construct($dbConn)
{
$this->pdo = $dbConn;
}
public function selectAll($table)
{
$statement = $this->pdo->prepare("SELECT * FROM {$table}");
$statement->execute();
return $statement->fetchAll(PDO::FETCH_ASSOC);
}
public function insert($table, $record)
{
$col_names = implode(', ', array_keys($record));
$parameters = ':' . implode(',:', array_keys($record));
$sql = sprintf("INSERT INTO %s (%s) VALUES (%s)", $table, $col_names, $parameters);
$parameters = explode(',', $parameters);
$arr = array_combine($parameters, $record);
try {
$stmt = $this->pdo->prepare($sql);
foreach ($arr as $param => $val) {
// cleaner to use bindValue() instead of bindParam()
// when values may be NULL
$stmt->bindValue($param, $val);
}
$stmt->execute();
} catch (PDOException $e) {
die('Database insert error.');
}
}
}
?>