Razvoj PHP aplikacije danas ne podrazumeva samo da forma radi, da se podaci snime u bazu i da korisnik može da se prijavi. Ako aplikacija nije bezbedna, svaka od tih funkcija može postati ulazna tačka za napad. OWASP i dalje među najvažnije rizike ubraja injection probleme, XSS, CSRF zaštitu kroz preporučene mere i posebno Broken Access Control kao jednu od najkritičnijih kategorija.

U praksi se četiri problema posebno često pojavljuju u PHP aplikacijama:

  • SQL Injection
  • CSRF
  • XSS
  • Broken Access Control

Ono što je važno jeste da se svaki od njih može veoma uspešno sprečiti ako se bezbednost uvede kao standard razvoja, a ne kao naknadna zakrpa. OWASP za SQL injection preporučuje prepared statements, za CSRF nepredvidive tokene i dodatne kontrole, za XSS kontekstualno escape-ovanje izlaza, a za access control centralizovanu autorizaciju i “deny by default”.

SQL Injection

SQL Injection nastaje kada aplikacija uzima korisnički unos i direktno ga ubacuje u SQL upit. OWASP navodi da se problem javlja kada se dinamički upiti grade konkatenacijom stringova i da je osnovna odbrana upotreba prepared statements sa parametrima.

Kako izgleda problem u praksi

Na primer, korisnik unosi email u login formu ili polje za pretragu. Ako aplikacija taj unos samo zalepi u SQL string, napadač može promeniti logiku upita.

Loš primer

<?php
$email = $_POST['email'] ?? '';$sql = "SELECT * FROM users WHERE email = '$email'";
$stmt = $pdo->query($sql);
$user = $stmt->fetch();

Ovde je problem što se korisnički unos direktno umeće u SQL. Ako neko pošalje posebno formiran unos, može pokušati da promeni značenje upita. OWASP izričito preporučuje da se prestane sa pisanjem dinamičkih query stringova konkatenacijom.

Ispravan primer

<?php
$email = $_POST['email'] ?? '';$sql = "SELECT * FROM users WHERE email = :email";
$stmt = $pdo->prepare($sql);
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();

Ovde baza jasno razlikuje SQL komandu od podatka. Korisnički unos više nije deo SQL sintakse nego vrednost parametra, što je upravo preporučeni OWASP pristup.

Još jedan praktičan problem: dinamički ORDER BY

Prepared statements ne rešavaju sve ako dozvoliš korisniku da bira naziv kolone bez kontrole.

Loš primer

<?php
$sort = $_GET['sort'] ?? 'id';$sql = "SELECT id, ime, email FROM users ORDER BY $sort";
$stmt = $pdo->query($sql);
$rows = $stmt->fetchAll();

Ispravan primer

<?php
$allowedSort = ['id', 'ime', 'email'];
$sort = $_GET['sort'] ?? 'id';if (!in_array($sort, $allowedSort, true)) {
$sort = 'id';
}

$sql = "SELECT id, ime, email FROM users ORDER BY $sort";
$stmt = $pdo->query($sql);
$rows = $stmt->fetchAll();

Ovde koristimo allow-list validaciju za delove upita koji ne mogu biti obični bind parametri. To je takođe pristup koji OWASP preporučuje kao dodatnu zaštitu.

CSRF

CSRF znači Cross-Site Request Forgery. To je napad u kome browser prijavljenog korisnika pošalje neželjen zahtev prema vašoj aplikaciji. Problem nastaje zato što browser automatski šalje session cookie, pa server bez dodatne zaštite može poverovati da je zahtev legitiman. OWASP za zaštitu preporučuje nepredvidiv CSRF token, kao i dodatne mere poput SameSite cookie atributa i provere porekla zahteva.

Kako izgleda problem u praksi

Korisnik je prijavljen u admin panel i otvara drugu stranicu. Ta stranica pokušava da pošalje POST zahtev ka vašem sistemu za promenu email adrese, lozinke ili statusa zapisa.

Loš primer

<?php
session_start();
require 'db.php';if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'] ?? '';

$stmt = $pdo->prepare("UPDATE users SET email = :email WHERE id = :id");
$stmt->execute([
'email' => $email,
'id' => $_SESSION['user_id']
]);

echo "Email je promenjen.";
}

Ovde server veruje svakom POST zahtevu koji dođe uz validnu sesiju. To je upravo situacija koju CSRF napad koristi.

Ispravan primer

<?php
session_start();
require 'db.php';if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['csrf_token'] ?? '';

if (!hash_equals($_SESSION['csrf_token'], $token)) {
http_response_code(403);
exit('Neispravan CSRF token.');
}

$email = $_POST['email'] ?? '';

$stmt = $pdo->prepare("UPDATE users SET email = :email WHERE id = :id");
$stmt->execute([
'email' => $email,
'id' => $_SESSION['user_id']
]);

echo "Email je promenjen.";
}

HTML forma:

<form method="post">
<input type="email" name="email" required>
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8') ?>">
<button type="submit">Sačuvaj</button>
</form>

Ovde server proverava da li je zahtev stigao sa ispravnim tokenom. Napadač koji nema pristup tokenu ne može lako da podmetne validan zahtev. OWASP preporučuje upravo ovakav princip sinhronizovanog tokena.

XSS

XSS znači Cross-Site Scripting. To je ranjivost kod koje aplikacija prikaže nepouzdan unos kao aktivan sadržaj, najčešće JavaScript. OWASP naglašava da je najvažnija odbrana escape output according to context, odnosno escape-ovanje izlaza prema mestu na kome se prikazuje.

Kako izgleda problem u praksi

Korisnik ostavi komentar, opis, poruku ili ime firme, a sistem to kasnije prikaže drugima bez zaštite.

Loš primer

<?php
$comment = $_POST['comment'] ?? '';
echo "<div class='comment'>$comment</div>";

Ako korisnik unese zlonameran HTML ili JavaScript, on može biti prikazan i izvršen u browseru drugih korisnika. To je klasičan stored XSS scenario.

Ispravan primer

<?php
$comment = $_POST['comment'] ?? '';
$safeComment = htmlspecialchars($comment, ENT_QUOTES, 'UTF-8');echo "<div class='comment'>{$safeComment}</div>";

Ovde korisnički unos više nije tretiran kao HTML ili skripta, već kao običan tekst. To je osnovna i najčešća zaštita za HTML body kontekst.

Problem u HTML atributu

XSS nije isti u svim kontekstima.

Loš primer

<?php
$name = $_GET['name'] ?? '';
echo "<input type='text' value='$name'>";

Ispravan primer

<?php
$name = $_GET['name'] ?? '';
$safeName = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');echo "<input type='text' value='{$safeName}'>";

OWASP posebno naglašava da zaštita zavisi od konteksta: HTML tekst, atribut, JavaScript, URL i CSS nisu isti.

Kada escape nije dovoljan

Ako dozvoljavaš rich text editor i deo HTML-a mora ostati sačuvan, tada nije dovoljno samo htmlspecialchars(). Tada treba raditi sanitizaciju po dozvoljenoj listi tagova, a ne puštati kompletan korisnički HTML bez kontrole. To je u skladu sa OWASP smernicama da output encoding nije dovoljan za sve HTML use case-ove i da ponekad treba koristiti sanitizer.

Broken Access Control

Broken Access Control znači da aplikacija ne proverava ispravno šta korisnik sme da vidi ili uradi. OWASP ovo i dalje tretira kao jednu od najkritičnijih kategorija i preporučuje centralizovanu autorizaciju i “deny by default”.

Kako izgleda problem u praksi

Korisnik vidi svoj dokument na URL-u:

document.php?id=25

Ako ručno promeni URL u:

document.php?id=26

i sistem mu prikaže tuđ dokument, imate problem kontrole pristupa.

Loš primer

<?php
session_start();
require 'db.php';$id = (int)($_GET['id'] ?? 0);

$stmt = $pdo->prepare("SELECT * FROM documents WHERE id = :id");
$stmt->execute(['id' => $id]);
$document = $stmt->fetch();

if (!$document) {
exit('Dokument ne postoji.');
}

echo "<h1>" . htmlspecialchars($document['title'], ENT_QUOTES, 'UTF-8') . "</h1>";
echo "<p>" . htmlspecialchars($document['content'], ENT_QUOTES, 'UTF-8') . "</p>";

Ovde proveravaš da li dokument postoji, ali ne proveravaš da li korisnik sme da ga vidi. To je tipičan IDOR/Broken Access Control problem. OWASP za ovu kategoriju posebno ističe potrebu da se proverava vlasništvo i ovlašćenje za svaki zahtev.

Ispravan primer

<?php
session_start();
require 'db.php';if (empty($_SESSION['user_id'])) {
http_response_code(401);
exit('Morate biti prijavljeni.');
}

$id = (int)($_GET['id'] ?? 0);

$stmt = $pdo->prepare("
SELECT *
FROM documents
WHERE id = :id AND owner_id = :owner_id
");
$stmt->execute([
'id' => $id,
'owner_id' => $_SESSION['user_id']
]);

$document = $stmt->fetch();

if (!$document) {
http_response_code(403);
exit('Nemate pravo pristupa ovom dokumentu.');
}

echo "<h1>" . htmlspecialchars($document['title'], ENT_QUOTES, 'UTF-8') . "</h1>";
echo "<p>" . htmlspecialchars($document['content'], ENT_QUOTES, 'UTF-8') . "</p>";

Ovde backend proverava i identitet korisnika i vlasništvo nad dokumentom. To je prava zaštita. Sakrivanje linka u interfejsu nije zaštita; backend autorizacija jeste.

Problem sa admin rutama

Loš primer

<?php
session_start();if (!empty($_SESSION['user_id'])) {
// korisnik je prijavljen, pa puštamo dalje
include 'admin_delete_user.php';
}

Ispravan primer

<?php
session_start();if (empty($_SESSION['user_id'])) {
http_response_code(401);
exit('Niste prijavljeni.');
}

if (($_SESSION['role'] ?? '') !== 'admin') {
http_response_code(403);
exit('Nemate administratorska prava.');
}

include 'admin_delete_user.php';

OWASP preporučuje centralizovano sprovođenje kontrole pristupa i da sve bude zabranjeno dok nije eksplicitno dozvoljeno.

Kako sve ovo povezati u jednoj PHP aplikaciji

U realnoj aplikaciji ove ranjivosti se često javljaju zajedno. Na primer, forma za izmenu dokumenta može istovremeno imati:

  • SQL injection ako se query pravi konkatenacijom,
  • CSRF ako nema token,
  • XSS ako se naslov dokumenta ispisuje bez escape-a,
  • broken access control ako korisnik može menjati tuđ dokument promenom ID-ja.

Zato se bezbednost ne rešava jednom merom, već slojevito. OWASP cheat sheet pristup upravo to i zagovara: više preciznih odbrana za različite vrste problema.

Minimalni bezbednosni standard za PHP aplikaciju

Ako želiš zdrav osnov za PHP aplikaciju, minimum bi trebalo da bude sledeći:

Sve upite radiš preko PDO prepared statements. Sve state-changing forme štitiš CSRF tokenom. Sve korisničke podatke prikazuješ kroz escape prema kontekstu. Sve rute i akcije štitiš centralizovanom autorizacijom i proverom vlasništva nad zapisom. Upravo su to osnovni obrasci koje OWASP preporučuje za ove kategorije ranjivosti.

Zaključak

SQL Injection, CSRF, XSS i Broken Access Control nisu apstraktni bezbednosni pojmovi, već konkretni problemi koji se vrlo lako pojave u svakodnevnom PHP kodu. SQL Injection nastaje kada korisnički unos tretiraš kao deo SQL komande. CSRF nastaje kada server veruje svakom zahtevu koji dolazi uz aktivnu sesiju. XSS nastaje kada nepouzdan unos prikažeš bez pravilnog escape-a. Broken Access Control nastaje kada ne proveravaš ko šta zaista sme da vidi ili menja. OWASP za svaki od ovih problema daje jasne i praktične smernice: prepared statements, CSRF tokeni, kontekstualno escape-ovanje i centralizovana kontrola pristupa.

Pristupačnost