# Audyt Backend – Piekarnia Jędryka (PHP / Slim 4)

Data audytu: 19.07.2026
Zakres: `backend/` (Slim 4, PDO/MySQL, JWT własnej implementacji, GD do obrazków)

Legenda priorytetów: 🔴 Krytyczny 🟠 Wysoki 🟡 Średni 🟢 Niski / kosmetyczny

---

## 1. BEZPIECZEŃSTWO

### 🔴 1.1. `displayErrorDetails = true` w produkcji
```php
$app->addErrorMiddleware(true, true, true);
```
Pierwszy parametr `true` włącza wyświetlanie pełnych stack trace'ów (ścieżki serwera, zapytania SQL, dane środowiskowe) w odpowiedzi HTTP przy każdym nieobsłużonym wyjątku. To poważny wyciek informacji ułatwiający atak.
**Rekomendacja:** uzależnić od `APP_ENV`/`APP_DEBUG` z `.env`:
```php
$debug = (getenv('APP_ENV') ?: 'production') !== 'production';
$app->addErrorMiddleware($debug, $debug, $debug);
```
oraz logować błędy do pliku zamiast pokazywać je klientowi.

### 🟠 1.2. Brak weryfikacji istnienia admina przy każdym żądaniu (JWT stateless)
`AuthMiddleware` ufa wyłącznie podpisowi tokena — nie sprawdza, czy `admin_id` nadal istnieje w bazie/czy konto nie zostało zablokowane. Usunięcie/zablokowanie admina nie unieważnia już wydanych tokenów (ważnych 24h).
**Rekomendacja:** albo krótszy czas życia tokena + refresh token, albo tabela `revoked_tokens` / `token_version` w tabeli `admins` sprawdzana przy dekodowaniu.

### 🟠 1.3. Brak rate-limitingu na `/api/login` i `/api/contact-messages`
Jedyna ochrona logowania to `sleep(1)` po błędnym haśle — nie chroni to przed rozproszonym brute-force (wiele równoległych żądań) ani nie ma limitu prób per IP/login. Formularz kontaktowy może być spamowany (mailowany + zapisywany do DB) bez żadnego throttlingu/captchy.
**Rekomendacja:** wprowadzić prosty rate-limiter (np. tabela `login_attempts`/`request_log` z IP + timestamp, limit np. 5 prób / 15 min) oraz honeypot/captcha na formularzu kontaktowym.

### ✅ 1.4. CORS: `Access-Control-Allow-Origin: *` jako fallback — NAPRAWIONE
Zunifikowano logikę w `ResponseHelper::resolveAllowedOrigin()` (używanej też przez `routes.php` dla `OPTIONS`):
- Origin żądania na liście `CORS_ALLOWED_ORIGINS` → zwracany dokładnie (echo).
- Brak konfiguracji + `APP_ENV=production` → nagłówek CORS w ogóle nie jest ustawiany.
- Brak konfiguracji + dev → fallback do `*` (wygoda lokalnego developmentu).
- Konfiguracja ustawiona, ale Origin nie pasuje → nagłówek **nie jest ustawiany** (usunięto fallback do pierwszego originu z listy).


### 🟠 1.5. Brak walidacji rozszerzenia/typu MIME z prawdziwej zawartości + brak limitu wymiarów obrazka
`ImageService` sprawdza `getimagesize()` (dobre), ale nie ogranicza maksymalnych wymiarów (`width x height`) przed próbą `imagecreatetruecolor`/resample — atakujący może wysłać bardzo mały plik z ogromną rozdzielczością (tzw. "decompression bomb"), co może zużyć całą dostępną pamięć/CPU serwera (DoS).
**Rekomendacja:** dodać limit np. `width * height <= 50_000_000` przed przetwarzaniem, odrzucać obrazy przekraczające limit.

### ✅ 1.6. Brak walidacji `category_slug`, `slug` pod kątem znaków specjalnych — NAPRAWIONE
`CategoryController::create()` i `update()` wymuszają teraz regex `^[a-z0-9\-]+$` na polu `slug` (zwracają 400 przy niezgodności). Frontend (admin) już generował slug zgodny z tym wzorcem, więc zmiana nie wprowadza regresji.


### 🟡 1.7. Brak globalnego limitu wielkości requestu / `getUploadedFiles`
Limit rozmiaru pliku (10MB) jest sprawdzany dopiero w `ImageService`. Warto upewnić się, że `php.ini`/`.htaccess` produkcyjne mają sensowne twarde limity (np. 10-15MB) niezależne od kodu aplikacji.

### 🟡 1.8. `JwtHelper` – własna, ręczna implementacja JWT
Biblioteka JWT jest napisana ręcznie (HMAC-SHA256, poprawnie użyty `hash_equals`). Ryzyko: brak obsługi `alg: none`, brak `iss`/`aud` claims, brak `jti`/rewokacji.
**Rekomendacja:** rozważyć `firebase/php-jwt` (mała, sprawdzona biblioteka) zamiast własnej implementacji.

### 🟡 1.9. Brak nagłówka `Content-Security-Policy` / `Strict-Transport-Security`
`ResponseHelper` ustawia `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` — dobrze, ale brakuje `Strict-Transport-Security` (jeśli serwowane po HTTPS) oraz rozważenia CSP dla panelu admina (`public/admin`).

### 🟢 1.10. `admins` – brak informacji o polityce haseł / 2FA
Zakładam `password_hash()` (bcrypt/argon2, OK). Warto rozważyć wymuszenie silnych haseł oraz opcjonalne 2FA dla panelu admina.

### 🟢 1.11. Sekrety w `.env.example`
Plik zawiera jawne placeholdery — OK, to tylko przykład. Upewnić się, że rzeczywisty `.env` nigdy nie trafia do repo (jest w `.gitignore` ✅).

### 🟡 1.12. Skrypty developerskie w katalogu głównym backendu
`get_schema.php`, `init_search.php`, `migrate_thumbnails.php`, `test_reorder.php`, `scripts/migrate.php`, `scripts/scrape_products.php`, `scripts/seed_products.php` — są dostępne pod publicznym URL-em, **jeśli** root serwera WWW wskazuje na katalog `backend/` zamiast `backend/public/`.
**Rekomendacja:**
- Upewnić się, że DocumentRoot na produkcji wskazuje **wyłącznie** na `backend/public/`.
- Przenieść te skrypty poza webroot albo dodać guard: `if (php_sapi_name() !== 'cli') { http_response_code(403); exit; }`.

---

## 2. OPTYMALIZACJA / WYDAJNOŚĆ

### 🟠 2.1. Brak paginacji na listach (`products`, `contact-messages`, `gallery`)
`ProductController::getAll`, `ContactController::getAll`, `GalleryController::getAll` zwracają **wszystkie** rekordy za jednym razem.
**Rekomendacja:** dodać `LIMIT`/`OFFSET` lub keyset pagination, przynajmniej dla `contact-messages` (rosnąca tabela) i `gallery`.

### 🟡 2.2. `SELECT *` zamiast wskazanych kolumn
Wiele zapytań używa `SELECT *`, co utrudnia utrzymanie i przesyła niepotrzebne dane.

### 🟡 2.3. Podwójne zapytanie przy tworzeniu rekordu (`INSERT` + `UPDATE image_url`)
`ProductController::create`, `GalleryController::create` najpierw wstawiają rekord z pustym `image_url`, potem robią drugi `UPDATE`. Dodatkowy round-trip do bazy, brak transakcji obejmującej insert+update+plik.

### 🟢 2.4. Połączenie PDO jako singleton – OK
`Database::getConnection()` poprawnie cache'uje połączenie (static). Bez zmian.

### 🟡 2.5. Brak indeksów – do weryfikacji
Warto zweryfikować indeksy na: `products.category_slug`, `products.is_available`, `contact_messages.type`, `contact_messages.created_at`, `gallery_photos.section`, `gallery_photos.order_index`.

### 🟢 2.6. Przetwarzanie obrazków (GD) blokuje request
Każdy upload obrazka wykonywany jest synchronicznie. Przy skalowaniu warto rozważyć kolejkę/async processing.

---

## 3. BUGI / BŁĘDY LOGICZNE

### 🟠 3.1. Osierocony rekord w bazie przy błędzie uploadu obrazka produktu
W `ProductController::create`, jeśli insert się powiedzie, ale upload rzuci wyjątek, kontroler zwraca błąd 400 — **ale produkt zostaje w bazie** z pustym `image_url` (w przeciwieństwie do `GalleryController::create`, który poprawnie robi `DELETE`).
**Rekomendacja:** ujednolicić zachowanie — usuwać rekord przy błędzie uploadu (jak w Gallery).

### 🟡 3.2. `ProductController::isAdmin` – wykrywanie admina tylko po obecności nagłówka
```php
$isAdmin = str_starts_with($authHeader, 'Bearer ') && strlen($authHeader) > 7;
```
To **nie weryfikuje tokena** — każdy może wysłać dowolny `Authorization: Bearer cokolwiek` i zobaczyć niedostępne produkty. Niski realny impact, ale niespójne z resztą aplikacji.

### 🟢 3.3. `ContactController::create` nadpisuje zmienną `$body`
Zła praktyka nazewnicza, ale bez błędu funkcjonalnego.

### 🟢 3.4. Brak spójnej walidacji `getParsedBody()` jako tablicy
Do ujednolicenia: wszędzie `$request->getParsedBody() ?? []`.

---

## 4. JAKOŚĆ KODU / UTRZYMANIE

- ✅ Duplikacja `slugify()` w `ProductController`, `ShopController`, `GalleryController` — wydzielona do `App\Helpers\SlugHelper::slugify()`.
- ✅ Duplikacja logiki usuwania plików/katalogów — wydzielona do `App\Helpers\FileHelper` (`deletePublicFile`, `deleteDirectory`, `deleteDirectoryIfEmpty`).
- 🟢 Brak testów automatycznych.
- 🟢 Brak statycznej analizy (PHPStan/Psalm).


---

## 5. PODSUMOWANIE — LISTA PRIORYTETOWA DO WDROŻENIA

| # | Priorytet | Problem | Plik | Status |
|---|-----------|---------|------|--------|
| 1 | 🔴 | `displayErrorDetails=true` na produkcji | `public/index.php` | ✅ |
| 2 | 🟠 | Brak rate-limitingu logowania/kontaktu | `AuthController`, `ContactController` | ✅ |
| 3 | 🟠 | JWT nie weryfikuje istnienia/aktywności admina | `AuthMiddleware`, `JwtHelper` | ✅ |
| 4 | 🟠 | Skrypty dev dostępne pod webrootem | `get_schema.php`, `scripts/*` | ✅ |
| 5 | 🟠 | Brak limitu wymiarów obrazka (decompression bomb) | `ImageService` | ✅ |
| 6 | 🟠 | Osierocone rekordy produktów przy błędzie uploadu | `ProductController::create` | ✅ |
| 7 | 🟡 | Brak paginacji na listach | `ProductController`, `ContactController`, `GalleryController` | ⬜ |
| 8 | 🟡 | CORS fallback do `*` | `ResponseHelper`, `routes.php` | ✅ |
| 9 | 🟡 | Walidacja sluga kategorii | `CategoryController` | ✅ |
| 10 | 🟢 | Refaktoryzacja duplikacji (slugify, file delete) | wiele plików | ✅ |




---

**Uwaga końcowa:** ogólna jakość kodu backendu jest dobra jak na projekt tej wielkości — konsekwentne używanie prepared statements (brak SQL Injection), poprawne hashowanie haseł (`password_verify`), sensowna struktura MVC. Główne ryzyka dotyczą konfiguracji środowiska produkcyjnego (debug mode, webroot) oraz braku ochrony przed nadużyciami (rate limiting, walidacja rozmiaru obrazków).
