166 Commits

Author SHA1 Message Date
dillard 3afe3b7698 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
2026-07-08 15:00:33 +02:00
Giandonato Inverso 8db3158239 Remove unnecessary extra information from documentation 2025-09-02 21:56:03 +02:00
Giandonato Inverso b3b799819e Delete support section from index.html 2025-09-02 21:53:41 +02:00
Giandonato Inverso f89b39bde3 Remove donation button from README 2025-09-02 21:52:03 +02:00
Giandonato Inverso acb154502b 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 f24a7086ab 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 2f0c879158 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 2ce6a03921 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 40ec07fbb1 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 6cfbc4a759 Update read.php prevent SQL Injection & XSS attacks 2025-03-26 17:04:33 +01:00
giandonato.inverso@edempg.it 3ee79bfab7 edit demo url 2025-03-22 15:51:02 +01:00
giandonato.inverso@edempg.it a2fdf79491 config rollback 2025-03-20 23:38:55 +01:00
giandonato.inverso@edempg.it 236d7342d3 Merge remote-tracking branch 'origin/master' 2025-03-19 23:40:50 +01:00
giandonato.inverso@edempg.it 1f3110b26e bug fix Class Qrcode not found 2025-03-19 23:40:36 +01:00
Giandonato Inverso 8b24fd839d Merge pull request #136 from MickGe/patch-1
add cookies secure flags
2025-02-09 16:16:53 +01:00
MickGe 011e91e3d0 add cookies secure flags 2025-02-07 11:35:24 +01:00
giandonato.inverso@edempg.it 737ceca97b updated readme and docs 2025-01-21 22:41:14 +01:00
giandonato.inverso@edempg.it b4b42ddb29 updated readme 2025-01-12 23:50:15 +01:00
giandonato.inverso@edempg.it 3ca4163715 Local setup eliminated and documentation updated 2025-01-12 23:48:49 +01:00
giandonato.inverso@edempg.it c5f1b0d74f Merge remote-tracking branch 'origin/master' 2025-01-12 19:48:31 +01:00
Giandonato Inverso d825f597b8 Merge pull request #130 from Shineson1001/feature/129-QRCodeGeneratorSwitch
🐛 Global switch for the QR code generator (#129)
2025-01-02 12:04:55 +01:00
Shine 850f5e5c52 🐛 Global switch for the QR code generator (#129) 2024-12-31 20:48:56 +01:00
giandonato.inverso@edempg.it c46dae55ce Revert "Revert "Instead using external API (api.qrserver.com) .. use chillerlan/php-qrcode""
This reverts commit 4d33cf5379.
2024-12-23 18:52:11 +01:00
giandonato.inverso@edempg.it 4d33cf5379 Revert "Instead using external API (api.qrserver.com) .. use chillerlan/php-qrcode"
This reverts commit 6d42677732.
2024-12-23 18:49:05 +01:00
Giandonato Inverso a1bd583ad3 Merge pull request #128 from Shineson1001/feature/116-EventIncorrectTimeZone
🐛 Event: Incorrect Time Zone
2024-12-20 20:02:32 +01:00
Shine 597afbb1fe 🐛 Event: Incorrect Time Zone
- Add "Time zone" input field.
- 24-Hour time format.
- Set Min-Year and Max-Year dynamically
2024-12-19 22:34:42 +01:00
Giandonato Inverso 5028541bfd Merge pull request #127 from Shineson1001/feature/88-SelfHostedQRCodeGenerator
Instead using external API (api.qrserver.com) .. use chillerlan/php-qrcode
2024-12-15 16:35:29 +01:00
Shine 6d42677732 Instead using external API (api.qrserver.com) .. use chillerlan/php-qrcode 2024-12-15 14:36:39 +01:00
Giandonato Inverso 1499f06c71 Merge pull request #126 from Shineson1001/feature/2FA-QRCodes
Add 2FA QR Code
2024-12-14 09:17:50 +01:00
Shine 058277c0fc Add 2FA QR Code
Save your 2FA secrets to QR-Code.
2024-12-13 23:42:57 +01:00
Giandonato Inverso a426d45be1 available plugins 2024-10-20 16:32:58 +02:00
Giandonato Inverso 3f7ac0bce1 Update README.md 2024-10-20 11:07:12 +02:00
giandonato.inverso@edempg.it 9548a6d73a bug fix in helpers.php 2024-04-21 12:55:03 +02:00
giandonato.inverso@edempg.it 2715f2106c edit readme 2024-04-21 12:38:35 +02:00
giandonato.inverso@edempg.it e43e8f774e bug fix 2024-04-18 20:54:04 +02:00
giandonato.inverso@edempg.it acebfdd708 fix in bulk action 2024-04-18 20:44:22 +02:00
giandonato.inverso@edempg.it 005c67d721 readme 2024-04-18 20:35:04 +02:00
giandonato.inverso@edempg.it 9f2d14abf8 bug fix in read.php, bump version in footer, NEW: bulk delete 2024-04-18 20:33:20 +02:00
giandonato.inverso@edempg.it eba0ebbba7 bug fix database prefix in Qrcode class 2024-03-11 15:43:16 +01:00
giandonato.inverso@edempg.it 068ddebd35 bug fix bulk download 2024-03-04 14:36:18 +01:00
Giandonato Inverso d0d3daf7db Merge pull request #94 from tranmh/xss_static_qrcode
Fix Security: Stored Cross Site Scripting for static QR code
2024-03-01 09:58:29 +01:00
Minh Cuong Tran 92eb66fb35 Fix Security: Stored Cross Site Scripting for static QR code, see https://github.com/giandonatoinverso/PHP-Dynamic-Qr-code/issues/93 2024-03-01 08:03:31 +01:00
Giandonato Inverso b85f98e16d Merge pull request #92 from tranmh/fix_remove_DATABASE_PREFIX_for_getOne
Inconsistency of using DATABASE_PREFIX with getOne()
2024-02-29 00:25:17 +01:00
Minh Cuong Tran a7953f05bb Inconsistency of using DATABASE_PREFIX with getOne() 2024-02-28 16:56:39 +01:00
Giandonato Inverso 06ddd19378 Merge pull request #91 from tranmh/mixed_content_blocked
fix: mixed content blocked for http and https
2024-02-28 14:16:21 +01:00
Minh Cuong Tran 2b6c802659 fix mixed content blocked for http and https: Mixed Content: The page at 'https://localhost/qrcode/dynamic_qrcodes.php' was loaded over HTTPS, but requested an insecure stylesheet 'http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css'. This request has been blocked; the content must be served over HTTPS. 2024-02-28 13:55:44 +01:00
Giandonato Inverso 33651fcabb Merge pull request #90 from tranmh/fix_case_sensitive_filename
Fix case sensitive filename
2024-02-27 13:25:17 +01:00
Minh Cuong Tran b496e51ace fix: case sensitive for filename 2024-02-27 13:18:52 +01:00
Minh Cuong Tran 1217e32856 fix: case sensitive for filename 2024-02-27 13:18:01 +01:00
Giandonato Inverso 6646cc9545 increased size of column link - dynamic qrcode
increased size of column link - dynamic qrcode
2024-02-14 23:58:00 +01:00
Giandonato Inverso 915ea383b7 Update README.md
updated php version requirement
2024-01-03 00:43:50 +01:00
Giandonato Inverso 93980efdb7 Update README.md 2023-10-30 17:02:13 +01:00
Giandonato Inverso b52a61ca40 bug fix redirect url with docker installation 2023-10-17 23:24:34 +02:00
Giandonato Inverso c9fa6abf06 bug fix 2023-10-17 00:41:15 +02:00
Giandonato Inverso 15c84c0863 bug fix and documentation 2023-10-16 22:57:16 +02:00
Giandonato Inverso 90a7b2890b updated documentation 2023-10-16 22:24:15 +02:00
Giandonato Inverso 03da2d0432 updated documentation 2023-10-16 20:56:06 +02:00
Giandonato Inverso 68a2b9f7f3 Refactoring docker image building, NEW: added docker compose support
Signed-off-by: giandonato.inverso@edempg.it <giandonato.inverso@studenti.unimi.it>
2023-10-16 19:49:29 +02:00
Giandonato Inverso 5335b6fb7e readme updated
Signed-off-by: giandonato.inverso@edempg.it <giandonato.inverso@studenti.unimi.it>
2023-10-16 13:15:53 +02:00
giandonato.inverso@edempg.it fadde16882 readme updated
Signed-off-by: giandonato.inverso@edempg.it <giandonato.inverso@studenti.unimi.it>
2023-10-16 13:15:07 +02:00
Giandonato Inverso 575a6b3caa refactoring of table database, added script for upgrading to versions >= 2.0, added multi-user support
Signed-off-by: giandonato.inverso@edempg.it <giandonato.inverso@studenti.unimi.it>
2023-10-16 13:10:57 +02:00
Giandonato Inverso cdab7df35d readme updated
Signed-off-by: giandonato.inverso@edempg.it <giandonato.inverso@studenti.unimi.it>
2023-10-16 01:43:56 +02:00
Giandonato Inverso 9a0a2c7928 eliminazione file superflui, spostamento file read.php all'interno del progetto, aggiunta astrazione classe Qrcode, miglioramento download bulk, refactoring generale
Signed-off-by: giandonato.inverso@edempg.it <giandonato.inverso@studenti.unimi.it>
2023-10-16 01:41:27 +02:00
Giandonato Inverso 995272fa8d Bug fix login.php
removed .min extension adminlte js file
2023-09-16 14:51:04 +02:00
Giandonato Inverso ef6410253c paypal donations 2023-09-05 23:31:38 +02:00
Giandonato Inverso f9ad48f65a Update README.md 2023-09-05 23:29:51 +02:00
giandonato.inverso@edempg.it 4b28c09d49 Merge remote-tracking branch 'origin/master' 2023-08-22 17:15:09 +02:00
giandonato.inverso@edempg.it d14ec02e4e doc update
Signed-off-by: giandonato.inverso@edempg.it <Zannabianca20>
2023-08-22 17:14:52 +02:00
Giandonato Inverso c87a99a067 Update Dockerfile 2023-01-25 18:51:10 +01:00
Giandonato Inverso eb3c275035 Merge pull request #52 from AyhamAl-Ali/fix/db_prefix
🚀 Fix DB Prefix in `read.php`
2023-01-25 18:50:15 +01:00
Ayham Al-Ali e448066176 Fix DB Prefix 2023-01-25 20:47:52 +03:00
Giandonato Inverso abd55062f3 Merge pull request #45 from chilluniverse/patch-1
DATABASE_HOST
2022-10-14 10:36:59 +02:00
Pascal e99e97cae2 DATABASE_HOST
In the docker-compose.yml is the Database_Host defined as "mariadb". If Host is not changed in the environment.php to "mariadb" as well the setup will fail
2022-10-14 01:26:06 +02:00
giandonato.inverso@edempg.it 6d1c3ac7c4 new version dockerfile 2022-09-28 16:23:44 +02:00
giandonato.inverso@edempg.it e2d9aad91f new version dockerfile 2022-09-28 16:22:18 +02:00
Giandonato Inverso fa709c95db Merge pull request #37 from 0xRenegade/feature/download-multiple-qr-img-at-once
Error Message for no qrcodes selected.
2022-09-24 22:43:16 +02:00
0xRenegade 8cb8e36114 Error Message for no qrcodes selected. 2022-09-24 15:40:56 -05:00
Giandonato Inverso c3225c6f3f Merge pull request #36 from 0xRenegade/feature/download-multiple-qr-img-at-once
Feature/download multiple qr img at once
2022-09-24 22:26:36 +02:00
0xRenegade 9cf354910b download multiple qrcodes at once feature 2022-09-24 15:14:20 -05:00
0xRenegade a18657994e add base_url() function in helpers, works with https and port number 2022-09-24 14:32:04 -05:00
0xRenegade 4b5c9a800b adds in custom.css for, well, custom styles. 2022-09-24 13:09:17 -05:00
Giandonato Inverso a85cee4fda Merge pull request #35 from 0xRenegade/issue-30/update-docker-yml-for-db-prefix
Issue 30/update docker yml for db prefix
2022-09-24 19:44:31 +02:00
Giandonato Inverso 327cadaddb Merge pull request #33 from 0xRenegade/QOL/ignore-env-and-use-example-instead
QOL/ignore env and use example instead
2022-09-24 19:44:23 +02:00
0xRenegade 4da9be3e3e update docker-compose.yml for database prefix option 2022-09-24 12:19:40 -05:00
0xRenegade b26c905b2e update docs for this change 2022-09-24 11:52:34 -05:00
0xRenegade f458094e0d update main gitignore to add in environment.php 2022-09-24 11:44:02 -05:00
0xRenegade b16f30b5a3 move env to example, so we don't run into merge conflicts constantly 2022-09-24 11:42:17 -05:00
0xRenegade f072f151fd Merge pull request #1 from giandonatoinverso/master
sync master branch with remote
2022-09-24 11:28:55 -05:00
Giandonato Inverso 38ddbe43c7 Merge pull request #31 from 0xRenegade/renegade/general-fixes
general fixes, fixed database prefix during install
2022-09-24 11:53:34 +02:00
0xRenegade 9799509ff6 set static attribute of class rather than non-DRY code 2022-09-24 00:24:03 -05:00
0xRenegade f0d43fd18b remove error_log debugging 2022-09-24 00:05:50 -05:00
0xRenegade 5e531f4e15 updated MysqliDb class functions to handle prefix properly. 2022-09-23 23:44:01 -05:00
0xRenegade a0944afd3c if this feature is accepted in Pull Request, will need to add this back 2022-09-23 23:02:05 -05:00
0xRenegade 9b61284a1e Merge branch 'renegade/general-fixes' of github.com:0xRenegade/PHP-Dynamic-Qr-code into renegade/general-fixes 2022-09-23 23:01:05 -05:00
0xRenegade 70ec7afc90 some queries are manually entered, which aren't picked up by 'prefix' class attribute in database class 2022-09-23 22:59:53 -05:00
0xRenegade 1934c78251 Merge branch 'master' into renegade/general-fixes 2022-09-23 21:05:16 -05:00
0xRenegade 8f053474e7 added in prefix by default, oops. 2022-09-23 21:00:41 -05:00
0xRenegade b80636049f Issue #30: Fixes database_prefix option during install 2022-09-23 20:55:55 -05:00
Giandonato Inverso a8e4cf18ff Update config.php 2022-09-24 03:24:15 +02:00
Giandonato Inverso 418215cd23 Update docker-compose.yml 2022-09-24 03:23:41 +02:00
Giandonato Inverso 8bbb4354c0 Update environment.php 2022-09-24 03:23:20 +02:00
0xRenegade 2057df1924 missing scroll bar on documentation page sidebar 2022-09-23 20:09:22 -05:00
Giandonato Inverso b457823654 Update add_dynamic_form.php
added support for http url
2022-09-10 11:50:19 +02:00
Giandonato Inverso 02754fba37 Update MysqliDb.php
fix deprecated implode()
2022-09-05 20:40:26 +02:00
Giandonato Inverso 25add5b7fd Merge pull request #25 from nirpt/master
docker build now supports app release version code.
2022-08-29 14:09:34 +02:00
nirpt 2aa867aa2a Docker build with app version added. 2022-08-29 12:32:20 +02:00
nirpt 9cbf68a99e Merge remote-tracking branch 'origin/master'
# Conflicts:
#	docker/README.md
2022-08-28 13:22:12 +02:00
Giandonato Inverso dfaee77d86 Update README.md 2022-08-28 12:33:14 +02:00
Giandonato Inverso 59c6dc9233 doc update
Signed-off-by: giandonato.inverso@edempg.it <Zannabianca20>
2022-08-28 12:04:48 +02:00
Giandonato Inverso c3e04a4357 new installation process via script, elimination of data entry form for installation
Signed-off-by: giandonato.inverso@edempg.it <Zannabianca20>
2022-08-28 11:57:31 +02:00
nirpt 97da7f0d4f Minor refactor and cleanup 2022-08-28 11:29:20 +02:00
Giandonato Inverso 4dd7f9ab25 configuration file modification, docker environment variable support 2022-08-28 11:24:32 +02:00
Giandonato Inverso 3000271d95 Merge pull request #23 from nirpt/master
docker support added
2022-08-28 10:47:05 +02:00
nirpt 1194259e6e docker support added 2022-08-28 10:24:07 +02:00
Giandonato Inverso 5f1534f0be Update README.md 2022-06-17 11:49:06 +02:00
Giandonato Inverso c76c1164fc Update README.md 2022-06-17 11:48:44 +02:00
Giandonato Inverso c8b3bba9f0 Merge pull request #16 from neoteknic/patch-1
Fix php 8.1 warning in form field
2022-02-22 16:55:19 +01:00
neoteknic 10091ac8e4 Update filters.php
Fix php 8.1 warning in form field
2022-02-22 16:22:13 +01:00
Giandonato Inverso a6bf0d6ca9 Add files via upload 2020-09-08 18:43:51 +02:00
Giandonato Inverso 44e8465129 Add files via upload 2020-09-08 18:42:29 +02:00
Giandonato Inverso cd00141252 Add files via upload 2020-09-08 18:41:51 +02:00
Giandonato Inverso 0ca8bd7cb2 Add files via upload 2020-09-08 18:30:32 +02:00
Giandonato Inverso 554a27762d Add files via upload 2020-09-08 18:28:26 +02:00
Giandonato Inverso 36c52ba003 Add files via upload 2020-09-08 18:22:57 +02:00
Giandonato Inverso 773d4960fd Add files via upload 2020-09-08 18:20:38 +02:00
Giandonato Inverso 7757bcf76b Add files via upload 2020-09-08 18:17:08 +02:00
Giandonato Inverso 76883de681 Add files via upload 2020-09-08 18:15:54 +02:00
Giandonato Inverso 8d45a02149 Add files via upload 2020-09-08 18:13:36 +02:00
Giandonato Inverso d3e11399f7 Add files via upload 2020-09-08 18:11:37 +02:00
Giandonato Inverso 86f32b6327 Add files via upload 2020-09-08 18:09:19 +02:00
Giandonato Inverso 97df13f3f0 Add files via upload 2020-09-08 18:06:28 +02:00
Giandonato Inverso 4ac9cd811a Add files via upload 2020-09-08 17:59:49 +02:00
Giandonato Inverso 1968201a48 Add files via upload 2020-09-08 17:59:21 +02:00
Giandonato Inverso b39c88fc12 Update README.md 2020-09-08 17:31:01 +02:00
Giandonato Inverso 9118d4f33c Update README.md 2020-09-08 17:29:26 +02:00
Giandonato Inverso f8bc49c164 Update README.md 2020-09-08 17:26:41 +02:00
Giandonato Inverso 9db2388d51 Update README.md 2020-09-08 17:25:38 +02:00
Giandonato Inverso 03c9c2d720 Update README.md 2020-09-08 17:24:07 +02:00
Giandonato Inverso 3bb2545521 Update README.md 2020-09-08 17:23:38 +02:00
Giandonato Inverso 283df4b81c Update README.md 2020-09-08 17:23:17 +02:00
Giandonato Inverso e64ca74003 Update README.md 2020-09-08 17:23:02 +02:00
Giandonato Inverso 0c2988791f Update README.md 2020-09-08 17:22:41 +02:00
Giandonato Inverso e123385dac Update README.md 2020-09-08 17:22:25 +02:00
Giandonato Inverso 1c819e6626 Update README.md 2020-09-08 17:21:33 +02:00
Giandonato Inverso 8af83452ba Update README.md 2020-09-08 17:21:07 +02:00
Giandonato Inverso 527eb7b3ba Update README.md 2020-09-08 17:20:39 +02:00
Giandonato Inverso fe4c63e3a6 Update README.md 2020-09-08 17:20:20 +02:00
Giandonato Inverso 601be05d5c Update README.md 2020-09-08 17:20:05 +02:00
Giandonato Inverso f4f061d857 Update README.md 2020-09-08 17:18:41 +02:00
Giandonato Inverso f1eb8545b4 Update README.md 2020-09-08 17:17:36 +02:00
Giandonato Inverso 7e3cef7544 Update README.md 2020-09-08 17:14:57 +02:00
Giandonato Inverso 5530f62195 Update README.md 2020-09-08 17:13:33 +02:00
Giandonato Inverso df3211d583 Update README.md 2020-09-08 17:13:06 +02:00
Giandonato Inverso 117b2b21a2 Update README.md 2020-09-08 17:12:50 +02:00
Giandonato Inverso 5430504fa0 Update README.md 2020-09-08 17:12:34 +02:00
Giandonato Inverso 65c279b65d Update README.md 2020-09-08 17:12:01 +02:00
Giandonato Inverso ceda04f97e Update README.md 2020-09-08 17:11:45 +02:00
Giandonato Inverso 76cad846f9 Update README.md 2020-09-08 17:11:27 +02:00
Giandonato Inverso f0d187f8b7 Update README.md 2020-09-08 17:10:01 +02:00
Giandonato Inverso fefac0fa9f Update README.md 2020-09-08 17:09:36 +02:00
Giandonato Inverso 6f920aec63 Update README.md 2020-09-08 17:09:15 +02:00
Giandonato Inverso ab7a82edbf Update README.md 2020-09-08 17:08:42 +02:00
Giandonato Inverso 7e867ddf9d Update README.md 2020-09-08 17:07:06 +02:00
Giandonato Inverso 255201169f Update README.md 2020-09-08 17:06:25 +02:00
Giandonato Inverso ca0108c247 Update README.md 2020-09-08 17:05:43 +02:00
Giandonato Inverso 23864047ce Initial commit 2020-09-08 16:56:05 +02: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
+2 -1
View File
@@ -1,4 +1,5 @@
.settings
.project
.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
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"]
+1 -3
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**
@@ -64,4 +62,4 @@ password: admin
```bash
docker compose build --no-cache && docker compose up -d
```
3. Open your browser at http://localhost:80 and login with (username: superadmin, password: superadmin)
3. Open your browser at http://localhost:80 and login with (username: superadmin, password: superadmin)
+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
+26 -21
View File
@@ -1,48 +1,53 @@
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:
php-dynamic-qrcode-network:
driver: bridge
driver: bridge
+1 -22
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 -->
@@ -302,4 +281,4 @@ However, this feature is not recommended as it can cause different QR code scann
<script src="assets/custom.js"></script>
</body>
</html>
</html>
+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;
}
}
+57 -34
View File
@@ -1,66 +1,89 @@
<?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'];
qr_record_login_attempt($username, true);
// Voorkom session fixation: nieuwe sessie-id na een geslaagde login.
session_regenerate_id(true);
$_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 (password_verify($password, $db_password))
if ($remember)
{
$_SESSION['user_logged_in'] = TRUE;
$_SESSION['type'] = $row['type'];
$_SESSION['user_id'] = $row['id'];
$series_id = randomString(16);
$remember_token = getSecureRandomToken(20);
$encryted_remember_token = password_hash($remember_token,PASSWORD_DEFAULT);
if ($remember)
{
$series_id = randomString(16);
$remember_token = getSecureRandomToken(20);
$encryted_remember_token = password_hash($remember_token,PASSWORD_DEFAULT);
$expiry_time = date('Y-m-d H:i:s', strtotime(' + 30 days'));
$expires = strtotime($expiry_time);
$is_https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
$expiry_time = date('Y-m-d H:i:s', strtotime(' + 30 days'));
$expires = strtotime($expiry_time);
$cookie_options = [
'expires' => $expires,
'path' => '/',
'secure' => $is_https,
'httponly' => true,
'samesite' => 'Lax',
];
setcookie('series_id', $series_id, $expires, '/');
setcookie('remember_token', $remember_token, $expires, '/');
setcookie('series_id', $series_id, $cookie_options);
setcookie('remember_token', $remember_token, $cookie_options);
$db = getDbInstance();
$db->where ('id',$user_id);
$db = getDbInstance();
$db->where ('id',$user_id);
$update_remember = array(
'series_id'=> $series_id,
'remember_token' => $encryted_remember_token,
'expires' =>$expiry_time
);
$db->update('users', $update_remember);
}
// Authentication successfull redirect user
header('Location: index.php');
}
else
{
$_SESSION['login_failure'] = 'Invalid username or password';
header('Location: login.php');
$update_remember = array(
'series_id'=> $series_id,
'remember_token' => $encryted_remember_token,
'expires' =>$expiry_time
);
$db->update('users', $update_remember);
}
// Authentication successfull redirect user
header('Location: index.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
*/
+44 -5
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,21 +337,23 @@ 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 {
$this->failure('Insert failed: ' . $db->getLastError());
}
}
/**
* Edit qr code
*
*
*/
public function editQrcode($input_data, $data_to_db) {
$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 {
@@ -344,10 +379,10 @@ class Qrcode {
}
}
/**
* Delete qr code
*
*
*/
public function deleteQrcode($id, $async = false) {
$db = getDbInstance();
@@ -356,7 +391,11 @@ 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"]);
}
+40 -1
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 {
@@ -162,7 +197,11 @@ 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"]);
}
+59 -11
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,8 +96,10 @@ 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');
}
}
/**
@@ -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');
+33 -10
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
echo 'Disabled link';
} 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';