Creare API REST sicure in PHP senza framework
Guida pratica alla creazione di una piccola API REST in PHP senza framework, con routing, JSON, PDO, autenticazione tramite token, autorizzazione per oggetto, gestione degli errori e protezioni essenziali.
Un framework accelera lo sviluppo, ma non è indispensabile per comprendere o costruire una piccola API REST. PHP dispone già degli strumenti necessari per leggere richieste HTTP, elaborare JSON, accedere a MySQL con PDO e restituire risposte coerenti. L'assenza di framework, però, rende esplicita ogni responsabilità: validazione, autenticazione, autorizzazione, errori, limiti e log devono essere progettati consapevolmente.
In questa guida realizzeremo una base didattica per gestire articoli tramite endpoint come GET /api/v1/posts e POST /api/v1/posts. Non è un prodotto completo, ma una struttura solida da adattare e testare.
1. Struttura minima del progetto
api/
├── public/
│ └── index.php
├── src/
│ ├── Database.php
│ ├── Http.php
│ ├── Auth.php
│ └── PostController.php
├── config/
│ └── config.php
└── storage/
└── logs/
Il document root del web server deve puntare a public. In questo modo configurazione, sorgenti e log non sono scaricabili direttamente. Password del database e segreti non devono essere inseriti nel repository: leggili da un file esterno non pubblico o da variabili configurate sul server.
2. Inoltrare le richieste al front controller
Con Nginx, un blocco essenziale può essere:
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ~ /\. {
deny all;
}
Adatta il socket alla versione PHP installata e verifica con nginx -t prima del reload. In produzione disabilita display_errors: dettagli di stack, percorsi e query non devono raggiungere il client.
3. Risposte JSON uniformi
Centralizza le risposte in src/Http.php:
Usa codici HTTP coerenti: 200 per una lettura riuscita, 201 dopo la creazione, 204 per una cancellazione senza corpo, 400 per sintassi non valida, 401 quando manca un'autenticazione valida, 403 quando l'identità è nota ma non autorizzata, 404 per la risorsa assente, 409 per un conflitto e 422 per dati semanticamente non validi.
4. Leggere JSON in modo sicuro
Accetta soltanto il formato atteso, limita la dimensione della richiesta anche nel web server e usa eccezioni per JSON malformato:
'Content-Type non supportato'], 415);
}
$raw = file_get_contents('php://input');
if ($raw === false || strlen($raw) > 1048576) {
jsonResponse(['error' => 'Corpo richiesta non valido'], 413);
}
try {
$data = json_decode($raw, true, 32, JSON_THROW_ON_ERROR);
} catch (JsonException) {
jsonResponse(['error' => 'JSON non valido'], 400);
}
if (!is_array($data)) {
jsonResponse(['error' => 'Oggetto JSON richiesto'], 400);
}
return $data;
}
Il limite applicativo è una seconda difesa; Nginx o Apache devono rifiutare corpi eccessivi prima che raggiungano PHP. Una profondità JSON limitata riduce input patologici.
5. Validare con una lista positiva
Non copiare direttamente l'intero payload nel database. Estrai i campi consentiti e controllali:
function validatePost(array $input): array
{
$title = trim((string)($input['title'] ?? ''));
$content = trim((string)($input['content'] ?? ''));
$errors = [];
if ($title === '' || mb_strlen($title) > 180) {
$errors['title'] = 'Titolo obbligatorio, massimo 180 caratteri';
}
if ($content === '' || mb_strlen($content) > 100000) {
$errors['content'] = 'Contenuto obbligatorio o troppo lungo';
}
if ($errors) {
jsonResponse(['error' => 'Dati non validi', 'fields' => $errors], 422);
}
return ['title' => $title, 'content' => $content];
}
La validazione dipende dal dominio: enum, intervalli, date, relazioni e unicità vanno controllati esplicitamente. Non affidarti all'interfaccia client, perché una API può essere chiamata direttamente.
6. Collegarsi a MySQL con PDO
PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
Usa un account MySQL dedicato con i soli privilegi necessari. L'applicazione non dovrebbe collegarsi come root. Le prepared statement separano query e parametri:
$stmt = $pdo->prepare(
'INSERT INTO posts (user_id, title, content) VALUES (:user_id, :title, :content)'
);
$stmt->execute([
'user_id' => $userId,
'title' => $post['title'],
'content' => $post['content'],
]);
I placeholder proteggono i valori, non identificatori come nomi di colonne o direzioni di ordinamento. Per questi usa una lista chiusa, per esempio accettando soltanto created_at o title.
7. Routing esplicito e metodi consentiti
Nel front controller normalizza percorso e metodo, senza trasformare input arbitrario in nomi di file o funzioni:
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($method === 'GET' && $path === '/api/v1/posts') {
listPosts($pdo);
}
if ($method === 'POST' && $path === '/api/v1/posts') {
createPost($pdo);
}
header('Allow: GET, POST');
jsonResponse(['error' => 'Endpoint non trovato'], 404);
Per una rotta esistente chiamata con metodo errato è più corretto restituire 405 e l'header Allow. Mantieni una versione nel percorso, come /api/v1, per poter evolvere il contratto senza rompere tutti i client.
8. Token casuali, non prevedibili
Per un'integrazione server-to-server semplice puoi usare token opachi generati con un generatore crittograficamente sicuro:
$plainToken = bin2hex(random_bytes(32));
$tokenHash = hash('sha256', $plainToken);
Mostra il token in chiaro una sola volta e salva nel database soltanto l'hash, insieme a utente, scope, scadenza e stato di revoca. Il client invia:
Authorization: Bearer TOKEN
Lato server estrai l'header, controlla formato e hash con un confronto sicuro. Per token ad alta entropia, l'hash SHA-256 è adatto alla ricerca; per password umane usa invece password_hash() e password_verify(), perché le password hanno entropia molto inferiore.
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!preg_match('/^Bearer\s+([A-Za-z0-9._~-]+)$/', $header, $m)) {
header('WWW-Authenticate: Bearer');
jsonResponse(['error' => 'Autenticazione richiesta'], 401);
}
$tokenHash = hash('sha256', $m[1]);
I bearer token concedono accesso a chiunque li possieda: usa sempre HTTPS, non inserirli negli URL, non registrarli nei log e prevedi rotazione e revoca.
9. Autorizzare ogni singolo oggetto
Autenticare un utente non significa autorizzarlo a qualsiasi record. Il rischio più comune nelle API è accettare un ID e restituire l'oggetto senza controllarne proprietario o ruolo.
$stmt = $pdo->prepare(
'SELECT id, title, content FROM posts WHERE id = :id AND user_id = :user_id'
);
$stmt->execute(['id' => $id, 'user_id' => $currentUserId]);
$post = $stmt->fetch();
if (!$post) {
jsonResponse(['error' => 'Risorsa non trovata'], 404);
}
Applicare il vincolo direttamente nella query riduce il rischio di dimenticarlo. Controlla anche le proprietà modificabili: un utente non deve poter assegnarsi campi come role, owner_id o is_admin inviandoli nel JSON.
10. CORS non è autenticazione
CORS controlla quali pagine web possono leggere una risposta nel browser; non impedisce richieste da script, server o strumenti da riga di comando. Se l'API è usata da un frontend separato, autorizza origini esplicite:
$allowed = ['https://app.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed, true)) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Vary: Origin');
}
Evita di riflettere qualunque origine ricevuta. Gestisci le richieste OPTIONS e autorizza soltanto metodi e header necessari.
11. Limitare abuso e consumo di risorse
Applica limiti per token, indirizzo IP o account, preferibilmente su un archivio condiviso come Redis quando esistono più istanze. Proteggi soprattutto login, invio email, esportazioni, ricerca complessa e upload. Restituisci 429 quando il limite viene superato e, se possibile, Retry-After.
Imponi inoltre paginazione e massimi rigidi:
$limit = min(max((int)($_GET['limit'] ?? 20), 1), 100);
$offset = max((int)($_GET['offset'] ?? 0), 0);
Non concatenare questi valori nella query senza controllo. Dopo averli limitati, effettua il binding come interi secondo il driver usato.
12. Errori e log senza fughe di informazioni
Il client deve ricevere un messaggio stabile e un identificatore di richiesta, non stack trace o SQL:
$requestId = bin2hex(random_bytes(8));
header('X-Request-ID: ' . $requestId);
try {
// routing
} catch (Throwable $e) {
error_log(json_encode([
'request_id' => $requestId,
'message' => $e->getMessage(),
]));
jsonResponse(['error' => 'Errore interno', 'request_id' => $requestId], 500);
}
I log devono essere accessibili soltanto agli amministratori, ruotati e privi di password, token, cookie e dati personali non necessari. In produzione imposta display_errors=Off e log_errors=On.
13. HTTPS, aggiornamenti e configurazione PHP
Pubblica l'API esclusivamente tramite HTTPS e reindirizza HTTP. Mantieni PHP, web server e sistema operativo aggiornati. Disabilita funzioni o estensioni inutilizzate soltanto dopo aver verificato l'impatto; la misura principale resta ridurre i servizi esposti e usare account con privilegi minimi.
Configura limiti coerenti per post_max_size, upload_max_filesize, max_execution_time e memoria. Una API non deve permettere a una singola richiesta di consumare risorse illimitate.
14. Test essenziali
Verifica almeno: JSON malformato, Content-Type errato, token assente o revocato, accesso all'oggetto di un altro utente, campi inattesi, ID fuori intervallo, query molto grandi, metodi non permessi e database temporaneamente indisponibile. Un test rapido con curl:
curl -i https://api.example.com/api/v1/posts \
-H 'Authorization: Bearer TOKEN'
curl -i -X POST https://api.example.com/api/v1/posts \
-H 'Authorization: Bearer TOKEN' \
-H 'Content-Type: application/json' \
--data '{"title":"Prova","content":"Contenuto"}'
Automatizza i test e includi casi negativi: la sicurezza emerge soprattutto da ciò che l'API rifiuta correttamente.
Checklist finale
- solo
publicè esposto dal web server; - segreti e configurazione non sono nel repository né nella document root;
- input JSON, tipi, lunghezze e campi sono validati;
- tutte le query usano prepared statement e liste positive per gli identificatori;
- token casuali viaggiano solo tramite HTTPS e possono essere revocati;
- ogni oggetto e proprietà dispone di controlli di autorizzazione;
- paginazione, dimensioni e frequenza delle richieste sono limitate;
- errori pubblici e log interni sono separati;
- CORS autorizza soltanto origini necessarie;
- dipendenze, PHP e sistema vengono aggiornati e testati.
Conclusione
Creare una API REST senza framework è possibile e istruttivo, purché la semplicità non venga confusa con l'assenza di regole. Routing esplicito, JSON controllato, PDO, token opachi, autorizzazione per oggetto e limiti di risorsa costituiscono una base concreta. Quando il progetto cresce, librerie consolidate possono ridurre il codice da mantenere, ma i principi rimangono gli stessi: non fidarsi dell'input, concedere il minimo privilegio e rendere ogni decisione di sicurezza verificabile.
Share
What's Your Reaction?
Like
0
Cancella like
0
Amore
0
Buffo
0
arrabbiato
0
Sad
0
Wow
0