Compare commits

..

2 Commits

Author SHA1 Message Date
Jacek
5598888716 security: faza 4 - ochrona CSRF panelu administracyjnego
- Nowa klasa \Shared\Security\CsrfToken (generate/validate/regenerate)
- Token CSRF we wszystkich formularzach edycji (form-edit.php)
- Walidacja CSRF w FormRequestHandler::handleSubmit()
- Token CSRF w formularzu logowania i formularzach 2FA
- Walidacja CSRF w App::special_actions() dla żądań POST
- Regeneracja tokenu po udanym logowaniu (bezpośrednia i przez 2FA)
- Fix XSS: htmlspecialchars na $alert w unlogged-layout.php
- 7 nowych testów CsrfTokenTest (817 testów łącznie)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 10:06:40 +01:00
Jacek
235a388199 build: ver_0.336 - error handling, try-catch Apilo, E_WARNING cron 2026-03-12 09:31:17 +01:00
15 changed files with 283 additions and 123 deletions

View File

@@ -45,16 +45,20 @@ shopPRO is a PHP e-commerce platform with an admin panel and customer-facing sto
# Specific test method
./test.ps1 --filter testGetQuantityReturnsCorrectValue
# Alternative
composer test
# Alternatives
composer test # standard
./test.bat # testdox (readable list)
./test-simple.bat # dots
./test-debug.bat # debug output
./test.sh # Git Bash
```
PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`.
Current suite: **810 tests, 2264 assertions**.
Current suite: **817 tests, 2271 assertions**.
### Creating Updates
See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs.
See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs. ZIP structure must start directly from project directories — no version subfolder inside the archive.
## Architecture
@@ -229,6 +233,9 @@ Before starting implementation, review current state of docs.
- `docs/DATABASE_STRUCTURE.md` — full database schema
- `docs/TESTING.md` — test suite guide and structure
- `docs/FORM_EDIT_SYSTEM.md` — form system architecture
- `docs/CLASS_CATALOG.md` — full catalog of all classes with descriptions
- `docs/TODO.md` — outstanding tasks and planned features
- `docs/CRON_QUEUE_PLAN.md` — planned cron/queue architecture
- `docs/CHANGELOG.md` — version history
- `api-docs/api-reference.json` — REST API documentation (ordersPRO)
- `api-docs/index.html` — REST API documentation (ordersPRO)

View File

@@ -78,7 +78,8 @@ $_SESSION['can_use_rfm'] = true;
action="<?= htmlspecialchars($form->action) ?>" enctype="multipart/form-data">
<input type="hidden" name="_form_id" value="<?= htmlspecialchars($form->formId) ?>">
<input type="hidden" name="_csrf_token" value="<?= htmlspecialchars(\Shared\Security\CsrfToken::getToken()) ?>">
<?php foreach ($form->hiddenFields as $name => $value): ?>
<input type="hidden" name="<?= htmlspecialchars($name) ?>" value="<?= htmlspecialchars($value ?? '') ?>">
<?php endforeach; ?>

View File

@@ -37,12 +37,13 @@
?>
<div class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
<i class="icon fa fa-ban "></i><?= $alert;?>
<i class="icon fa fa-ban "></i><?= htmlspecialchars($alert) ?>
</div>
<? endif;
?>
<form method="POST" action="/admin/" class="form-horizontal" rol="form">
<input type="hidden" name="s-action" value="user-logon" />
<input type="hidden" name="_csrf_token" value="<?= htmlspecialchars(\Shared\Security\CsrfToken::getToken()) ?>">
<div class="form-group form-inline row">
<div class="col-12">
<div class="input-group input-login">

View File

@@ -1,5 +1,6 @@
<form method="POST" action="/admin/" class="form-horizontal" rol="form">
<input type="hidden" name="s-action" value="user-2fa-verify">
<input type="hidden" name="_csrf_token" value="<?= htmlspecialchars(\Shared\Security\CsrfToken::getToken()) ?>">
<div class="form-group row">
<label class="col col-sm-4 control-label" for="login">Kod z e-maila:</label>
<div class="col col-sm-8">
@@ -14,5 +15,6 @@
</form>
<form method="POST" action="/admin/" style="margin-top:10px">
<input type="hidden" name="s-action" value="user-2fa-resend">
<input type="hidden" name="_csrf_token" value="<?= htmlspecialchars(\Shared\Security\CsrfToken::getToken()) ?>">
<button class="btn btn-danger">Wyślij kod ponownie</button>
</form>

View File

@@ -0,0 +1,26 @@
<?php
namespace Shared\Security;
class CsrfToken
{
const SESSION_KEY = 'csrf_token';
public static function getToken(): string
{
if (empty($_SESSION[self::SESSION_KEY])) {
$_SESSION[self::SESSION_KEY] = bin2hex(random_bytes(32));
}
return (string) $_SESSION[self::SESSION_KEY];
}
public static function validate(string $token): bool
{
$sessionToken = isset($_SESSION[self::SESSION_KEY]) ? (string) $_SESSION[self::SESSION_KEY] : '';
return $sessionToken !== '' && hash_equals($sessionToken, $token);
}
public static function regenerate(): void
{
$_SESSION[self::SESSION_KEY] = bin2hex(random_bytes(32));
}
}

View File

@@ -43,6 +43,15 @@ class App
$sa = \Shared\Helpers\Helpers::get( 's-action' );
if ( !$sa ) return;
if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) {
$csrfToken = isset( $_POST['_csrf_token'] ) ? (string) $_POST['_csrf_token'] : '';
if ( !\Shared\Security\CsrfToken::validate( $csrfToken ) ) {
\Shared\Helpers\Helpers::alert( 'Nieprawidłowy token bezpieczeństwa. Spróbuj ponownie.' );
header( 'Location: /admin/' );
exit;
}
}
$domain = preg_replace( '/^www\./', '', $_SERVER['SERVER_NAME'] );
$cookie_name = 'admin_remember_' . str_replace( '.', '-', $domain );
$users = new \Domain\User\UserRepository( $mdb );
@@ -84,6 +93,7 @@ class App
exit;
}
\Shared\Security\CsrfToken::regenerate();
self::finalize_admin_login( $user, $domain, $cookie_name, (bool) \Shared\Helpers\Helpers::get( 'remember' ) );
header( 'Location: /admin/articles/list/' );
exit;
@@ -127,6 +137,7 @@ class App
header( 'Location: /admin/' );
exit;
}
\Shared\Security\CsrfToken::regenerate();
self::finalize_admin_login( $user, $domain, $cookie_name, !empty( $pending['remember'] ) );
header( 'Location: /admin/articles/list/' );
exit;

View File

@@ -32,6 +32,13 @@ class FormRequestHandler
'data' => []
];
// Walidacja CSRF
$csrfToken = isset($postData['_csrf_token']) ? (string) $postData['_csrf_token'] : '';
if (!\Shared\Security\CsrfToken::validate($csrfToken)) {
$result['errors'] = ['csrf' => 'Nieprawidłowy token bezpieczeństwa. Odśwież stronę i spróbuj ponownie.'];
return $result;
}
// Walidacja
$errors = $this->validator->validate($postData, $formViewModel->fields, $formViewModel->languages);

View File

@@ -4,6 +4,18 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
---
## ver. 0.337 (2026-03-12) - Bezpieczeństwo: ochrona CSRF panelu administracyjnego
- **SECURITY**: `autoload/Shared/Security/CsrfToken.php` — nowa klasa z `getToken()`, `validate()`, `regenerate()` (token 64-znakowy hex, `hash_equals()` przeciw timing attacks)
- **SECURITY**: `admin/templates/components/form-edit.php` — dodano ukryte pole `_csrf_token` we wszystkich formularzach edycji
- **SECURITY**: `autoload/admin/Support/Forms/FormRequestHandler::handleSubmit()` — walidacja CSRF przed przetworzeniem danych formularza
- **SECURITY**: `admin/templates/site/unlogged-layout.php` — token CSRF w formularzu logowania + fix XSS na komunikacie alertu (`htmlspecialchars`)
- **SECURITY**: `admin/templates/users/user-2fa.php` — token CSRF w obu formularzach 2FA (weryfikacja i resend)
- **SECURITY**: `autoload/admin/App::special_actions()` — walidacja CSRF dla żądań POST; regeneracja tokenu po udanym logowaniu (obie ścieżki: bezpośrednia i przez 2FA)
- **TEST**: `tests/Unit/Shared/Security/CsrfTokenTest.php` — 7 nowych testów; suite: 817 testów, 2271 asercji
---
## ver. 0.336 (2026-03-12) - Poprawki bezpieczeństwa: error handling w krytycznych ścieżkach
- **FIX**: `cron.php` — przywrócono `E_WARNING` i `E_DEPRECATED` (wyciszano je od zawsze, ukrywając potencjalne błędy)

View File

@@ -23,10 +23,10 @@ composer test # standard
## Aktualny stan
```text
OK (810 tests, 2264 assertions)
OK (817 tests, 2271 assertions)
```
Zweryfikowano: 2026-03-10 (ver. 0.333)
Zweryfikowano: 2026-03-12 (ver. 0.337)
## Konfiguracja
@@ -71,6 +71,9 @@ tests/
| | |-- Transport/TransportRepositoryTest.php
| | |-- Update/UpdateRepositoryTest.php
| | `-- User/UserRepositoryTest.php
| |-- Shared/
| | `-- Security/
| | `-- CsrfTokenTest.php
| `-- admin/
| `-- Controllers/
| |-- ArticlesControllerTest.php

View File

@@ -0,0 +1,60 @@
<?php
namespace Tests\Unit\Shared\Security;
use PHPUnit\Framework\TestCase;
use Shared\Security\CsrfToken;
class CsrfTokenTest extends TestCase
{
protected function setUp(): void
{
$_SESSION = [];
}
public function testGetTokenReturns64CharHexString(): void
{
$token = CsrfToken::getToken();
$this->assertIsString($token);
$this->assertSame(64, strlen($token));
$this->assertMatchesRegularExpression('/^[0-9a-f]{64}$/', $token);
}
public function testGetTokenIsIdempotent(): void
{
$first = CsrfToken::getToken();
$second = CsrfToken::getToken();
$this->assertSame($first, $second);
}
public function testValidateReturnsTrueForCorrectToken(): void
{
$token = CsrfToken::getToken();
$this->assertTrue(CsrfToken::validate($token));
}
public function testValidateReturnsFalseForEmptyString(): void
{
CsrfToken::getToken();
$this->assertFalse(CsrfToken::validate(''));
}
public function testValidateReturnsFalseForWrongToken(): void
{
CsrfToken::getToken();
$this->assertFalse(CsrfToken::validate('aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899'));
}
public function testValidateReturnsFalseWhenNoSessionToken(): void
{
$this->assertFalse(CsrfToken::validate('sometoken'));
}
public function testRegenerateChangesToken(): void
{
$before = CsrfToken::getToken();
CsrfToken::regenerate();
$after = CsrfToken::getToken();
$this->assertNotSame($before, $after);
$this->assertSame(64, strlen($after));
}
}

View File

@@ -17,6 +17,7 @@ if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
'admin\\ViewModels\\Forms\\' => __DIR__ . '/../autoload/admin/ViewModels/Forms/',
'admin\\Validation\\' => __DIR__ . '/../autoload/admin/Validation/',
'api\\' => __DIR__ . '/../autoload/api/',
'Shared\\Security\\' => __DIR__ . '/../autoload/Shared/Security/',
];
foreach ($prefixes as $prefix => $baseDir) {

BIN
updates/0.30/ver_0.336.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,26 @@
{
"changelog": "Poprawki error handling: try-catch tokeny Apilo, przywrocenie E_WARNING w cron, error_log w safeUnlink",
"version": "0.336",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"autoload/Domain/Article/ArticleRepository.php",
"autoload/Domain/Integrations/IntegrationsRepository.php",
"autoload/Domain/Product/ProductRepository.php",
"cron.php"
]
},
"checksum_zip": "sha256:15d4be791a2ee766e8e66ef4b2da61fd6efd0d0331a15c3b4ceed1a3702f7173",
"sql": [
],
"date": "2026-03-12",
"directories_deleted": [
]
}

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
<?
$current_ver = 335;
$current_ver = 336;
for ($i = 1; $i <= $current_ver; $i++)
{