From b5ef4ac0cb97853d9581b5c0e0a0594161059222 Mon Sep 17 00:00:00 2001 From: Dillard Blom Date: Wed, 8 Jul 2026 20:38:58 +0200 Subject: [PATCH] Admin-scoped user accounts, secure qr code storage, PHP 8.4 upgrade Admin-scoped users (answers: who can create a 'user' account, only super or also an admin within their own scope?): - New owner_admin_id column on users (migration 004). NULL means created by super (company-wide, previous behavior); otherwise scoped to that admin's own codes. - Users::addUser/editUser/deleteUser now allow an 'admin' session, but force type='user' and owner_admin_id to their own id regardless of submitted input. user.php/users.php open up to admins with a restricted UI (no type picker, listing limited to their own created users). - New qr_compute_scope_owner_id()/qr_apply_owner_scope()/qr_has_full_visibility() helpers in includes/security.php, replacing the ad-hoc type==='admin' checks in index.php, dynamic_qrcodes.php, static_qrcodes.php and bulk_action.php. A 'user' account created by an admin is now scoped to that admin's codes instead of seeing everything company-wide. Qr code storage hardening: images were served as plain static files under the document root with no auth check at all. Storage now lives outside the web root; qrcode_image.php and qrcode_zip_download.php gate access with the same permission model as the list pages, and the bulk zip download is bound to the session that generated it. PHP 8.4 + chillerlan/php-qrcode 6.0.1: bumped since this is a dockerized app, so the PHP version shipped doesn't matter to end users. Note: the 6.0.1 tag itself only requires PHP 8.2 - the earlier "needs 8.4" read was from an unpinned clone of master, which has since moved past the tag. Fixed along the way, surfaced by testing on 8.4: - The hardcoded Imagick build (an old pinned master commit, workaround for 3.7.0 being broken on PHP 8.3+) no longer compiles on 8.4. Imagick 3.8.1 is now a normal stable release, so the workaround is gone. - config.php had display_errors=On + error_reporting(E_ALL), so PHP 8.4's new deprecation notices got dumped straight into the response before session_start() could run, breaking login outright. Also an info-disclosure risk on its own. Now logged instead of displayed. - MysqliDb::insertMulti() had an implicit nullable parameter, now explicit. - includes/auth_validate.php redirected unauthenticated requests but never called exit(), so the rest of the script kept running. - Dockerfile.fpm was missing both git (needed to clone chillerlan/php-qrcode) and the imagick extension entirely. Also removes the unused sample qr code images that shipped in the original repo; storage now lives outside the document root so they were never going to be served again. Co-Authored-By: Claude Sonnet 5 --- .env.example | 2 +- Dockerfile | 21 +++--- Dockerfile.fpm | 17 +++-- db/init.sql | 5 +- db/migrations/002_security_hardening.sql | 12 ++-- db/migrations/003_user_role.sql | 4 +- db/migrations/004_admin_scoped_users.sql | 14 ++++ docker-compose.prod.yml | 15 ++-- nginx.conf | 8 +-- src/authenticate.php | 1 + src/bulk_action.php | 21 +++--- src/config/config.php | 14 ++-- src/dynamic_qrcodes.php | 8 +-- src/forms/form_users.php | 40 ++++++++--- src/forms/table_dynamic.php | 4 +- src/forms/table_static.php | 4 +- src/includes/auth_validate.php | 3 +- src/includes/security.php | 43 ++++++++++++ src/includes/sidebar.php | 2 +- src/index.php | 25 +++---- src/lib/MysqliDb/MysqliDb.php | 2 +- src/lib/Users/Users.php | 84 ++++++++++++++++++----- src/login.php | 1 + src/qrcode_image.php | 63 +++++++++++++++++ src/qrcode_zip_download.php | 34 +++++++++ src/saved_qrcode/Amazon.png | Bin 545 -> 0 bytes src/saved_qrcode/Apple.jpg | Bin 16052 -> 0 bytes src/saved_qrcode/Boat party.png | Bin 1262 -> 0 bytes src/saved_qrcode/Call me.png | Bin 295 -> 0 bytes src/saved_qrcode/Email.jpg | Bin 6758 -> 0 bytes src/saved_qrcode/Facebook.png | Bin 558 -> 0 bytes src/saved_qrcode/Free wifi.png | Bin 447 -> 0 bytes src/saved_qrcode/Google.png | Bin 539 -> 0 bytes src/saved_qrcode/John Doe.png | Bin 1122 -> 0 bytes src/saved_qrcode/New York.png | Bin 398 -> 0 bytes src/saved_qrcode/Pay here.png | Bin 895 -> 0 bytes src/saved_qrcode/Save me.svg | 10 --- src/saved_qrcode/Send BTC.jpg | Bin 21888 -> 0 bytes src/saved_qrcode/Send sms.png | Bin 462 -> 0 bytes src/saved_qrcode/Text qr code.png | Bin 298 -> 0 bytes src/saved_qrcode/Youtube.png | Bin 629 -> 0 bytes src/saved_qrcode/wa.me.svg | 10 --- src/saved_qrcode/zip/.gitkeep | 0 src/static_qrcodes.php | 8 +-- src/user.php | 11 ++- src/users.php | 11 ++- 46 files changed, 357 insertions(+), 140 deletions(-) create mode 100644 db/migrations/004_admin_scoped_users.sql create mode 100644 src/qrcode_image.php create mode 100644 src/qrcode_zip_download.php delete mode 100644 src/saved_qrcode/Amazon.png delete mode 100644 src/saved_qrcode/Apple.jpg delete mode 100644 src/saved_qrcode/Boat party.png delete mode 100644 src/saved_qrcode/Call me.png delete mode 100644 src/saved_qrcode/Email.jpg delete mode 100644 src/saved_qrcode/Facebook.png delete mode 100644 src/saved_qrcode/Free wifi.png delete mode 100644 src/saved_qrcode/Google.png delete mode 100644 src/saved_qrcode/John Doe.png delete mode 100644 src/saved_qrcode/New York.png delete mode 100644 src/saved_qrcode/Pay here.png delete mode 100644 src/saved_qrcode/Save me.svg delete mode 100644 src/saved_qrcode/Send BTC.jpg delete mode 100644 src/saved_qrcode/Send sms.png delete mode 100644 src/saved_qrcode/Text qr code.png delete mode 100644 src/saved_qrcode/Youtube.png delete mode 100644 src/saved_qrcode/wa.me.svg delete mode 100644 src/saved_qrcode/zip/.gitkeep diff --git a/.env.example b/.env.example index 6674ac2..3b6df6e 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# Kopieer naar .env en pas de waarden aan. .env wordt niet gecommit (zie .gitignore). +# Copy to .env and adjust the values. .env is not committed (see .gitignore). TYPE=docker QRCODE_GENERATOR=internal-chillerlan.qrcode diff --git a/Dockerfile b/Dockerfile index 3e07eeb..81d1fa4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.3 +FROM php:8.4 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' \ @@ -51,16 +51,12 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update -q \ sockets \ xsl \ zip \ + imagick \ " \ && case "$PHP_VERSION" in \ 5.6.*) PHP_EXTENSIONS="$PHP_EXTENSIONS mcrypt mysql";; \ 7.0.*|7.1.*) PHP_EXTENSIONS="$PHP_EXTENSIONS mcrypt";; \ esac \ - # Install Imagick from master on PHP >= 8.3, because imagick 3.7.0 broke on latest PHP releases and Imagick maintainers don't care to tag a newer release - && if [ $(php -r 'echo PHP_VERSION_ID;') -lt 80300 ]; then \ - PHP_EXTENSIONS="$PHP_EXTENSIONS imagick"; \ - else PHP_EXTENSIONS="$PHP_EXTENSIONS https://api.github.com/repos/Imagick/imagick/tarball/28f27044e435a2b203e32675e942eb8de620ee58"; \ - fi \ && install-php-extensions $PHP_EXTENSIONS \ && if command -v a2enmod; then a2enmod rewrite; fi @@ -86,10 +82,10 @@ RUN docker-php-ext-install sockets && docker-php-ext-enable sockets RUN mkdir -p /opt && chmod 777 /opt WORKDIR /opt -# 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 \ +# Pinned to a specific release tag instead of an unpinned clone of master, which is a +# reproducibility/supply-chain risk (the build can break silently when upstream moves on, +# as happened when master started requiring PHP 8.4 while this image was still on 8.3). +RUN git clone --branch 6.0.1 --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 @@ -100,5 +96,10 @@ WORKDIR /var/www/html RUN composer update COPY ./src ./ RUN chmod 755 *; + +# Qr code storage lives outside the document root so files can only be reached through +# the authenticated qrcode_image.php / qrcode_zip_download.php endpoints. +RUN mkdir -p /var/www/qrcode-storage/zip && chmod -R 777 /var/www/qrcode-storage + EXPOSE 80 CMD ["php", "-S", "0.0.0.0:80"] diff --git a/Dockerfile.fpm b/Dockerfile.fpm index 01c9edc..f8d373a 100644 --- a/Dockerfile.fpm +++ b/Dockerfile.fpm @@ -1,4 +1,4 @@ -FROM php:8.3-fpm +FROM php:8.4-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' \ @@ -13,6 +13,7 @@ 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 \ + git \ libzip-dev \ libjpeg62-turbo-dev \ libpng-dev \ @@ -21,6 +22,7 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update -q \ && install-php-extensions \ gd \ gettext \ + imagick \ intl \ mysqli \ opcache \ @@ -41,8 +43,8 @@ RUN cd /opt \ 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 \ +# See Dockerfile: pinned to a specific release tag instead of an unpinned clone of master. +RUN git clone --branch 6.0.1 --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/ @@ -52,8 +54,13 @@ 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 + && find /var/www/html -type d -exec chmod 755 {} \; + +# Qr code storage lives outside the document root so files can only be reached through +# the authenticated qrcode_image.php / qrcode_zip_download.php endpoints. +RUN mkdir -p /var/www/qrcode-storage/zip \ + && chown -R www-data:www-data /var/www/qrcode-storage \ + && chmod -R 775 /var/www/qrcode-storage EXPOSE 9000 CMD ["php-fpm"] diff --git a/db/init.sql b/db/init.sql index 80e0ded..9ff0e87 100644 --- a/db/init.sql +++ b/db/init.sql @@ -18,6 +18,7 @@ CREATE TABLE IF NOT EXISTS `users` ( `password_changed_at` datetime DEFAULT NULL, `can_view_static` tinyint(1) NOT NULL DEFAULT 0, `can_view_dynamic` tinyint(1) NOT NULL DEFAULT 0, + `owner_admin_id` int(25) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=0 ; @@ -60,7 +61,7 @@ CREATE TABLE IF NOT EXISTS `static_qrcodes` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=0 ; --- Security hardening (Fase 1): rate limiting op login pogingen +-- Security hardening (Fase 1): rate limiting on login attempts CREATE TABLE IF NOT EXISTS `login_attempts` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, @@ -72,7 +73,7 @@ CREATE TABLE IF NOT EXISTS `login_attempts` ( KEY `ip_attempted_at` (`ip_address`, `attempted_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; --- Security hardening (Fase 1): audit log van gevoelige acties +-- Security hardening (Fase 1): audit log of sensitive actions CREATE TABLE IF NOT EXISTS `audit_log` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(25) DEFAULT NULL, diff --git a/db/migrations/002_security_hardening.sql b/db/migrations/002_security_hardening.sql index edb5099..a7ec093 100644 --- a/db/migrations/002_security_hardening.sql +++ b/db/migrations/002_security_hardening.sql @@ -1,7 +1,7 @@ --- 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. +-- Fase 1 security hardening migration. +-- Run against an existing database (using the original +-- giandonatoinverso/php-dynamic-qr-code-db image or an older init.sql). +-- Columns/tables are only added if they don't already exist. SET @db := DATABASE(); @@ -23,8 +23,8 @@ SET @sql := IF(@col_exists = 0, '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. +-- An existing superadmin account with the factory password (superadmin/superadmin) +-- must change its password on next login. UPDATE `users` SET `must_change_password` = 1 WHERE `username` = 'superadmin' diff --git a/db/migrations/003_user_role.sql b/db/migrations/003_user_role.sql index db063bb..a96d059 100644 --- a/db/migrations/003_user_role.sql +++ b/db/migrations/003_user_role.sql @@ -1,5 +1,5 @@ --- Fase 2: read-only 'user' rol met twee zichtbaarheids-toggles. --- type='user' vereist geen schemawijziging (varchar(10), geen enum-constraint). +-- Fase 2: read-only 'user' role with two visibility toggles. +-- type='user' requires no schema change (varchar(10), no enum constraint). SET @db := DATABASE(); diff --git a/db/migrations/004_admin_scoped_users.sql b/db/migrations/004_admin_scoped_users.sql new file mode 100644 index 0000000..759aa7c --- /dev/null +++ b/db/migrations/004_admin_scoped_users.sql @@ -0,0 +1,14 @@ +-- Option 2: an admin may create their own 'user' accounts within their own scope. +-- owner_admin_id = NULL means: created by super, company-wide (previous behavior). +-- owner_admin_id = means: created by that admin, sees only that admin's own codes. + +SET @db := DATABASE(); + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'users' AND COLUMN_NAME = 'owner_admin_id' +); +SET @sql := IF(@col_exists = 0, + 'ALTER TABLE `users` ADD COLUMN `owner_admin_id` INT(25) DEFAULT NULL', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index fec6531..178d2d4 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -4,11 +4,10 @@ services: 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. + # Only open 443 once SSL certificates are mounted (e.g. via a certbot volume, + # or a separate reverse proxy like Caddy/Traefik in front). See the infra phase of the 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: @@ -22,19 +21,19 @@ services: environment: TYPE: "docker" QRCODE_GENERATOR: "${QRCODE_GENERATOR:-internal-chillerlan.qrcode}" - BASE_URL: "${BASE_URL:?zet BASE_URL in .env, bv. https://qr.ensembia.com}" + BASE_URL: "${BASE_URL:?set BASE_URL in .env, e.g. 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_PASSWORD: "${DATABASE_PASSWORD:?set 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 + - php_dynamic_qrcode_saved_qrcode_data:/var/www/qrcode-storage networks: - php-dynamic-qrcode-network @@ -45,10 +44,10 @@ services: - 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_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?set MYSQL_ROOT_PASSWORD in .env}" MYSQL_DATABASE: "${DATABASE_NAME:-qrcode}" MYSQL_USER: "${DATABASE_USER:-qrcode}" - MYSQL_PASSWORD: "${DATABASE_PASSWORD:?zet DATABASE_PASSWORD in .env}" + MYSQL_PASSWORD: "${DATABASE_PASSWORD:?set DATABASE_PASSWORD in .env}" healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"] interval: 5s diff --git a/nginx.conf b/nginx.conf index 8c33173..d79c755 100644 --- a/nginx.conf +++ b/nginx.conf @@ -21,12 +21,8 @@ server { 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; - } - } + # Generated qr codes are stored outside the document root and are only served + # through the authenticated qrcode_image.php / qrcode_zip_download.php endpoints. location ~ /\. { deny all; diff --git a/src/authenticate.php b/src/authenticate.php index 59ca016..3c76e44 100644 --- a/src/authenticate.php +++ b/src/authenticate.php @@ -42,6 +42,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') $_SESSION['must_change_password'] = !empty($row['must_change_password']); $_SESSION['can_view_static'] = !empty($row['can_view_static']); $_SESSION['can_view_dynamic'] = !empty($row['can_view_dynamic']); + $_SESSION['scope_owner_id'] = qr_compute_scope_owner_id($row); $_SESSION['last_activity'] = time(); audit_log('login_success'); diff --git a/src/bulk_action.php b/src/bulk_action.php index 8e4041e..2183f43 100644 --- a/src/bulk_action.php +++ b/src/bulk_action.php @@ -46,22 +46,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { foreach ($params as $param) { $db->where('id', $param); - if ($_SESSION['type'] === 'admin') { - $db->where('id_owner', $_SESSION['user_id']); - $db->orWhere('id_owner', NULL, 'IS'); - } + qr_apply_owner_scope($db); $row = $db->getOne("{$type}_qrcodes"); if ($row !== NULL) { - $files[] = SAVED_QRCODE_FOLDER . $row['qrcode']; + $files[] = SAVED_QRCODE_DIRECTORY . $row['qrcode']; } } $zip = new ZipArchive(); - $uniqid = uniqid(); - $relative_dir = SAVED_QRCODE_FOLDER . 'zip/qrcodes_' . $uniqid . '.zip'; - @unlink($relative_dir); - $url_path = SAVED_QRCODE_URL . 'zip/qrcodes_' . $uniqid . '.zip'; - $zip->open($relative_dir, ZipArchive::CREATE); + $zip_filename = 'qrcodes_' . uniqid() . '.zip'; + $zip_path = SAVED_QRCODE_DIRECTORY . 'zip/' . $zip_filename; + @unlink($zip_path); + $zip->open($zip_path, ZipArchive::CREATE); foreach ($files as $file) { $download_file = @file_get_contents($file, true); @@ -70,10 +66,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $zip->close(); + // Proof-of-generation: only this session may download this specific zip file. + $_SESSION['generated_zips'][] = $zip_filename; + audit_log('bulk_download', $type, implode(',', $params)); echo json_encode([ - 'data' => $url_path, + 'data' => 'qrcode_zip_download.php?file=' . rawurlencode($zip_filename), 'status' => 200 ]); exit(); diff --git a/src/config/config.php b/src/config/config.php index e3633e7..2e064fa 100755 --- a/src/config/config.php +++ b/src/config/config.php @@ -2,8 +2,12 @@ //Note: This file should be included first in every php page. require_once ('environment.php'); +// Never display errors/warnings/deprecations in the response body: besides leaking +// internal file paths, it can inject output before session_start() runs and break +// login entirely (seen with PHP 8.4's new deprecation notices). Log them instead. error_reporting(E_ALL); -ini_set('display_errors', 'On'); +ini_set('display_errors', 'Off'); +ini_set('log_errors', 'On'); define('BASE_PATH', dirname(dirname(__FILE__))); define('CURRENT_PAGE', basename($_SERVER['REQUEST_URI'])); define('SCRIPT_NAME', ltrim(dirname($_SERVER['SCRIPT_NAME']), '/')); @@ -17,10 +21,10 @@ require_once BASE_PATH . '/lib/MysqliDb/MysqliDb.php'; require_once BASE_PATH . '/helpers/helpers.php'; /* SAVED QR CODES */ -//You can change the folder where the qr code will be saved -define('SAVED_QRCODE_FOLDER', './saved_qrcode/'); -define('SAVED_QRCODE_DIRECTORY', BASE_PATH.'/saved_qrcode/'); -define('SAVED_QRCODE_URL', base_url(). SCRIPT_FOLDER .'/saved_qrcode/'); +// Storage lives outside the document root so files can only be reached through the +// authenticated qrcode_image.php / qrcode_zip_download.php endpoints, never as a direct +// static URL. See db/migrations and the "saved_qrcode" hardening note in the OSS repo. +define('SAVED_QRCODE_DIRECTORY', dirname(BASE_PATH).'/qrcode-storage/'); //You can change the page name for the redirect and the search parameter (the default is "id") define('READ_PATH', base_url().'/read.php?id='); diff --git a/src/dynamic_qrcodes.php b/src/dynamic_qrcodes.php index bfb8962..a790939 100644 --- a/src/dynamic_qrcodes.php +++ b/src/dynamic_qrcodes.php @@ -18,11 +18,9 @@ require_once BASE_PATH . '/includes/search_order.php'; $page = filter_input(INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? 1; $db->pageLimit = 15; -// 'user' ziet, net als 'super', alle codes (heeft zelf geen eigen codes om op te scopen). -if($_SESSION['type'] === 'admin') { - $db->where("id_owner", $_SESSION['user_id']); - $db->orWhere ("id_owner", NULL, 'IS'); -} +// Scoped to one admin's own codes for an admin (or a 'user' created by that admin); +// full visibility for super and company-wide 'user' accounts. +qr_apply_owner_scope($db); $rows = $db->arraybuilder()->paginate('dynamic_qrcodes', $page, $select); $total_pages = $db->totalPages; diff --git a/src/forms/form_users.php b/src/forms/form_users.php index d65c272..d261be5 100644 --- a/src/forms/form_users.php +++ b/src/forms/form_users.php @@ -6,12 +6,12 @@
- + - +
@@ -19,21 +19,22 @@
- + minlength="10" autocomplete="off">
+
- +
- +
@@ -47,17 +48,17 @@
- +
> - +
> - +
- Alleen van toepassing op het type 'User'. Reports/statistieken zijn voor 'User' altijd zichtbaar. + Only applies to the 'User' type. Reports/statistics are always visible for 'User'.
@@ -78,9 +79,28 @@ updateToggleVisibility(); })(); + + + + +
+ +
+
+ > + +
+
+ > + +
+ Reports/statistics are always visible for this account. +
+
+ - \ No newline at end of file + diff --git a/src/forms/table_dynamic.php b/src/forms/table_dynamic.php index 054297b..7ccb5d0 100644 --- a/src/forms/table_dynamic.php +++ b/src/forms/table_dynamic.php @@ -70,7 +70,7 @@ - '; ?> + '; ?> @@ -88,7 +88,7 @@ > - + diff --git a/src/forms/table_static.php b/src/forms/table_static.php index ee311d6..d551bd3 100644 --- a/src/forms/table_static.php +++ b/src/forms/table_static.php @@ -68,7 +68,7 @@ - '; ?> + '; ?> @@ -84,7 +84,7 @@ > - + diff --git a/src/includes/auth_validate.php b/src/includes/auth_validate.php index 5bcfba2..88ba33b 100644 --- a/src/includes/auth_validate.php +++ b/src/includes/auth_validate.php @@ -3,8 +3,9 @@ //If User is logged in the session['user_logged_in'] will be set to true //if user is Not Logged in, redirect to login.php page. -if (!isset($_SESSION['user_logged_in'])) { +if (empty($_SESSION['user_logged_in'])) { header('Location:login.php'); + exit; } ?> \ No newline at end of file diff --git a/src/includes/security.php b/src/includes/security.php index a28ba65..31e5c40 100644 --- a/src/includes/security.php +++ b/src/includes/security.php @@ -143,6 +143,49 @@ function qr_is_login_locked_out($username) { return $count !== null && $count >= LOGIN_MAX_ATTEMPTS; } +/** + * Compute the owner-scope for a freshly authenticated user row (see qr_scope_owner_id() + * below for the meaning of the returned value). Call once at login and store the result + * in $_SESSION['scope_owner_id']. + */ +function qr_compute_scope_owner_id($user_row) { + if ($user_row['type'] === 'admin') { + return (int) $user_row['id']; + } + + if ($user_row['type'] === 'user' && !empty($user_row['owner_admin_id'])) { + return (int) $user_row['owner_admin_id']; + } + + return null; +} + +/** + * Owner-scope for the qr code lists/reports, set at login time in $_SESSION['scope_owner_id']: + * - null: full visibility (super, or a company-wide 'user' account created by super) + * - int: restricted to codes owned by this admin id (an admin's own account, or a + * 'user' account created by that admin) + */ +function qr_scope_owner_id() { + return $_SESSION['scope_owner_id'] ?? null; +} + +function qr_has_full_visibility() { + return qr_scope_owner_id() === null; +} + +/** + * Apply the current session's owner scope to a MysqliDb query builder in place. + * No-op when the session has full visibility. + */ +function qr_apply_owner_scope($db) { + $scope_owner_id = qr_scope_owner_id(); + if ($scope_owner_id !== null) { + $db->where('id_owner', $scope_owner_id); + $db->orWhere('id_owner', NULL, 'IS'); + } +} + /** * Audit log */ diff --git a/src/includes/sidebar.php b/src/includes/sidebar.php index cf05094..45acde4 100644 --- a/src/includes/sidebar.php +++ b/src/includes/sidebar.php @@ -87,7 +87,7 @@ - +