Files
QRForge-selfhosted/src/change_password.php
T
dillard feb5380b28 Security hardening: CSRF, rate limiting, session/password policy, audit log
Fixes critical pre-existing issues found during review: bulk_action.php had no
auth check at all (unauthenticated download/delete of any qrcode) and built a
table name from unwhitelisted user input (SQL injection); the QR generator
classes wrote files from unvalidated filename/format, allowing path traversal
and arbitrary file writes. Also pins chillerlan/php-qrcode to 5.0.5 since
master now requires PHP 8.4, breaking the PHP 8.3 build.

- CSRF tokens on all POST forms and the bulk_action.php JSON endpoint
- Login rate limiting (5 attempts / 15 min) via new login_attempts table
- Hardened sessions: httponly/samesite cookies, 30 min idle timeout,
  session regeneration on login
- Forced password change for the default superadmin/superadmin account
- Server-side validation in Users/DynamicQrcode/Qrcode classes
- Audit log table for auth, user, and qrcode actions
- Checked-in db schema (db/init.sql, migrations/) instead of relying on an
  opaque prebuilt db image
- Production docker-compose with Nginx + php-fpm instead of the PHP dev server

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 15:00:33 +02:00

100 lines
3.5 KiB
PHP

<?php
require_once 'includes/bootstrap.php';
if (empty($_SESSION['user_logged_in'])) {
header('Location: login.php');
exit;
}
$forced = !empty($_SESSION['must_change_password']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify_or_die();
$current_password = $_POST['current_password'] ?? '';
$new_password = $_POST['new_password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
$db = getDbInstance();
$db->where('id', $_SESSION['user_id']);
$user = $db->getOne('users');
if ($user === NULL || !password_verify($current_password, $user['password'])) {
$_SESSION['failure'] = 'Current password is incorrect.';
} elseif (strlen($new_password) < 10) {
$_SESSION['failure'] = 'New password must be at least 10 characters long.';
} elseif ($new_password !== $confirm_password) {
$_SESSION['failure'] = 'New password and confirmation do not match.';
} elseif ($new_password === $current_password) {
$_SESSION['failure'] = 'New password must be different from the current password.';
} else {
$db = getDbInstance();
$db->where('id', $_SESSION['user_id']);
$db->update('users', [
'password' => password_hash($new_password, PASSWORD_DEFAULT),
'must_change_password' => 0,
'password_changed_at' => date('Y-m-d H:i:s'),
]);
$_SESSION['must_change_password'] = false;
audit_log('password_changed');
$_SESSION['success'] = 'Password updated successfully.';
header('Location: index.php');
exit;
}
}
?>
<!DOCTYPE html>
<html lang="en">
<title>Change password - Qrcode Generator</title>
<?php include './includes/head.php'; ?>
<body class="login-page" style="min-height: 512.391px;">
<div class="login-box">
<div class="login-logo">
<img src="dist/img/DynamicQRCode_Original.png" style="width: 95%; height: 95%">
</div>
<div class="card">
<div class="card-body login-card-body">
<p class="login-box-msg">
<?php echo $forced
? 'You must change your password before continuing.'
: 'Change your password'; ?>
</p>
<?php include './includes/flash_messages.php'; ?>
<form method="POST" action="change_password.php">
<?php echo csrf_field(); ?>
<div class="input-group mb-3">
<input type="password" name="current_password" class="form-control" placeholder="Current password" required="required" autocomplete="current-password">
</div>
<div class="input-group mb-3">
<input type="password" name="new_password" class="form-control" placeholder="New password (min. 10 characters)" required="required" minlength="10" autocomplete="new-password">
</div>
<div class="input-group mb-3">
<input type="password" name="confirm_password" class="form-control" placeholder="Confirm new password" required="required" minlength="10" autocomplete="new-password">
</div>
<div class="row">
<div class="col-12">
<button type="submit" class="btn btn-primary btn-block">Update password</button>
</div>
</div>
</form>
<?php if (!$forced): ?>
<p class="mt-3 text-center"><a href="index.php">Back to dashboard</a></p>
<?php endif; ?>
</div>
</div>
</div>
<script src="../../plugins/jquery/jquery.min.js"></script>
<script src="../../plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="../../dist/js/adminlte.js"></script>
</body>
</html>