Compare commits

...

10 Commits

Author SHA1 Message Date
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
Giandonato Inverso 268a6a3f65 Remove unnecessary extra information from documentation 2025-09-02 21:56:03 +02:00
Giandonato Inverso 812db2bf9b Delete support section from index.html 2025-09-02 21:53:41 +02:00
Giandonato Inverso 48d7fefd02 Remove donation button from README 2025-09-02 21:52:03 +02:00
Giandonato Inverso 27a5dbd466 Merge pull request #144 from CLAlberto/master
fix(read.php): replace deprecated FILTER_SANITIZE_STRING for PHP 8.3 …
2025-05-08 08:56:07 +02:00
CLAlberto 2c3f2bb030 fix(read.php): replace deprecated FILTER_SANITIZE_STRING for PHP 8.3 compatibility
### Problem

The usage of `FILTER_SANITIZE_STRING` in `read.php` causes a deprecation warning in PHP 8.1 and breaks functionality entirely in PHP 8.3, as the constant was removed.

### Solution

This commit replaces:
```php
filter_input(INPUT_GET, 'id', FILTER_SANITIZE_STRING);



with a safer and future-proof alternative:

$id = filter_input(INPUT_GET, 'id', FILTER_UNSAFE_RAW);
$id = trim(strip_tags($id));


> _Thanks for maintaining this project! Happy to contribute._ 😊
2025-05-06 16:16:22 +02:00
Giandonato Inverso e4d3af69b5 Merge pull request #143 from angelosleebos/patch-1
Make environment variables compatible for other platforms
2025-04-14 12:19:34 +02:00
Giandonato Inverso 938d934b51 Merge pull request #142 from rafinou62/patch-1
Update read.php prevent SQL Injection & XSS attacks
2025-04-14 12:18:32 +02:00
Angelo Sleebos cee6f4d3d5 Make environment variables compatible for other platforms
Make environment variables compatible for other platforms
2025-04-07 01:11:17 +02:00
Raphaël Wanecque 1465ce02ad Update read.php prevent SQL Injection & XSS attacks 2025-03-26 17:04:33 +01:00
52 changed files with 993 additions and 191 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
.idea
.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
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
RUN cp ./php-qrcode/composer.json /var/www/html/composer.json
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"]
-2
View File
@@ -10,8 +10,6 @@ password: admin
[DOCUMENTATION](https://giandonatoinverso.it/qrcode/documentation)
[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UEYVHYZYCGYYN)
# Features
- **#1 Dynamic Qr code generator on GitHub with a database to store Qr codes**
+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:
php-dynamic-qrcode:
image: "giandonatoinverso/php-dynamic-qr-code:latest"
build:
context: .
dockerfile: Dockerfile
restart: "unless-stopped"
environment:
TYPE: "docker"
QRCODE_GENERATOR: "internal-chillerlan.qrcode"
BASE_URL: "https://mydomain.com"
TYPE: "${TYPE:-docker}"
QRCODE_GENERATOR: "${QRCODE_GENERATOR:-internal-chillerlan.qrcode}"
BASE_URL: "${BASE_URL:-http://localhost}"
DATABASE_HOST: "php-dynamic-qrcode-db"
DATABASE_PORT: "3306"
DATABASE_NAME: "qrcode"
DATABASE_USER: "qrcode"
DATABASE_PASSWORD: "changeme"
DATABASE_PREFIX: ""
DATABASE_CHARSET: "utf8"
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}"
ports:
- 80:80
- "80:80"
depends_on:
- php-dynamic-qrcode-db
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: "giandonatoinverso/php-dynamic-qr-code-db:latest"
image: "mysql:8.0"
restart: "unless-stopped"
volumes:
- php_dynamic_qrcode_db_data:/var/lib/mysql
ports:
- '13306:3306'
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
environment:
MYSQL_ROOT_PASSWORD: "changeme"
MYSQL_DATABASE: "qrcode"
MYSQL_USER: "qrcode"
MYSQL_PASSWORD: "changeme"
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_config_data:
php_dynamic_qrcode_saved_qrcode_data:
networks:
-21
View File
@@ -267,27 +267,6 @@ The first shows a weekly report of the number of qr codes created (dynamic and s
You can also customize the redirect page and increase the timer
</p>
</div>
<div id="extra">
<h2>EXTRA</h2>
<p>My script is in constant development and I hope to expand it from time to time with more and more useful features, so stay tuned for the updates.<br> With the first version, in the classes that realize the 2 types of qr code, an additional method not mentioned in the above documentation called <strong>addLogo()</strong> is included.
To add this functionality you need to delete the comment characters inside the class and add the input fields to the forms for the user to upload the logo.
However, this feature is not recommended as it can cause different QR code scanning errors depending on the scanner applications.
</p>
</div>
<div id="support">
<h2>Support</h2>
<div class="wrapper">
<div class="alert alert-success alert-dismissible" role="alert">
If you have any question please feel free to email me at <strong><a href="mailto:hello@giandonatoinverso.dev?Subject=Dynamic%20Qrcode" target="_top">hello@giandonatoinverso.dev</a></strong>
</div>
<p>Please don't forget to rate my script on GitHub.
<br>
<br>Thank You, <b><br><br> Giandonato Inverso</b></p>
</div>
</div>
</div>
<!-- Main Panel End -->
+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
require_once 'config/config.php';
session_start();
require_once 'includes/bootstrap.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST')
{
csrf_verify_or_die();
$username = filter_input(INPUT_POST, 'username');
$password = filter_input(INPUT_POST, 'password');
$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.
$db = getDbInstance();
$db->where('username', $username);
$row = $db->getOne('users');
if ($db->count >= 1)
if ($db->count >= 1 && password_verify($password, $row['password']))
{
$db_password = $row['password'];
$user_id = $row['id'];
qr_record_login_attempt($username, true);
// 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['type'] = $row['type'];
$_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)
{
@@ -35,9 +54,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST')
$expiry_time = date('Y-m-d H:i:s', strtotime(' + 30 days'));
$expires = strtotime($expiry_time);
$is_https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
setcookie('series_id', $series_id, $expires, '/');
setcookie('remember_token', $remember_token, $expires, '/');
$cookie_options = [
'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->where ('id',$user_id);
@@ -51,16 +79,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST')
}
// Authentication successfull redirect user
header('Location: index.php');
}
else
{
$_SESSION['login_failure'] = 'Invalid username or password';
header('Location: login.php');
}
exit;
}
else
{
qr_record_login_attempt($username, false);
$_SESSION['login_failure'] = 'Invalid username or password';
header('Location: login.php');
exit;
+31 -17
View File
@@ -1,9 +1,14 @@
<?php
session_start();
require_once 'config/config.php';
require_once 'includes/bootstrap.php';
require_once BASE_PATH . '/includes/auth_validate.php';
require_once BASE_PATH . '/lib/DynamicQrcode/DynamicQrcode.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') {
$db = getDbInstance();
$json = json_decode(file_get_contents('php://input'), true);
@@ -12,8 +17,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$params = $json['params'];
$files = [];
if (isset($json['type'])) {
$type = filter_var($json['type'], FILTER_SANITIZE_FULL_SPECIAL_CHARS);
if (isset($json['type']) && in_array($json['type'], $allowed_types, true)) {
$type = $json['type'];
} else {
echo json_encode([
'data' => 'Type action field in the request.',
@@ -31,9 +36,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
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");
@$files[] = SAVED_QRCODE_FOLDER . $row['qrcode'];
if ($row !== NULL) {
$files[] = SAVED_QRCODE_FOLDER . $row['qrcode'];
}
}
$zip = new ZipArchive();
@@ -50,6 +61,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$zip->close();
audit_log('bulk_download', $type, implode(',', $params));
echo json_encode([
'data' => $url_path,
'status' => 200
@@ -57,10 +70,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit();
} else if($json["action"] == "delete") {
$params = $json['params'];
$files = [];
if (isset($json['type'])) {
$type = filter_var($json['type'], FILTER_SANITIZE_FULL_SPECIAL_CHARS);
if (isset($json['type']) && in_array($json['type'], $allowed_types, true)) {
$type = $json['type'];
} else {
echo json_encode([
'data' => 'Type action field in the request.',
@@ -79,16 +91,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if($type == "dynamic")
$instance = new DynamicQrcode();
else if($type == "static")
$instance = new StaticQrcode();
else
die("Type not allowed");
$instance = new StaticQrcode();
foreach ($params as $param) {
$a = 0;
$instance->deleteQrcode($param, true);
}
audit_log('bulk_delete', $type, implode(',', $params));
echo json_encode([
'action' => "delete",
'data' => "Qrcode deleted",
@@ -96,9 +107,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
]);
exit();
} else
exit("Action not allowed");
} else {
echo json_encode(['data' => 'Action not allowed', 'status' => 400]);
exit();
}
} 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>
+12 -29
View File
@@ -1,35 +1,18 @@
<?php
/*
|--------------------------------------------------------------------------
| DOCKER INSTALLATION
| UNIFIED ENVIRONMENT CONFIGURATION
|--------------------------------------------------------------------------
*/
*/
if(is_string(Getenv('TYPE')) && Getenv('TYPE') == "docker") {
define('DATABASE_HOST', Getenv('DATABASE_HOST'));
define('DATABASE_PORT', filter_var(Getenv('DATABASE_PORT'), FILTER_VALIDATE_INT));
define('DATABASE_NAME', Getenv('DATABASE_NAME'));
define('DATABASE_USER', Getenv('DATABASE_USER'));
define('DATABASE_PASSWORD', Getenv('DATABASE_PASSWORD'));
define('DATABASE_PREFIX', Getenv('DATABASE_PREFIX'));
define('DATABASE_CHARSET', Getenv('DATABASE_CHARSET'));
define('TYPE', Getenv('TYPE'));
define('BASE_URL', Getenv('BASE_URL'));
define("QRCODE_GENERATOR", Getenv('QRCODE_GENERATOR'));
} else {
define('DATABASE_HOST', "localhost");
define('DATABASE_PORT', "3306");
define('DATABASE_NAME', "qrcode");
define('DATABASE_USER', "root");
define('DATABASE_PASSWORD', "root");
define('DATABASE_PREFIX', "qr_");
define('DATABASE_CHARSET', "utf8");
define("QRCODE_GENERATOR", "external-api.qrserver.com"); // external-api.qrserver.com => https://api.qrserver.com/v1/create-qr-code/?data= // internal-chillerlan.qrcode => https://github.com/chillerlan/php-qrcode
}
/*
|--------------------------------------------------------------------------
| INSTALLATION WITHOUT CONTAINER
|--------------------------------------------------------------------------
*/
define('DATABASE_HOST', getenv('DATABASE_HOST') ?: 'localhost');
define('DATABASE_PORT', filter_var(getenv('DATABASE_PORT'), FILTER_VALIDATE_INT) ?: 3306);
define('DATABASE_NAME', getenv('DATABASE_NAME') ?: 'qrcode');
define('DATABASE_USER', getenv('DATABASE_USER') ?: 'root');
define('DATABASE_PASSWORD', getenv('DATABASE_PASSWORD') ?: 'root');
define('DATABASE_PREFIX', getenv('DATABASE_PREFIX') !== false ? getenv('DATABASE_PREFIX') : 'qr_');
define('DATABASE_CHARSET', getenv('DATABASE_CHARSET') ?: 'utf8');
?>
define('TYPE', getenv('TYPE') ?: 'local');
define('BASE_URL', getenv('BASE_URL') ?: 'http://localhost');
define('QRCODE_GENERATOR', getenv('QRCODE_GENERATOR') ?: 'external-api.qrserver.com'); // opties: external-api.qrserver.com of internal-chillerlan.qrcode
+3
View File
@@ -470,6 +470,9 @@
data: JSON.stringify(data),
dataType: "json",
contentType: 'application/json',
headers: {
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},
success: (res) => {
if (res.status == 200) {
if(data["action"] === "download") {
+6 -2
View File
@@ -1,11 +1,14 @@
<?php
session_start();
require_once 'config/config.php';
require_once 'includes/bootstrap.php';
require_once BASE_PATH.'/includes/auth_validate.php';
require_once BASE_PATH . '/lib/DynamicQrcode/DynamicQrcode.php';
$dynamic_qrcode_instance = new DynamicQrcode();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify_or_die();
}
$edit = false;
if($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["edit"]) && $_GET["edit"] == "true" && isset($_GET["id"])) {
$edit = true;
@@ -83,6 +86,7 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
<h3 class="card-title">Enter the requested data</h3>
</div>
<form class="form" action="" method="post" id="dynamic_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<div class="card-body">
<?php
if($edit)
+1 -2
View File
@@ -1,6 +1,5 @@
<?php
session_start();
require_once 'config/config.php';
require_once 'includes/bootstrap.php';
require_once BASE_PATH . '/includes/auth_validate.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>
</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>
+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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<!-- 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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<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">
<?php echo csrf_field(); ?>
<?php include BASE_PATH.'/forms/qrcode_options.php'; ?>
<!-- Input forms -->
<div class="col-sm-12 mb-2">
+2
View File
@@ -3,6 +3,7 @@
<div id="err-msg"></div>
<div class="bulk-action-wrapper">
<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="row">
<div class="col-5 col-md-2">
@@ -100,6 +101,7 @@
<div class="modal fade" id="delete-modal" role="dialog">
<div class="modal-dialog">
<form action="dynamic_qrcode.php" method="POST">
<?php echo csrf_field(); ?>
<!-- Modal content -->
<div class="modal-content">
+2
View File
@@ -3,6 +3,7 @@
<div id="err-msg"></div>
<div class="bulk-action-wrapper">
<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="row">
<div class="col-5 col-md-2">
@@ -96,6 +97,7 @@
<div class="modal fade" id="delete-modal" role="dialog">
<div class="modal-dialog">
<form action="static_qrcode.php" method="POST">
<?php echo csrf_field(); ?>
<!-- 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-dialog">
<form action="user.php" method="POST">
<?php echo csrf_field(); ?>
<!-- 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 name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="csrf-token" content="<?php echo csrf_token(); ?>">
<!-- Font Awesome Icons -->
<link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css">
+2 -6
View File
@@ -16,14 +16,10 @@
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
<div class="dropdown-divider"></div>
<!--<a href="#" class="dropdown-item">
<i class="fas fa-user"></i> Profile
<a href="./change_password.php" class="dropdown-item">
<i class="fas fa-key"></i> Change password
</a>
<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">
<i class="fas fa-sign-out-alt"></i> Logout
</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
//Use httponly flag
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/bootstrap.php';
require_once 'includes/auth_validate.php';
$db = getDbInstance();
+15
View File
@@ -56,6 +56,8 @@ class DynamicQrcode {
* We save into db the url of qrcode image
*/
public function addQrcode($input_data) {
$this->validateLink($input_data['link'] ?? '');
if($input_data['id_owner'] != "")
$data_to_db['id_owner'] = $input_data['id_owner'];
else
@@ -79,6 +81,8 @@ class DynamicQrcode {
*
*/
public function editQrcode($input_data) {
$this->validateLink($input_data['link'] ?? '');
if($input_data['id_owner'] != "")
$data_to_db['id_owner'] = $input_data['id_owner'];
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
*/
+39
View File
@@ -20,6 +20,8 @@ class Qrcode {
private string $table;
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) {
$db = getDbInstance();
@@ -94,6 +123,9 @@ class Qrcode {
public function addQrcode($input_data, $data_to_db, $data_to_qrcode) {
$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;
$imageFormat = strtolower($data_to_db['format']);
$fileExt = $imageFormat;
@@ -305,6 +337,7 @@ class Qrcode {
$this->failure('You cannot create a new qr code with an existing name on the server!');
if ($last_id){
audit_log('qrcode_created', $this->table, $last_id);
$this->success('Qr code added successfully!');
}
else {
@@ -320,6 +353,7 @@ class Qrcode {
$db = getDbInstance();
$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"];
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!');
if ($stat){
audit_log('qrcode_updated', $this->table, $input_data['id']);
$this->success('Qr code updated successfully!');
}
else {
@@ -357,6 +392,10 @@ class Qrcode {
$db->where('id', $id);
$status = $db->delete($this->table);
if ($status) {
audit_log('qrcode_deleted', $this->table, $id);
}
try{
unlink(SAVED_QRCODE_DIRECTORY.$qrcode["filename"].'.'.$qrcode["format"]);
}
+39
View File
@@ -5,6 +5,8 @@ class Qrcode {
private string $table;
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) {
$db = getDbInstance();
@@ -79,6 +108,9 @@ class Qrcode {
public function addQrcode($input_data, $data_to_db, $data_to_qrcode) {
$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'])){
$url =
'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!');
if ($last_id){
audit_log('qrcode_created', $this->table, $last_id);
$this->success('Qr code added successfully!');
}
else {
@@ -126,6 +159,7 @@ class Qrcode {
$db = getDbInstance();
$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"];
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!');
if ($stat){
audit_log('qrcode_updated', $this->table, $input_data['id']);
$this->success('Qr code updated successfully!');
}
else {
@@ -163,6 +198,10 @@ class Qrcode {
$db->where('id', $id);
$status = $db->delete($this->table);
if ($status) {
audit_log('qrcode_deleted', $this->table, $id);
}
try{
unlink(SAVED_QRCODE_DIRECTORY.$qrcode["filename"].'.'.$qrcode["format"]);
}
+58 -10
View File
@@ -3,6 +3,8 @@ require_once 'config/config.php';
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) {
$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['password'] = password_hash($input_data['password'], PASSWORD_DEFAULT);
$data_to_db["type"] = $input_data["type"];
@@ -66,9 +96,11 @@ class Users
$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');
}
}
/**
* Edit user
@@ -77,28 +109,43 @@ class Users
public function editUser($input_data) {
$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('id', $input_data["id"], '!=');
$row = $db->getOne('users');
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);
}
$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"];
// 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"]);
$stat = $db->update('users', $data_to_db);
if ($stat)
if ($stat) {
audit_log('user_updated', 'user', $input_data['id']);
$this->success('User updated successfully!');
else
} else
$this->failure('Failed to update User: ' . $db->getLastError());
}
@@ -116,9 +163,10 @@ class Users
$db->where('id', $id);
$stat = $db->delete('users');
if ($stat)
if ($stat) {
audit_log('user_deleted', 'user', $id);
$this->info('User deleted successfully!');
else
} else
$this->failure('Unable to delete user');
}
+11 -2
View File
@@ -1,12 +1,12 @@
<?php
session_start();
require_once 'config/config.php';
require_once 'includes/bootstrap.php';
$token = bin2hex(openssl_random_pseudo_bytes(16));
// If User has already logged in, redirect to dashboard page.
if (isset($_SESSION['user_logged_in']) && $_SESSION['user_logged_in'] === TRUE)
{
header('Location: index.php');
exit;
}
// If user has previously selected "remember me option":
@@ -33,9 +33,17 @@ if (isset($_COOKIE['series_id']) && isset($_COOKIE['remember_token']))
exit;
}
session_regenerate_id(true);
$_SESSION['user_logged_in'] = TRUE;
$_SESSION['user_id'] = $row['id'];
$_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');
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>
<form method="POST" action="authenticate.php">
<?php echo csrf_field(); ?>
<div class="input-group mb-3">
<input type="text" name="username" class="form-control" placeholder="Username" required="required">
<div class="input-group-append">
+14 -4
View File
@@ -1,10 +1,20 @@
<?php
require_once './config/config.php';
session_start();
require_once 'includes/bootstrap.php';
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();
if(isset($_COOKIE['series_id']) && isset($_COOKIE['remember_token'])){
if (isset($_COOKIE['series_id']) && isset($_COOKIE['remember_token'])) {
clearAuthCookie();
}
header('Location:index.php');
+31 -8
View File
@@ -1,24 +1,47 @@
<?php
include 'config/config.php';
if($_SERVER["REQUEST_METHOD"] !== "GET" || !isset($_GET['id']))
if ($_SERVER["REQUEST_METHOD"] !== "GET" || !isset($_GET['id'])) {
die("Method not allowed. Check id parameter");
}
// Validation and sanitization of the input UPDATE to php 8.3
$id = filter_input(INPUT_GET, 'id', FILTER_UNSAFE_RAW);
$id = trim(strip_tags($id));
if (!$id) {
die("Invalid ID parameter");
}
$db = getDbInstance();
$db->where("identifier", $_GET['id']);
// Using prepared statements to avoid SQL injections
$db->where("identifier", $id);
$qrcode = $db->getOne("dynamic_qrcodes");
$data = array (
if (!$qrcode) {
die("QR code not found");
}
$data = array(
'scan' => $db->inc(1)
);
$db->where("identifier", $_GET['id']);
$db->update ('dynamic_qrcodes', $data);
if($qrcode['state'] == 'enable'){
echo '<meta http-equiv="refresh" content="0; URL='.$qrcode['link'].'" />';
$db->where("identifier", $id);
if (!$db->update('dynamic_qrcodes', $data)) {
die("Failed to update scan count");
}
if ($qrcode['state'] == 'enable') {
// Validation and escaping of the URL to avoid XSS attacks
$link = filter_var($qrcode['link'], FILTER_VALIDATE_URL);
if ($link) {
echo '<meta http-equiv="refresh" content="0; URL=' . htmlspecialchars($link, ENT_QUOTES, 'UTF-8') . '" />';
echo 'Loading...'; // You can include a custom page to display during the redirect
} else {
echo 'Invalid URL';
}
else
} else {
echo 'Disabled link';
}
?>
+6 -2
View File
@@ -1,11 +1,14 @@
<?php
session_start();
require_once 'config/config.php';
require_once 'includes/bootstrap.php';
require_once BASE_PATH.'/includes/auth_validate.php';
require_once BASE_PATH . '/lib/StaticQrcode/StaticQrcode.php';
$static_qrcode_instance = new StaticQrcode();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify_or_die();
}
$edit = false;
if($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["edit"]) && $_GET["edit"] == "true" && isset($_GET["id"])) {
$edit = true;
@@ -115,6 +118,7 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
</div>
<?php if($edit) {?>
<form class="form" action="" method="post" id="static_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<div class="card-body">
<?php include BASE_PATH . '/forms/form_static_edit.php';?>
</div>
+1 -2
View File
@@ -1,6 +1,5 @@
<?php
session_start();
require_once 'config/config.php';
require_once 'includes/bootstrap.php';
require_once BASE_PATH . '/includes/auth_validate.php';
require_once BASE_PATH . '/lib/StaticQrcode/StaticQrcode.php';
+5 -2
View File
@@ -1,6 +1,5 @@
<?php
session_start();
require_once 'config/config.php';
require_once 'includes/bootstrap.php';
require_once BASE_PATH . '/includes/auth_validate.php';
require_once BASE_PATH . '/lib/Users/Users.php';
@@ -9,6 +8,9 @@ $user_instance = new Users();
if ($_SESSION['type'] !== 'super')
$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;
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>
</div>
<form class="well form-horizontal" action="" method="post" id="contact_form" enctype="multipart/form-data">
<?php echo csrf_field(); ?>
<div class="card-body">
<?php include BASE_PATH . '/forms/form_users.php'; ?>
</div>
+1 -2
View File
@@ -1,6 +1,5 @@
<?php
session_start();
require_once 'config/config.php';
require_once 'includes/bootstrap.php';
require_once BASE_PATH . '/includes/auth_validate.php';
require_once BASE_PATH . '/lib/Users/Users.php';