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
This commit is contained in:
2026-07-08 15:00:33 +02:00
parent 8db3158239
commit 3afe3b7698
48 changed files with 946 additions and 127 deletions
+15
View File
@@ -0,0 +1,15 @@
# Kopieer naar .env en pas de waarden aan. .env wordt niet gecommit (zie .gitignore).
TYPE=docker
QRCODE_GENERATOR=internal-chillerlan.qrcode
BASE_URL=http://localhost
DATABASE_HOST=php-dynamic-qrcode-db
DATABASE_PORT=3306
DATABASE_NAME=qrcode
DATABASE_USER=qrcode
DATABASE_PASSWORD=change-me-to-a-strong-password
DATABASE_PREFIX=
DATABASE_CHARSET=utf8
MYSQL_ROOT_PASSWORD=change-me-to-a-strong-root-password
+1
View File
@@ -2,3 +2,4 @@
.project .project
.idea .idea
.DS_Store .DS_Store
.env
+4 -1
View File
@@ -86,7 +86,10 @@ RUN docker-php-ext-install sockets && docker-php-ext-enable sockets
RUN mkdir -p /opt && chmod 777 /opt RUN mkdir -p /opt && chmod 777 /opt
WORKDIR /opt WORKDIR /opt
RUN git clone https://github.com/chillerlan/php-qrcode.git \ # Vastgezet op 5.0.5 (laatste 5.x-release): vanaf 6.0.0 vereist de library PHP >= 8.4,
# terwijl deze image op PHP 8.3 draait. Een ongepinde clone van master is bovendien
# een reproduceerbaarheids-/supply-chain-risico (build kan zonder waarschuwing breken).
RUN git clone --branch 5.0.5 --depth 1 https://github.com/chillerlan/php-qrcode.git \
&& chmod -R 777 ./php-qrcode && chmod -R 777 ./php-qrcode
RUN cp ./php-qrcode/composer.json /var/www/html/composer.json RUN cp ./php-qrcode/composer.json /var/www/html/composer.json
RUN mkdir -p /var/www/html/test && chmod 777 /var/www/html/test RUN mkdir -p /var/www/html/test && chmod 777 /var/www/html/test
+59
View File
@@ -0,0 +1,59 @@
FROM php:8.3-fpm
RUN if [ "$(grep '^VERSION_ID=' /etc/os-release | cut -d '=' -f 2 | tr -d '"')" -eq "9" ]; then \
sed -i -e 's/deb.debian.org/archive.debian.org/g' \
-e 's/security.debian.org/archive.debian.org/g' \
-e '/stretch-updates/d' /etc/apt/sources.list; \
fi
ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN chmod +x /usr/local/bin/install-php-extensions
RUN DEBIAN_FRONTEND=noninteractive apt-get update -q \
&& DEBIAN_FRONTEND=noninteractive apt-get install -qq -y \
curl \
libzip-dev \
libjpeg62-turbo-dev \
libpng-dev \
libfreetype6-dev \
zip unzip \
&& install-php-extensions \
gd \
gettext \
intl \
mysqli \
opcache \
pdo_mysql \
sockets \
zip
# Install Composer.
ENV PATH=$PATH:/root/composer/vendor/bin \
COMPOSER_ALLOW_SUPERUSER=1 \
COMPOSER_HOME=/root/composer
RUN cd /opt \
&& curl -sSL https://getcomposer.org/installer > composer-setup.php \
&& curl -sSL https://composer.github.io/installer.sha384sum > composer-setup.sha384sum \
&& sha384sum --check composer-setup.sha384sum \
&& php composer-setup.php --install-dir=/usr/local/bin --filename=composer --2 \
&& rm /opt/composer-setup.php /opt/composer-setup.sha384sum
RUN mkdir -p /opt && chmod 777 /opt
WORKDIR /opt
# Zie Dockerfile: vastgezet op 5.0.5, want 6.0.0+ vereist PHP >= 8.4.
RUN git clone --branch 5.0.5 --depth 1 https://github.com/chillerlan/php-qrcode.git \
&& chmod -R 777 ./php-qrcode
RUN cp ./php-qrcode/composer.json /var/www/html/composer.json
RUN cp -R ./php-qrcode/src /var/www/html/
WORKDIR /var/www/html
RUN composer update
COPY ./src ./
RUN chown -R www-data:www-data /var/www/html \
&& find /var/www/html -type f -exec chmod 644 {} \; \
&& find /var/www/html -type d -exec chmod 755 {} \; \
&& chmod -R 775 /var/www/html/saved_qrcode
EXPOSE 9000
CMD ["php-fpm"]
+91
View File
@@ -0,0 +1,91 @@
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
CREATE TABLE IF NOT EXISTS `users` (
`id` int(25) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`series_id` varchar(60) DEFAULT NULL,
`remember_token` varchar(255) DEFAULT NULL,
`expires` datetime DEFAULT NULL,
`type` varchar(10) NOT NULL,
`must_change_password` tinyint(1) NOT NULL DEFAULT 0,
`password_changed_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=0 ;
-- Default super admin account. Credentials: superadmin / superadmin
-- must_change_password=1 forces a password change on first login (see Fase 1 hardening).
INSERT INTO `users` (`id`, `username`, `password`, `series_id`, `remember_token`, `expires`, `type`, `must_change_password`, `password_changed_at`) VALUES
(1, 'superadmin', '$2y$10$xpZc5KC.aU2XHkcqhuZGFuAnqmtL4Unt8MysOyylceq.19XIyoZpG', NULL, NULL, NULL, 'super', 1, NULL);
CREATE TABLE IF NOT EXISTS `dynamic_qrcodes` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`id_owner` int(25) NULL DEFAULT NULL,
`filename` varchar(45) NOT NULL,
`format` varchar(45) DEFAULT NULL,
`identifier` longtext,
`link` varchar(500) DEFAULT NULL,
`qrcode` varchar(60) DEFAULT NULL,
`scan` int(11) NOT NULL DEFAULT '0',
`state` varchar(20) NOT NULL DEFAULT 'enable',
`created_by` int(10) unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_by` int(10) unsigned NOT NULL DEFAULT '0',
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=0 ;
CREATE TABLE IF NOT EXISTS `static_qrcodes` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`id_owner` int(25) NULL DEFAULT NULL,
`filename` varchar(45) CHARACTER SET utf8 NOT NULL,
`format` varchar(45) DEFAULT NULL,
`type` varchar(45) CHARACTER SET utf8 DEFAULT NULL,
`content` mediumtext CHARACTER SET utf8,
`qrcode` varchar(60) CHARACTER SET utf8 DEFAULT NULL,
`state` varchar(20) CHARACTER SET utf8 NOT NULL DEFAULT 'enable',
`created_by` int(10) unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_by` int(10) unsigned NOT NULL DEFAULT '0',
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=0 ;
-- Security hardening (Fase 1): rate limiting op login pogingen
CREATE TABLE IF NOT EXISTS `login_attempts` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`ip_address` varchar(45) NOT NULL,
`success` tinyint(1) NOT NULL DEFAULT 0,
`attempted_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `username_attempted_at` (`username`, `attempted_at`),
KEY `ip_attempted_at` (`ip_address`, `attempted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Security hardening (Fase 1): audit log van gevoelige acties
CREATE TABLE IF NOT EXISTS `audit_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(25) DEFAULT NULL,
`username` varchar(50) DEFAULT NULL,
`action` varchar(50) NOT NULL,
`target_type` varchar(30) DEFAULT NULL,
`target_id` varchar(50) DEFAULT NULL,
`ip_address` varchar(45) DEFAULT NULL,
`user_agent` varchar(255) DEFAULT NULL,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `created_at` (`created_at`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+57
View File
@@ -0,0 +1,57 @@
-- Fase 1 security hardening migratie.
-- Voer uit tegen een bestaande database (gebruikt de originele
-- giandonatoinverso/php-dynamic-qr-code-db image of een oudere init.sql).
-- Kolommen/tabellen worden alleen toegevoegd als ze nog niet bestaan.
SET @db := DATABASE();
SET @col_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'users' AND COLUMN_NAME = 'must_change_password'
);
SET @sql := IF(@col_exists = 0,
'ALTER TABLE `users` ADD COLUMN `must_change_password` TINYINT(1) NOT NULL DEFAULT 0',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @col_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'users' AND COLUMN_NAME = 'password_changed_at'
);
SET @sql := IF(@col_exists = 0,
'ALTER TABLE `users` ADD COLUMN `password_changed_at` DATETIME DEFAULT NULL',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- Bestaand superadmin account met het fabriekswachtwoord (superadmin/superadmin)
-- moet bij eerstvolgende login het wachtwoord wijzigen.
UPDATE `users`
SET `must_change_password` = 1
WHERE `username` = 'superadmin'
AND `password` = '$2y$10$xpZc5KC.aU2XHkcqhuZGFuAnqmtL4Unt8MysOyylceq.19XIyoZpG';
CREATE TABLE IF NOT EXISTS `login_attempts` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`ip_address` varchar(45) NOT NULL,
`success` tinyint(1) NOT NULL DEFAULT 0,
`attempted_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `username_attempted_at` (`username`, `attempted_at`),
KEY `ip_attempted_at` (`ip_address`, `attempted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `audit_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(25) DEFAULT NULL,
`username` varchar(50) DEFAULT NULL,
`action` varchar(50) NOT NULL,
`target_type` varchar(30) DEFAULT NULL,
`target_id` varchar(50) DEFAULT NULL,
`ip_address` varchar(45) DEFAULT NULL,
`user_agent` varchar(255) DEFAULT NULL,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `created_at` (`created_at`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+66
View File
@@ -0,0 +1,66 @@
services:
nginx:
image: "nginx:1.27-alpine"
restart: "unless-stopped"
ports:
- "80:80"
# 443 pas openzetten zodra SSL-certificaten zijn gemount (bv. via certbot-volume
# of een losse reverse proxy zoals Caddy/Traefik ervoor). Zie infra-fase van het plan.
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- php_dynamic_qrcode_saved_qrcode_data:/var/www/html/saved_qrcode:ro
depends_on:
- php-dynamic-qrcode
networks:
- php-dynamic-qrcode-network
php-dynamic-qrcode:
build:
context: .
dockerfile: Dockerfile.fpm
restart: "unless-stopped"
environment:
TYPE: "docker"
QRCODE_GENERATOR: "${QRCODE_GENERATOR:-internal-chillerlan.qrcode}"
BASE_URL: "${BASE_URL:?zet BASE_URL in .env, bv. https://qr.ensembia.com}"
DATABASE_HOST: "php-dynamic-qrcode-db"
DATABASE_PORT: "3306"
DATABASE_NAME: "${DATABASE_NAME:-qrcode}"
DATABASE_USER: "${DATABASE_USER:-qrcode}"
DATABASE_PASSWORD: "${DATABASE_PASSWORD:?zet DATABASE_PASSWORD in .env}"
DATABASE_PREFIX: "${DATABASE_PREFIX:-}"
DATABASE_CHARSET: "${DATABASE_CHARSET:-utf8}"
depends_on:
php-dynamic-qrcode-db:
condition: service_healthy
volumes:
- php_dynamic_qrcode_saved_qrcode_data:/var/www/html/saved_qrcode
networks:
- php-dynamic-qrcode-network
php-dynamic-qrcode-db:
image: "mysql:8.0"
restart: "unless-stopped"
volumes:
- php_dynamic_qrcode_db_data:/var/lib/mysql
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?zet MYSQL_ROOT_PASSWORD in .env}"
MYSQL_DATABASE: "${DATABASE_NAME:-qrcode}"
MYSQL_USER: "${DATABASE_USER:-qrcode}"
MYSQL_PASSWORD: "${DATABASE_PASSWORD:?zet DATABASE_PASSWORD in .env}"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 5s
timeout: 5s
retries: 10
networks:
- php-dynamic-qrcode-network
volumes:
php_dynamic_qrcode_db_data:
php_dynamic_qrcode_saved_qrcode_data:
networks:
php-dynamic-qrcode-network:
driver: bridge
+25 -20
View File
@@ -1,46 +1,51 @@
version: "3.2"
services: services:
php-dynamic-qrcode: php-dynamic-qrcode:
image: "giandonatoinverso/php-dynamic-qr-code:latest" build:
context: .
dockerfile: Dockerfile
restart: "unless-stopped" restart: "unless-stopped"
environment: environment:
TYPE: "docker" TYPE: "${TYPE:-docker}"
QRCODE_GENERATOR: "internal-chillerlan.qrcode" QRCODE_GENERATOR: "${QRCODE_GENERATOR:-internal-chillerlan.qrcode}"
BASE_URL: "https://mydomain.com" BASE_URL: "${BASE_URL:-http://localhost}"
DATABASE_HOST: "php-dynamic-qrcode-db" DATABASE_HOST: "php-dynamic-qrcode-db"
DATABASE_PORT: "3306" DATABASE_PORT: "3306"
DATABASE_NAME: "qrcode" DATABASE_NAME: "${DATABASE_NAME:-qrcode}"
DATABASE_USER: "qrcode" DATABASE_USER: "${DATABASE_USER:-qrcode}"
DATABASE_PASSWORD: "changeme" DATABASE_PASSWORD: "${DATABASE_PASSWORD:?zet DATABASE_PASSWORD in .env}"
DATABASE_PREFIX: "" DATABASE_PREFIX: "${DATABASE_PREFIX:-}"
DATABASE_CHARSET: "utf8" DATABASE_CHARSET: "${DATABASE_CHARSET:-utf8}"
ports: ports:
- 80:80 - "80:80"
depends_on: depends_on:
- php-dynamic-qrcode-db php-dynamic-qrcode-db:
condition: service_healthy
volumes: volumes:
- php_dynamic_qrcode_saved_qrcode_data:/var/www/html/saved_qrcode - php_dynamic_qrcode_saved_qrcode_data:/var/www/html/saved_qrcode
networks: networks:
- php-dynamic-qrcode-network - php-dynamic-qrcode-network
php-dynamic-qrcode-db: php-dynamic-qrcode-db:
image: "giandonatoinverso/php-dynamic-qr-code-db:latest" image: "mysql:8.0"
restart: "unless-stopped" restart: "unless-stopped"
volumes: volumes:
- php_dynamic_qrcode_db_data:/var/lib/mysql - php_dynamic_qrcode_db_data:/var/lib/mysql
ports: - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
- '13306:3306'
environment: environment:
MYSQL_ROOT_PASSWORD: "changeme" MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?zet MYSQL_ROOT_PASSWORD in .env}"
MYSQL_DATABASE: "qrcode" MYSQL_DATABASE: "${DATABASE_NAME:-qrcode}"
MYSQL_USER: "qrcode" MYSQL_USER: "${DATABASE_USER:-qrcode}"
MYSQL_PASSWORD: "changeme" MYSQL_PASSWORD: "${DATABASE_PASSWORD:?zet DATABASE_PASSWORD in .env}"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 5s
timeout: 5s
retries: 10
networks: networks:
- php-dynamic-qrcode-network - php-dynamic-qrcode-network
volumes: volumes:
php_dynamic_qrcode_db_data: php_dynamic_qrcode_db_data:
php_dynamic_qrcode_config_data:
php_dynamic_qrcode_saved_qrcode_data: php_dynamic_qrcode_saved_qrcode_data:
networks: networks:
+34
View File
@@ -0,0 +1,34 @@
server {
listen 80;
server_name _;
root /var/www/html;
index index.php;
client_max_body_size 20m;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "same-origin" always;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass php-dynamic-qrcode:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Statisch gegenereerde qrcodes mogen gedownload worden, maar niet als PHP uitgevoerd.
location /saved_qrcode/ {
location ~ \.php$ {
deny all;
}
}
location ~ /\. {
deny all;
}
}
+39 -16
View File
@@ -1,31 +1,50 @@
<?php <?php
require_once 'includes/bootstrap.php';
require_once 'config/config.php';
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') if ($_SERVER['REQUEST_METHOD'] === 'POST')
{ {
csrf_verify_or_die();
$username = filter_input(INPUT_POST, 'username'); $username = filter_input(INPUT_POST, 'username');
$password = filter_input(INPUT_POST, 'password'); $password = filter_input(INPUT_POST, 'password');
$remember = filter_input(INPUT_POST, 'remember'); $remember = filter_input(INPUT_POST, 'remember');
if (!$username || !$password) {
$_SESSION['login_failure'] = 'Invalid username or password';
header('Location: login.php');
exit;
}
if (qr_is_login_locked_out($username)) {
$_SESSION['login_failure'] = 'Too many failed login attempts. Try again in 15 minutes.';
header('Location: login.php');
exit;
}
// Get DB instance. // Get DB instance.
$db = getDbInstance(); $db = getDbInstance();
$db->where('username', $username); $db->where('username', $username);
$row = $db->getOne('users'); $row = $db->getOne('users');
if ($db->count >= 1) if ($db->count >= 1 && password_verify($password, $row['password']))
{ {
$db_password = $row['password']; qr_record_login_attempt($username, true);
$user_id = $row['id'];
// Voorkom session fixation: nieuwe sessie-id na een geslaagde login.
session_regenerate_id(true);
if (password_verify($password, $db_password))
{
$_SESSION['user_logged_in'] = TRUE; $_SESSION['user_logged_in'] = TRUE;
$_SESSION['type'] = $row['type']; $_SESSION['type'] = $row['type'];
$_SESSION['user_id'] = $row['id']; $_SESSION['user_id'] = $row['id'];
$_SESSION['username'] = $row['username'];
$_SESSION['must_change_password'] = !empty($row['must_change_password']);
$_SESSION['last_activity'] = time();
audit_log('login_success');
$user_id = $row['id'];
if ($remember) if ($remember)
{ {
@@ -35,9 +54,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST')
$expiry_time = date('Y-m-d H:i:s', strtotime(' + 30 days')); $expiry_time = date('Y-m-d H:i:s', strtotime(' + 30 days'));
$expires = strtotime($expiry_time); $expires = strtotime($expiry_time);
$is_https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
setcookie('series_id', $series_id, $expires, '/'); $cookie_options = [
setcookie('remember_token', $remember_token, $expires, '/'); 'expires' => $expires,
'path' => '/',
'secure' => $is_https,
'httponly' => true,
'samesite' => 'Lax',
];
setcookie('series_id', $series_id, $cookie_options);
setcookie('remember_token', $remember_token, $cookie_options);
$db = getDbInstance(); $db = getDbInstance();
$db->where ('id',$user_id); $db->where ('id',$user_id);
@@ -51,16 +79,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST')
} }
// Authentication successfull redirect user // Authentication successfull redirect user
header('Location: index.php'); header('Location: index.php');
}
else
{
$_SESSION['login_failure'] = 'Invalid username or password';
header('Location: login.php');
}
exit; exit;
} }
else else
{ {
qr_record_login_attempt($username, false);
$_SESSION['login_failure'] = 'Invalid username or password'; $_SESSION['login_failure'] = 'Invalid username or password';
header('Location: login.php'); header('Location: login.php');
exit; exit;
+31 -17
View File
@@ -1,9 +1,14 @@
<?php <?php
session_start(); require_once 'includes/bootstrap.php';
require_once 'config/config.php'; require_once BASE_PATH . '/includes/auth_validate.php';
require_once BASE_PATH . '/lib/DynamicQrcode/DynamicQrcode.php'; require_once BASE_PATH . '/lib/DynamicQrcode/DynamicQrcode.php';
require_once BASE_PATH . '/lib/StaticQrcode/StaticQrcode.php'; require_once BASE_PATH . '/lib/StaticQrcode/StaticQrcode.php';
header('Content-Type: application/json');
csrf_verify_header_or_die();
$allowed_types = ['dynamic', 'static'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$db = getDbInstance(); $db = getDbInstance();
$json = json_decode(file_get_contents('php://input'), true); $json = json_decode(file_get_contents('php://input'), true);
@@ -12,8 +17,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$params = $json['params']; $params = $json['params'];
$files = []; $files = [];
if (isset($json['type'])) { if (isset($json['type']) && in_array($json['type'], $allowed_types, true)) {
$type = filter_var($json['type'], FILTER_SANITIZE_FULL_SPECIAL_CHARS); $type = $json['type'];
} else { } else {
echo json_encode([ echo json_encode([
'data' => 'Type action field in the request.', 'data' => 'Type action field in the request.',
@@ -31,9 +36,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
foreach ($params as $param) { foreach ($params as $param) {
$row = $db->where('id', $param); $db->where('id', $param);
if ($_SESSION['type'] !== 'super') {
$db->where('id_owner', $_SESSION['user_id']);
$db->orWhere('id_owner', NULL, 'IS');
}
$row = $db->getOne("{$type}_qrcodes"); $row = $db->getOne("{$type}_qrcodes");
@$files[] = SAVED_QRCODE_FOLDER . $row['qrcode']; if ($row !== NULL) {
$files[] = SAVED_QRCODE_FOLDER . $row['qrcode'];
}
} }
$zip = new ZipArchive(); $zip = new ZipArchive();
@@ -50,6 +61,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$zip->close(); $zip->close();
audit_log('bulk_download', $type, implode(',', $params));
echo json_encode([ echo json_encode([
'data' => $url_path, 'data' => $url_path,
'status' => 200 'status' => 200
@@ -57,10 +70,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit(); exit();
} else if($json["action"] == "delete") { } else if($json["action"] == "delete") {
$params = $json['params']; $params = $json['params'];
$files = [];
if (isset($json['type'])) { if (isset($json['type']) && in_array($json['type'], $allowed_types, true)) {
$type = filter_var($json['type'], FILTER_SANITIZE_FULL_SPECIAL_CHARS); $type = $json['type'];
} else { } else {
echo json_encode([ echo json_encode([
'data' => 'Type action field in the request.', 'data' => 'Type action field in the request.',
@@ -79,16 +91,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if($type == "dynamic") if($type == "dynamic")
$instance = new DynamicQrcode(); $instance = new DynamicQrcode();
else if($type == "static")
$instance = new StaticQrcode();
else else
die("Type not allowed"); $instance = new StaticQrcode();
foreach ($params as $param) { foreach ($params as $param) {
$a = 0;
$instance->deleteQrcode($param, true); $instance->deleteQrcode($param, true);
} }
audit_log('bulk_delete', $type, implode(',', $params));
echo json_encode([ echo json_encode([
'action' => "delete", 'action' => "delete",
'data' => "Qrcode deleted", 'data' => "Qrcode deleted",
@@ -96,9 +107,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
]); ]);
exit(); exit();
} else } else {
exit("Action not allowed"); echo json_encode(['data' => 'Action not allowed', 'status' => 400]);
exit();
}
} else { } else {
exit('Direct access to this script not allowed.'); http_response_code(405);
echo json_encode(['data' => 'Direct access to this script not allowed.', 'status' => 405]);
exit();
} }
?>
+99
View File
@@ -0,0 +1,99 @@
<?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>
+3
View File
@@ -470,6 +470,9 @@
data: JSON.stringify(data), data: JSON.stringify(data),
dataType: "json", dataType: "json",
contentType: 'application/json', contentType: 'application/json',
headers: {
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},
success: (res) => { success: (res) => {
if (res.status == 200) { if (res.status == 200) {
if(data["action"] === "download") { if(data["action"] === "download") {
+6 -2
View File
@@ -1,11 +1,14 @@
<?php <?php
session_start(); require_once 'includes/bootstrap.php';
require_once 'config/config.php';
require_once BASE_PATH.'/includes/auth_validate.php'; require_once BASE_PATH.'/includes/auth_validate.php';
require_once BASE_PATH . '/lib/DynamicQrcode/DynamicQrcode.php'; require_once BASE_PATH . '/lib/DynamicQrcode/DynamicQrcode.php';
$dynamic_qrcode_instance = new DynamicQrcode(); $dynamic_qrcode_instance = new DynamicQrcode();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify_or_die();
}
$edit = false; $edit = false;
if($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["edit"]) && $_GET["edit"] == "true" && isset($_GET["id"])) { if($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["edit"]) && $_GET["edit"] == "true" && isset($_GET["id"])) {
$edit = true; $edit = true;
@@ -83,6 +86,7 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
<h3 class="card-title">Enter the requested data</h3> <h3 class="card-title">Enter the requested data</h3>
</div> </div>
<form class="form" action="" method="post" id="dynamic_form" enctype="multipart/form-data"> <form class="form" action="" method="post" id="dynamic_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<div class="card-body"> <div class="card-body">
<?php <?php
if($edit) if($edit)
+1 -2
View File
@@ -1,6 +1,5 @@
<?php <?php
session_start(); require_once 'includes/bootstrap.php';
require_once 'config/config.php';
require_once BASE_PATH . '/includes/auth_validate.php'; require_once BASE_PATH . '/includes/auth_validate.php';
require_once BASE_PATH . '/lib/DynamicQrcode/DynamicQrcode.php'; require_once BASE_PATH . '/lib/DynamicQrcode/DynamicQrcode.php';
+1 -1
View File
@@ -20,7 +20,7 @@
<span class="input-group-text"><i class="fa fa-lock"></i></span> <span class="input-group-text"><i class="fa fa-lock"></i></span>
</div> </div>
<input type="password" name="password" placeholder="Password" class="form-control" required="required" autocomplete="off"> <input type="password" name="password" placeholder="<?php echo ($edit) ? 'Leave blank to keep current password' : 'Password'; ?>" class="form-control" <?php echo ($edit) ? '' : 'required="required"'; ?> minlength="10" autocomplete="off">
</div> </div>
</div> </div>
</div> </div>
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=2fa" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=2fa" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=bitcoin" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=bitcoin" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=bookmark" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=bookmark" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=email" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=email" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=event" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=event" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-4"> <div class="col-sm-4">
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=location" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=location" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-4"> <div class="col-sm-4">
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=paypal" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=paypal" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=phone" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=phone" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=skype" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=skype" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-4"> <div class="col-sm-4">
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=sms" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=sms" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=text" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=text" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-6"> <div class="col-sm-6">
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=vcard" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=vcard" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<!-- First row --> <!-- First row -->
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=whatsapp" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=whatsapp" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
+1
View File
@@ -1,4 +1,5 @@
<form class="form" action="static_qrcode.php?type=wifi" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="static_qrcode.php?type=wifi" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?> <?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms --> <!-- Input forms -->
<div class="col-sm-12 mb-2"> <div class="col-sm-12 mb-2">
+2
View File
@@ -3,6 +3,7 @@
<div id="err-msg"></div> <div id="err-msg"></div>
<div class="bulk-action-wrapper"> <div class="bulk-action-wrapper">
<form id="bulk-action" action="bulk_action.php" method="POST"> <form id="bulk-action" action="bulk_action.php" method="POST">
<?php echo csrf_field(); ?>
<div class="col-sm-12 mb-2" style="margin-left: 10px"> <div class="col-sm-12 mb-2" style="margin-left: 10px">
<div class="row"> <div class="row">
<div class="col-5 col-md-2"> <div class="col-5 col-md-2">
@@ -100,6 +101,7 @@
<div class="modal fade" id="delete-modal" role="dialog"> <div class="modal fade" id="delete-modal" role="dialog">
<div class="modal-dialog"> <div class="modal-dialog">
<form action="dynamic_qrcode.php" method="POST"> <form action="dynamic_qrcode.php" method="POST">
<?php echo csrf_field(); ?>
<!-- Modal content --> <!-- Modal content -->
<div class="modal-content"> <div class="modal-content">
+2
View File
@@ -3,6 +3,7 @@
<div id="err-msg"></div> <div id="err-msg"></div>
<div class="bulk-action-wrapper"> <div class="bulk-action-wrapper">
<form id="bulk-action" action="bulk_action.php" method="POST"> <form id="bulk-action" action="bulk_action.php" method="POST">
<?php echo csrf_field(); ?>
<div class="col-sm-12 mb-2" style="margin-left: 10px"> <div class="col-sm-12 mb-2" style="margin-left: 10px">
<div class="row"> <div class="row">
<div class="col-5 col-md-2"> <div class="col-5 col-md-2">
@@ -96,6 +97,7 @@
<div class="modal fade" id="delete-modal" role="dialog"> <div class="modal fade" id="delete-modal" role="dialog">
<div class="modal-dialog"> <div class="modal-dialog">
<form action="static_qrcode.php" method="POST"> <form action="static_qrcode.php" method="POST">
<?php echo csrf_field(); ?>
<!-- Modal content --> <!-- Modal content -->
<div class="modal-content"> <div class="modal-content">
+1
View File
@@ -47,6 +47,7 @@
<div class="modal fade" id="delete-modal" role="dialog"> <div class="modal fade" id="delete-modal" role="dialog">
<div class="modal-dialog"> <div class="modal-dialog">
<form action="user.php" method="POST"> <form action="user.php" method="POST">
<?php echo csrf_field(); ?>
<!-- Modal content --> <!-- Modal content -->
<div class="modal-content"> <div class="modal-content">
+14
View File
@@ -0,0 +1,14 @@
<?php
/**
* Centrale bootstrap voor elke entrypoint: config laden, sessie starten met
* verharde instellingen, sessie-timeout en verplichte wachtwoordwijziging afdwingen.
*
* Vervangt de losse "session_start(); require_once 'config/config.php';" aanroepen.
*/
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/security.php';
qr_session_start();
qr_enforce_session_timeout();
qr_enforce_password_change();
+1
View File
@@ -1,6 +1,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="csrf-token" content="<?php echo csrf_token(); ?>">
<!-- Font Awesome Icons --> <!-- Font Awesome Icons -->
<link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css"> <link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css">
+2 -6
View File
@@ -16,14 +16,10 @@
</a> </a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<!--<a href="#" class="dropdown-item"> <a href="./change_password.php" class="dropdown-item">
<i class="fas fa-user"></i> Profile <i class="fas fa-key"></i> Change password
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a href="#" class="dropdown-item">
<i class="fa fa-cog"></i> Settings
</a>-->
<div class="dropdown-divider"></div>
<a href="./logout.php" class="dropdown-item"> <a href="./logout.php" class="dropdown-item">
<i class="fas fa-sign-out-alt"></i> Logout <i class="fas fa-sign-out-alt"></i> Logout
</a> </a>
+161
View File
@@ -0,0 +1,161 @@
<?php
/**
* Fase 1 security hardening: sessiebeheer, CSRF, rate limiting, audit log.
* Wordt geladen via includes/bootstrap.php, dat als eerste in elke entrypoint hoort te staan.
*/
define('SESSION_IDLE_TIMEOUT', 30 * 60); // 30 minuten inactiviteit -> uitloggen
define('LOGIN_MAX_ATTEMPTS', 5);
define('LOGIN_LOCKOUT_WINDOW', 15 * 60); // 15 minuten
/**
* Start de sessie met verharde cookie-instellingen. Moet vóór elke output aangeroepen worden.
*/
function qr_session_start() {
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
$is_https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
ini_set('session.gc_maxlifetime', (string) SESSION_IDLE_TIMEOUT);
ini_set('session.use_strict_mode', '1');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => $is_https,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
function qr_client_ip() {
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
/**
* Logt de gebruiker uit als de sessie te lang inactief is geweest.
*/
function qr_enforce_session_timeout() {
if (empty($_SESSION['user_logged_in'])) {
return;
}
$now = time();
if (isset($_SESSION['last_activity']) && ($now - $_SESSION['last_activity']) > SESSION_IDLE_TIMEOUT) {
$_SESSION = [];
$_SESSION['login_failure'] = 'Je sessie is verlopen wegens inactiviteit. Log opnieuw in.';
header('Location: login.php');
exit;
}
$_SESSION['last_activity'] = $now;
}
/**
* Stuurt ingelogde gebruikers met een verplichte wachtwoordwijziging naar change_password.php,
* behalve op de wijzigingspagina en logout zelf.
*/
function qr_enforce_password_change() {
if (empty($_SESSION['user_logged_in']) || empty($_SESSION['must_change_password'])) {
return;
}
$current_script = basename(parse_url($_SERVER['SCRIPT_NAME'], PHP_URL_PATH));
$exempt = ['change_password.php', 'logout.php'];
if (in_array($current_script, $exempt, true)) {
return;
}
header('Location: change_password.php');
exit;
}
/**
* CSRF-bescherming
*/
function csrf_token() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrf_field() {
return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars(csrf_token(), ENT_QUOTES, 'UTF-8') . '">';
}
function csrf_is_valid($token) {
return isset($_SESSION['csrf_token']) && is_string($token) && hash_equals($_SESSION['csrf_token'], $token);
}
/**
* Voor klassieke form-POSTs: verwacht een verborgen veld "csrf_token".
*/
function csrf_verify_or_die() {
if (!csrf_is_valid($_POST['csrf_token'] ?? '')) {
http_response_code(403);
exit('403 Forbidden: invalid or missing CSRF token.');
}
}
/**
* Voor JSON/AJAX-endpoints (bv. bulk_action.php): verwacht header X-CSRF-Token.
*/
function csrf_verify_header_or_die() {
$token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!csrf_is_valid($token)) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode(['data' => 'Invalid or missing CSRF token', 'status' => 403]);
exit;
}
}
/**
* Rate limiting op login
*/
function qr_record_login_attempt($username, $success) {
$db = getDbInstance();
$db->insert('login_attempts', [
'username' => $username,
'ip_address' => qr_client_ip(),
'success' => $success ? 1 : 0,
'attempted_at' => date('Y-m-d H:i:s'),
]);
}
function qr_is_login_locked_out($username) {
$db = getDbInstance();
$window_start = date('Y-m-d H:i:s', time() - LOGIN_LOCKOUT_WINDOW);
$db->where('username', $username);
$db->where('success', 0);
$db->where('attempted_at', $window_start, '>=');
$count = $db->getValue('login_attempts', 'count(*)');
return $count !== null && $count >= LOGIN_MAX_ATTEMPTS;
}
/**
* Audit log
*/
function audit_log($action, $target_type = null, $target_id = null) {
$db = getDbInstance();
$db->insert('audit_log', [
'user_id' => $_SESSION['user_id'] ?? null,
'username' => $_SESSION['username'] ?? null,
'action' => $action,
'target_type' => $target_type,
'target_id' => $target_id !== null ? (string) $target_id : null,
'ip_address' => qr_client_ip(),
'user_agent' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255),
'created_at' => date('Y-m-d H:i:s'),
]);
}
+1 -11
View File
@@ -1,15 +1,5 @@
<?php <?php
//Use httponly flag require_once 'includes/bootstrap.php';
ini_set('session.cookie_httponly', 1);
//Use only cookies
ini_set('session.use_only_cookies', 1);
//Use secure flag
ini_set('session.cookie_secure', 1);
session_start();
require_once './config/config.php';
require_once 'includes/auth_validate.php'; require_once 'includes/auth_validate.php';
$db = getDbInstance(); $db = getDbInstance();
+15
View File
@@ -56,6 +56,8 @@ class DynamicQrcode {
* We save into db the url of qrcode image * We save into db the url of qrcode image
*/ */
public function addQrcode($input_data) { public function addQrcode($input_data) {
$this->validateLink($input_data['link'] ?? '');
if($input_data['id_owner'] != "") if($input_data['id_owner'] != "")
$data_to_db['id_owner'] = $input_data['id_owner']; $data_to_db['id_owner'] = $input_data['id_owner'];
else else
@@ -79,6 +81,8 @@ class DynamicQrcode {
* *
*/ */
public function editQrcode($input_data) { public function editQrcode($input_data) {
$this->validateLink($input_data['link'] ?? '');
if($input_data['id_owner'] != "") if($input_data['id_owner'] != "")
$data_to_db['id_owner'] = $input_data['id_owner']; $data_to_db['id_owner'] = $input_data['id_owner'];
else else
@@ -117,6 +121,17 @@ class DynamicQrcode {
} }
/**
* Server-side validatie van de redirect-link (verplicht, max. 500 tekens per kolomdefinitie).
*/
private function validateLink($link) {
$link = trim((string) $link);
if ($link === '' || strlen($link) > 500) {
$this->failure('Link is required and must be at most 500 characters.');
}
}
/** /**
* Flash message Failure process * Flash message Failure process
*/ */
+39
View File
@@ -20,6 +20,8 @@ class Qrcode {
private string $table; private string $table;
private string $redirect_url; private string $redirect_url;
const ALLOWED_FORMATS = ['png', 'gif', 'jpeg', 'jpg', 'svg', 'svgbw', 'eps'];
/** /**
* *
*/ */
@@ -43,6 +45,33 @@ class Qrcode {
{ {
} }
/**
* Voorkomt path traversal / arbitrary file write via een gemanipuleerde bestandsnaam.
*/
private function sanitizeFilename($filename) {
$filename = trim((string) $filename);
if ($filename === '' || strlen($filename) > 45) {
$this->failure('Filename must be between 1 and 45 characters.');
}
if (preg_match('#[\\/\\\\]#', $filename) || strpos($filename, '..') !== false || strpos($filename, "\0") !== false) {
$this->failure('Filename cannot contain path separators.');
}
return $filename;
}
private function validateFormat($format) {
$format = strtolower((string) $format);
if (!in_array($format, self::ALLOWED_FORMATS, true)) {
$this->failure('Invalid qr code format.');
}
return $format;
}
public function getQrcode($id) { public function getQrcode($id) {
$db = getDbInstance(); $db = getDbInstance();
@@ -94,6 +123,9 @@ class Qrcode {
public function addQrcode($input_data, $data_to_db, $data_to_qrcode) { public function addQrcode($input_data, $data_to_db, $data_to_qrcode) {
$options = $this->setOptions($input_data); $options = $this->setOptions($input_data);
$data_to_db['filename'] = $this->sanitizeFilename($data_to_db['filename']);
$data_to_db['format'] = $this->validateFormat($data_to_db['format']);
$outputInterface = QRGdImagePNG::class; $outputInterface = QRGdImagePNG::class;
$imageFormat = strtolower($data_to_db['format']); $imageFormat = strtolower($data_to_db['format']);
$fileExt = $imageFormat; $fileExt = $imageFormat;
@@ -305,6 +337,7 @@ class Qrcode {
$this->failure('You cannot create a new qr code with an existing name on the server!'); $this->failure('You cannot create a new qr code with an existing name on the server!');
if ($last_id){ if ($last_id){
audit_log('qrcode_created', $this->table, $last_id);
$this->success('Qr code added successfully!'); $this->success('Qr code added successfully!');
} }
else { else {
@@ -320,6 +353,7 @@ class Qrcode {
$db = getDbInstance(); $db = getDbInstance();
$old_qrcode = $this->getQrcode($input_data["id"]); $old_qrcode = $this->getQrcode($input_data["id"]);
$data_to_db['filename'] = $this->sanitizeFilename($data_to_db['filename']);
$data_to_db['qrcode'] = $data_to_db['filename'].'.'.$old_qrcode["format"]; $data_to_db['qrcode'] = $data_to_db['filename'].'.'.$old_qrcode["format"];
if(!file_exists(SAVED_QRCODE_DIRECTORY.$data_to_db['filename'].'.'.$old_qrcode["format"]) || $data_to_db['filename'] == $input_data["old_filename"]){ if(!file_exists(SAVED_QRCODE_DIRECTORY.$data_to_db['filename'].'.'.$old_qrcode["format"]) || $data_to_db['filename'] == $input_data["old_filename"]){
@@ -337,6 +371,7 @@ class Qrcode {
$this->failure('You cannot edit a qr code with an existing name on the server!'); $this->failure('You cannot edit a qr code with an existing name on the server!');
if ($stat){ if ($stat){
audit_log('qrcode_updated', $this->table, $input_data['id']);
$this->success('Qr code updated successfully!'); $this->success('Qr code updated successfully!');
} }
else { else {
@@ -357,6 +392,10 @@ class Qrcode {
$db->where('id', $id); $db->where('id', $id);
$status = $db->delete($this->table); $status = $db->delete($this->table);
if ($status) {
audit_log('qrcode_deleted', $this->table, $id);
}
try{ try{
unlink(SAVED_QRCODE_DIRECTORY.$qrcode["filename"].'.'.$qrcode["format"]); unlink(SAVED_QRCODE_DIRECTORY.$qrcode["filename"].'.'.$qrcode["format"]);
} }
+39
View File
@@ -5,6 +5,8 @@ class Qrcode {
private string $table; private string $table;
private string $redirect_url; private string $redirect_url;
const ALLOWED_FORMATS = ['png', 'gif', 'jpeg', 'jpg', 'svg', 'eps'];
/** /**
* *
*/ */
@@ -28,6 +30,33 @@ class Qrcode {
{ {
} }
/**
* Voorkomt path traversal / arbitrary file write via een gemanipuleerde bestandsnaam.
*/
private function sanitizeFilename($filename) {
$filename = trim((string) $filename);
if ($filename === '' || strlen($filename) > 45) {
$this->failure('Filename must be between 1 and 45 characters.');
}
if (preg_match('#[\\/\\\\]#', $filename) || strpos($filename, '..') !== false || strpos($filename, "\0") !== false) {
$this->failure('Filename cannot contain path separators.');
}
return $filename;
}
private function validateFormat($format) {
$format = strtolower((string) $format);
if (!in_array($format, self::ALLOWED_FORMATS, true)) {
$this->failure('Invalid qr code format.');
}
return $format;
}
public function getQrcode($id) { public function getQrcode($id) {
$db = getDbInstance(); $db = getDbInstance();
@@ -79,6 +108,9 @@ class Qrcode {
public function addQrcode($input_data, $data_to_db, $data_to_qrcode) { public function addQrcode($input_data, $data_to_db, $data_to_qrcode) {
$options = $this->setOptions($input_data); $options = $this->setOptions($input_data);
$data_to_db['filename'] = $this->sanitizeFilename($data_to_db['filename']);
$data_to_db['format'] = $this->validateFormat($data_to_db['format']);
if(!file_exists(SAVED_QRCODE_DIRECTORY.$data_to_db['filename'].'.'.$data_to_db['format'])){ if(!file_exists(SAVED_QRCODE_DIRECTORY.$data_to_db['filename'].'.'.$data_to_db['format'])){
$url = $url =
'https://api.qrserver.com/v1/create-qr-code/?data='. 'https://api.qrserver.com/v1/create-qr-code/?data='.
@@ -111,6 +143,7 @@ class Qrcode {
$this->failure('You cannot create a new qr code with an existing name on the server!'); $this->failure('You cannot create a new qr code with an existing name on the server!');
if ($last_id){ if ($last_id){
audit_log('qrcode_created', $this->table, $last_id);
$this->success('Qr code added successfully!'); $this->success('Qr code added successfully!');
} }
else { else {
@@ -126,6 +159,7 @@ class Qrcode {
$db = getDbInstance(); $db = getDbInstance();
$old_qrcode = $this->getQrcode($input_data["id"]); $old_qrcode = $this->getQrcode($input_data["id"]);
$data_to_db['filename'] = $this->sanitizeFilename($data_to_db['filename']);
$data_to_db['qrcode'] = $data_to_db['filename'].'.'.$old_qrcode["format"]; $data_to_db['qrcode'] = $data_to_db['filename'].'.'.$old_qrcode["format"];
if(!file_exists(SAVED_QRCODE_DIRECTORY.$data_to_db['filename'].'.'.$old_qrcode["format"]) || $data_to_db['filename'] == $input_data["old_filename"]){ if(!file_exists(SAVED_QRCODE_DIRECTORY.$data_to_db['filename'].'.'.$old_qrcode["format"]) || $data_to_db['filename'] == $input_data["old_filename"]){
@@ -143,6 +177,7 @@ class Qrcode {
$this->failure('You cannot edit a qr code with an existing name on the server!'); $this->failure('You cannot edit a qr code with an existing name on the server!');
if ($stat){ if ($stat){
audit_log('qrcode_updated', $this->table, $input_data['id']);
$this->success('Qr code updated successfully!'); $this->success('Qr code updated successfully!');
} }
else { else {
@@ -163,6 +198,10 @@ class Qrcode {
$db->where('id', $id); $db->where('id', $id);
$status = $db->delete($this->table); $status = $db->delete($this->table);
if ($status) {
audit_log('qrcode_deleted', $this->table, $id);
}
try{ try{
unlink(SAVED_QRCODE_DIRECTORY.$qrcode["filename"].'.'.$qrcode["format"]); unlink(SAVED_QRCODE_DIRECTORY.$qrcode["filename"].'.'.$qrcode["format"]);
} }
+58 -10
View File
@@ -3,6 +3,8 @@ require_once 'config/config.php';
class Users class Users
{ {
const ALLOWED_TYPES = ['super', 'admin'];
/** /**
* *
*/ */
@@ -10,6 +12,25 @@ class Users
{ {
} }
/**
* Server-side validatie van username/type. Geeft een foutmelding terug (string) of null als geldig.
*/
private function validateUsernameAndType($username, $type) {
if (!is_string($username) || strlen($username) < 3 || strlen($username) > 50) {
return 'Username must be between 3 and 50 characters.';
}
if (!preg_match('/^[a-zA-Z0-9._-]+$/', $username)) {
return 'Username may only contain letters, numbers, dots, underscores and hyphens.';
}
if (!in_array($type, self::ALLOWED_TYPES, true)) {
return 'Invalid user type.';
}
return null;
}
/** /**
* *
*/ */
@@ -54,6 +75,15 @@ class Users
public function addUser($input_data) { public function addUser($input_data) {
$db = getDbInstance(); $db = getDbInstance();
$validation_error = $this->validateUsernameAndType($input_data['username'] ?? '', $input_data['type'] ?? '');
if ($validation_error !== null) {
$this->failure($validation_error, 'Location: user.php');
}
if (!isset($input_data['password']) || strlen($input_data['password']) < 10) {
$this->failure('Password must be at least 10 characters long.', 'Location: user.php');
}
$data_to_db["username"] = $input_data["username"]; $data_to_db["username"] = $input_data["username"];
$data_to_db['password'] = password_hash($input_data['password'], PASSWORD_DEFAULT); $data_to_db['password'] = password_hash($input_data['password'], PASSWORD_DEFAULT);
$data_to_db["type"] = $input_data["type"]; $data_to_db["type"] = $input_data["type"];
@@ -66,9 +96,11 @@ class Users
$last_id = $db->insert('users', $data_to_db); $last_id = $db->insert('users', $data_to_db);
if ($last_id) if ($last_id) {
audit_log('user_created', 'user', $last_id);
$this->success('User added successfully'); $this->success('User added successfully');
} }
}
/** /**
* Edit user * Edit user
@@ -77,28 +109,43 @@ class Users
public function editUser($input_data) { public function editUser($input_data) {
$db = getDbInstance(); $db = getDbInstance();
$query_string = http_build_query(array(
'id' => $input_data["id"],
'edit' => "true",
));
$validation_error = $this->validateUsernameAndType($input_data['username'] ?? '', $input_data['type'] ?? '');
if ($validation_error !== null) {
$this->failure($validation_error, 'Location: user.php?'.$query_string);
}
if (isset($input_data['password']) && strlen($input_data['password']) > 0 && strlen($input_data['password']) < 10) {
$this->failure('Password must be at least 10 characters long.', 'Location: user.php?'.$query_string);
}
$db->where('username', $input_data['username']); $db->where('username', $input_data['username']);
$db->where('id', $input_data["id"], '!='); $db->where('id', $input_data["id"], '!=');
$row = $db->getOne('users'); $row = $db->getOne('users');
if (!empty($row['username'])) { if (!empty($row['username'])) {
$query_string = http_build_query(array(
'id' => $input_data["id"],
'edit' => "true",
));
$this->failure('Username already exists', 'Location: user.php?'.$query_string); $this->failure('Username already exists', 'Location: user.php?'.$query_string);
} }
$data_to_db["username"] = $input_data["username"]; $data_to_db["username"] = $input_data["username"];
$data_to_db['password'] = password_hash($input_data['password'], PASSWORD_DEFAULT);
$data_to_db["type"] = $input_data["type"]; $data_to_db["type"] = $input_data["type"];
// Alleen wachtwoord overschrijven als er een nieuwe waarde is opgegeven.
if (!empty($input_data['password'])) {
$data_to_db['password'] = password_hash($input_data['password'], PASSWORD_DEFAULT);
}
$db->where('id', $input_data["id"]); $db->where('id', $input_data["id"]);
$stat = $db->update('users', $data_to_db); $stat = $db->update('users', $data_to_db);
if ($stat) if ($stat) {
audit_log('user_updated', 'user', $input_data['id']);
$this->success('User updated successfully!'); $this->success('User updated successfully!');
else } else
$this->failure('Failed to update User: ' . $db->getLastError()); $this->failure('Failed to update User: ' . $db->getLastError());
} }
@@ -116,9 +163,10 @@ class Users
$db->where('id', $id); $db->where('id', $id);
$stat = $db->delete('users'); $stat = $db->delete('users');
if ($stat) if ($stat) {
audit_log('user_deleted', 'user', $id);
$this->info('User deleted successfully!'); $this->info('User deleted successfully!');
else } else
$this->failure('Unable to delete user'); $this->failure('Unable to delete user');
} }
+11 -2
View File
@@ -1,12 +1,12 @@
<?php <?php
session_start(); require_once 'includes/bootstrap.php';
require_once 'config/config.php';
$token = bin2hex(openssl_random_pseudo_bytes(16)); $token = bin2hex(openssl_random_pseudo_bytes(16));
// If User has already logged in, redirect to dashboard page. // If User has already logged in, redirect to dashboard page.
if (isset($_SESSION['user_logged_in']) && $_SESSION['user_logged_in'] === TRUE) if (isset($_SESSION['user_logged_in']) && $_SESSION['user_logged_in'] === TRUE)
{ {
header('Location: index.php'); header('Location: index.php');
exit;
} }
// If user has previously selected "remember me option": // If user has previously selected "remember me option":
@@ -33,9 +33,17 @@ if (isset($_COOKIE['series_id']) && isset($_COOKIE['remember_token']))
exit; exit;
} }
session_regenerate_id(true);
$_SESSION['user_logged_in'] = TRUE; $_SESSION['user_logged_in'] = TRUE;
$_SESSION['user_id'] = $row['id']; $_SESSION['user_id'] = $row['id'];
$_SESSION['type'] = $row['type']; $_SESSION['type'] = $row['type'];
$_SESSION['username'] = $row['username'];
$_SESSION['must_change_password'] = !empty($row['must_change_password']);
$_SESSION['last_activity'] = time();
audit_log('login_success_remember');
header('Location: index.php'); header('Location: index.php');
exit; exit;
} }
@@ -71,6 +79,7 @@ if (isset($_COOKIE['series_id']) && isset($_COOKIE['remember_token']))
<p class="login-box-msg">Sign in to start your session</p> <p class="login-box-msg">Sign in to start your session</p>
<form method="POST" action="authenticate.php"> <form method="POST" action="authenticate.php">
<?php echo csrf_field(); ?>
<div class="input-group mb-3"> <div class="input-group mb-3">
<input type="text" name="username" class="form-control" placeholder="Username" required="required"> <input type="text" name="username" class="form-control" placeholder="Username" required="required">
<div class="input-group-append"> <div class="input-group-append">
+14 -4
View File
@@ -1,10 +1,20 @@
<?php <?php
require_once './config/config.php'; require_once 'includes/bootstrap.php';
session_start();
if (!empty($_SESSION['user_logged_in'])) {
audit_log('logout');
}
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
}
session_destroy(); session_destroy();
if (isset($_COOKIE['series_id']) && isset($_COOKIE['remember_token'])) {
if(isset($_COOKIE['series_id']) && isset($_COOKIE['remember_token'])){
clearAuthCookie(); clearAuthCookie();
} }
header('Location:index.php'); header('Location:index.php');
+6 -2
View File
@@ -1,11 +1,14 @@
<?php <?php
session_start(); require_once 'includes/bootstrap.php';
require_once 'config/config.php';
require_once BASE_PATH.'/includes/auth_validate.php'; require_once BASE_PATH.'/includes/auth_validate.php';
require_once BASE_PATH . '/lib/StaticQrcode/StaticQrcode.php'; require_once BASE_PATH . '/lib/StaticQrcode/StaticQrcode.php';
$static_qrcode_instance = new StaticQrcode(); $static_qrcode_instance = new StaticQrcode();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify_or_die();
}
$edit = false; $edit = false;
if($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["edit"]) && $_GET["edit"] == "true" && isset($_GET["id"])) { if($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["edit"]) && $_GET["edit"] == "true" && isset($_GET["id"])) {
$edit = true; $edit = true;
@@ -115,6 +118,7 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
</div> </div>
<?php if($edit) {?> <?php if($edit) {?>
<form class="form" action="" method="post" id="static_form" enctype="multipart/form-data"> <form class="form" action="" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<div class="card-body"> <div class="card-body">
<?php include BASE_PATH . '/forms/form_static_edit.php';?> <?php include BASE_PATH . '/forms/form_static_edit.php';?>
</div> </div>
+1 -2
View File
@@ -1,6 +1,5 @@
<?php <?php
session_start(); require_once 'includes/bootstrap.php';
require_once 'config/config.php';
require_once BASE_PATH . '/includes/auth_validate.php'; require_once BASE_PATH . '/includes/auth_validate.php';
require_once BASE_PATH . '/lib/StaticQrcode/StaticQrcode.php'; require_once BASE_PATH . '/lib/StaticQrcode/StaticQrcode.php';
+5 -2
View File
@@ -1,6 +1,5 @@
<?php <?php
session_start(); require_once 'includes/bootstrap.php';
require_once 'config/config.php';
require_once BASE_PATH . '/includes/auth_validate.php'; require_once BASE_PATH . '/includes/auth_validate.php';
require_once BASE_PATH . '/lib/Users/Users.php'; require_once BASE_PATH . '/lib/Users/Users.php';
@@ -9,6 +8,9 @@ $user_instance = new Users();
if ($_SESSION['type'] !== 'super') if ($_SESSION['type'] !== 'super')
$user_instance->failure('Only a "super admin" account can access the admin listing page', 'Location: index.php'); $user_instance->failure('Only a "super admin" account can access the admin listing page', 'Location: index.php');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify_or_die();
}
$edit = false; $edit = false;
if($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["edit"]) && $_GET["edit"] == "true" && isset($_GET["id"])) { if($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["edit"]) && $_GET["edit"] == "true" && isset($_GET["id"])) {
@@ -83,6 +85,7 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
<h3 class="card-title">Enter the requested data</h3> <h3 class="card-title">Enter the requested data</h3>
</div> </div>
<form class="well form-horizontal" action="" method="post" id="contact_form" enctype="multipart/form-data"> <form class="well form-horizontal" action="" method="post" id="contact_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<div class="card-body"> <div class="card-body">
<?php include BASE_PATH . '/forms/form_users.php'; ?> <?php include BASE_PATH . '/forms/form_users.php'; ?>
</div> </div>
+1 -2
View File
@@ -1,6 +1,5 @@
<?php <?php
session_start(); require_once 'includes/bootstrap.php';
require_once 'config/config.php';
require_once BASE_PATH . '/includes/auth_validate.php'; require_once BASE_PATH . '/includes/auth_validate.php';
require_once BASE_PATH . '/lib/Users/Users.php'; require_once BASE_PATH . '/lib/Users/Users.php';