# Security & Performance Patch v3.25.0

## 1. Pronađeni problemi

- Login rate limit je bio vezan za PHP sesiju (`$_SESSION['login_attempts']`), pa ga je nova sesija ili obrisan cookie resetovao.
- Session bootstrap nije eksplicitno uključivao strict mode, cookie-only sesije i zabranu URL session ID-a.
- HTTPS/IP detekcija nije imala eksplicitan trusted-proxy model.
- `add_participant` nije imao posebnu serversku autorizaciju; običan učesnik koji vidi tiket mogao je ručno da pošalje POST.
- `participant_search.php` je bio globalni imenik za svaki autentifikovani nalog.
- `tickets.php` je završavao listu na `LIMIT 200` bez ukupnog broja i pravih stranica.
- QA/runtime PHP, QA JavaScript, SQL dump i deo razvojne dokumentacije nisu bili potpuno obuhvaćeni produkcionom web zaštitom.
- SQL šema je već imala većinu potrebnih indeksa, ali nije imala persistent login tabelu ni kompozitne indekse koji prate sortiranje komentara/događaja po tiketu.
- U CSS-u su postojale dve dokazano identične globalne deklaracije.

## 2. Izmenjeni fajlovi

- `app/request.php` — trusted-proxy-aware HTTPS i IP detekcija.
- `app/security.php` — persistent login rate limit, HMAC identifikatori i security log događaji.
- `app/bootstrap.php` — session hardening pre `session_start()` i učitavanje security modula.
- `config.php`, `config.example.php` — HTTPS-only cookie/proxy konfiguracija i opcioni rate-limit secret.
- `login.php` — uklonjen session limiter i uvedena DB politika sa delay/429 blokadom.
- `app/functions.php` — centralna `can_manage_ticket_participants()` dozvola.
- `ticket_view.php` — serverski `403` i uslovni prikaz forme za učesnike.
- `participant_search.php` — ticket-scoped autorizacija; poseban CSRF-zaštićen kontekst za formu novog tiketa.
- `ticket_create.php`, `assets/js/app.js` — prosleđivanje bezbednog konteksta i pravilno dodavanje `q` parametra.
- `tickets.php` — `COUNT(*)`, parametrizovani `LIMIT/OFFSET`, validacija i pagination UI.
- `.htaccess`, `.productionignore` — defense-in-depth zabrana i produkcioni exclude manifest.
- `20260823_security_performance_indexes.sql`, `baza.sql` — nova tabela i provereni indeksi.
- `assets/css/style.css` — uklonjene samo dve identične globalne deklaracije; vrednosti i cascade rezultat ostaju isti.
- `qa_security_performance_v3_25_0.js` i postojeći QA fajlovi — nove kontrole i asset verzija.

## 3. Uvedene kontrole

### Login

- Prozor: 15 minuta.
- Od 5 aktivnih neuspeha: progresivni delay od 0,5 s do najviše 3 s.
- 10 neuspeha za isti email identitet: privremena account-scope blokada.
- 20 neuspeha sa iste IP adrese: privremena IP-scope blokada.
- Blokada automatski nestaje kada pokušaji izađu iz 15-minutnog prozora; nema trajnog zaključavanja.
- Email i IP se čuvaju samo kao HMAC-SHA256 vrednosti. Lozinka, cookie, session ID i CSRF token se ne loguju.
- Uspešna prijava resetuje account-scope brojanje logički preko poslednjeg uspešnog reda; IP zaštita ostaje nezavisna.
- Ako persistent storage nije dostupan, prijava se bezbedno odbija sa generičkom `503` porukom.

### Učesnici

Učesnika na postojećem tiketu mogu dodati samo:

1. podnosilac;
2. trenutno zaduženi korisnik;
3. korisnik sa trenutno odgovornom ulogom;
4. sistem administrator.

Običan učesnik zadržava postojeće pravo pregleda/komentarisanja, ali nema formu, search pristup niti POST pravo za dodavanje drugih učesnika. Ručni pokušaj vraća `403`.

Za novi tiket, gde ID još ne postoji, postojeći autocomplete je sačuvan kroz poseban `context=create`, uz validan CSRF token i postojeće `can_create_ticket()` pravo.

### Sesije

- `session.use_strict_mode=1`
- `session.use_only_cookies=1`
- `session.use_trans_sid=0`
- Postojeći HttpOnly, SameSite=Lax, regeneracija ID-a, CSRF i timeout nisu menjani.
- `APP_FORCE_SECURE_COOKIE=true` forsira Secure cookie u HTTPS-only produkciji.
- `X-Forwarded-For/Proto` se veruju samo ako je `REMOTE_ADDR` naveden u `TRUSTED_PROXY_IPS`.

## 4. SQL migracija i indeksi

Pre postavljanja PHP patch-a izvršiti:

```text
20260823_security_performance_indexes.sql
```

Migracija dodaje:

- `login_attempts` sa email/IP/success/time indeksima i posebnim indeksom za čišćenje;
- `idx_comments_ticket_created_id (ticket_id, created_at, id)`;
- `idx_events_ticket_created_id (ticket_id, created_at, id)`.

Prethodno je u `baza.sql` potvrđeno da već postoje:

- `tickets`: user, assigned, status/priority, updated i role/status/updated indeksi;
- `ticket_participants`: `PRIMARY KEY(ticket_id,user_id)` i reverse user/ticket indeks;
- `user_roles`: unique user/role i reverse role/user indeks;
- `notifications`: user/read/created composite indeks;
- `equipment`: user/status composite indeks.

Zato ti indeksi nisu duplirani. Živa baza nije bila dostupna iz lokalnog okruženja; pre produkcije preporučeno je potvrditi `SHOW INDEX` i napraviti backup.

## 5. Produkcioni deployment

1. Napraviti backup baze i trenutnih aplikacionih fajlova.
2. Na staging/maintenance prozoru izvršiti SQL migraciju.
3. U `config.php` postaviti jedinstveni `APP_RATE_LIMIT_SECRET` od najmanje 32 nasumična karaktera. Ako nije postavljen, postoji server-specific fallback izveden iz DB konfiguracije.
4. Ako postoji reverse proxy, u `TRUSTED_PROXY_IPS` navesti samo njegove neposredne IP adrese.
5. Deploy paket napraviti uz `.productionignore`; QA, SQL, Markdown, upgrade, `create_admin.php`, lokalni upload i log fajlovi ne ulaze u web root.
6. `.htaccess` mora ostati u produkciji kao dodatna zaštita. Proveriti da QA/SQL/MD URL vraća `403`.

## 6. Testiranje

### Participant autorizacija

- Kao podnosilac, zaduženi, korisnik odgovorne uloge i administrator: otvoriti tiket, pretražiti i dodati učesnika.
- Kao običan učesnik: potvrditi da forma nije prikazana; poslati ručni `add_participant` POST sa validnim CSRF tokenom i očekivati `403`.
- Kao običan učesnik ručno pozvati `participant_search.php?ticket_id=ID&q=pe` i očekivati `403`.
- Kao nevezan korisnik potvrditi postojeći `404`/zabranu pregleda tiketa.

### Login rate limit

- Na staging nalogu izvršiti neuspehe u istoj i novoj sesiji, uz brisanje session cookie-ja između pokušaja.
- Potvrditi progresivni delay nakon pet neuspeha, account `429` nakon deset i IP `429` nakon dvadeset neuspeha prema više identiteta.
- Proveriti `Retry-After` i generičke poruke.
- Nakon izlaska pokušaja iz 15-minutnog prozora potvrditi uspešnu prijavu.

### Pagination

- Proveriti bez filtera, svaki pojedinačni filter, pretragu i kombinacije.
- Proveriti prvu, srednju i poslednju stranicu.
- Proveriti `page=0`, `page=-1`, string, ekstremno veliku vrednost i stranicu veću od ukupnog broja.
- Potvrditi da linkovi čuvaju `view`, `q`, `status`, `priority` i `per_page`.

### Automatizovane provere

Pokrenuti `node qa_security_performance_v3_25_0.js`, pa sve postojeće `qa_*.js` testove. PHP runtime QA treba dodatno pokrenuti u staging okruženju sa instaliranim PHP-om i migriranom bazom.

Lokalno su uspešno prošli novi security/performance test i svi postojeći QA testovi čiji su ulazni fajlovi prisutni u repozitorijumu. PHP CLI i živa MariaDB instanca nisu dostupni u ovom lokalnom okruženju, pa `php -l`, izvršavanje migracije i runtime auth/DB scenariji moraju biti završeni na stagingu. Nekoliko istorijskih QA skripti zavisi od ranijih SQL/migration fajlova koji više nisu u repozitorijumu; to je postojeće ograničenje test paketa, a ne rezultat ovog patch-a.

## 7. Rollback

1. Vratiti prethodne PHP/JS/CSS/`.htaccess` fajlove iz backupa.
2. Vratiti prethodnu asset verziju u `app/header.php`.
3. `login_attempts` može ostati bez uticaja na staru aplikaciju; po potrebi ga ukloniti tek nakon arhiviranja: `DROP TABLE login_attempts`.
4. Novi kompozitni indeksi mogu ostati; ako je rollback obavezan, ukloniti ih sa `DROP INDEX ... ON ...`.
5. Ne vraćati bazu iz starog backupa osim ako je to deo kontrolisanog disaster-recovery postupka, jer bi se izgubili novi produkcioni tiketi.

## 8. Nije menjano

- boje, fontovi, ikonice, kartice, meniji i responsive breakpoint-i;
- korisničke role i postojeći ticket workflow;
- komentari, prilozi, obaveštenja, oprema, autentikacioni identitet i CSRF model;
- `can_view_ticket()` ponašanje za postojeće učesnike;
- page-specific JavaScript nije razdvojen jer bi promena redosleda učitavanja nosila nepotreban regresioni rizik.
