Access control
Authentication e Authorization
L'authentication è il metodo con cui si identifica l'utente, viene gestita attraverso sessione, cookies e authentication provider.
L'Authorization identifica ciò che l'utente può fare ed è gestita attraverso permissions, route access, entity access, block access e field access.
Un utente autenticato non è necessariamente autorizzato ad eseguire una certa operazione.
// controllo troppo generico, insufficiente
if (
$account->isAuthenticated()
) {
return TRUE;
}
// controllare il ruolo non è sufficiente, bisogna verificare le permissions
$account->hasRole(
'administrator',
);
Evitare di controllare solo nel template, l'accesso deve essere verificato server-side prima di produrre il contenuto:
{# Twig decide la presentazione, non l'autorizzazione, questo controllo è insufficiente #}
{% if user_has_permission %}
...
{% endif %}
Principio del minimo privilegio
Ogni utente dovrebbe possedere soltanto i privilegi necessari:
if (
$account->hasPermission(
'administer weather settings',
)
) {
// .........
}
Nel file .permissions.yml vengono descritte le permissions necessarie per il modulo, ma la riga restrict access: true non aggiunge alcun controllo, serve solo a marcare quella permission come rischiosa nella pagina di amministrazione.
Access control sulla route
Nel file .routing.yml alla voce requirements è indicata la permission necessaria per accedere alla route, definita nel file .permissions.yml. Se la route opera su entità allora bisogna aggiungere anche sotto requirements la voce _entity_access.
Accesso alla entity
Le operazioni sulla entity possono essere view, update e delete:
$access = $node->access(
'view',
$account,
TRUE,
);
Field access
Il fatto che un utente possa visualizzare una entity non implica necessariamente che possa visualizzare ogni suo field:
$access = $node
->get('field_alert_message')
->access(
'view',
$account,
TRUE,
);
EntityQuery
Nell'eseguire la query bisogna specificare specificare se debbano essere restituite solo le entità di contenuto a cui l'utente corrente ha accesso:
$ids = $storage
->getQuery()
->accessCheck(TRUE) //solo le entità a cui l'utente ha accesso
->condition(
'type',
'weather_alert',
)
->condition(
'status',
1,
)
->execute();
AccessResult
Quando implementiamo un controllo custom dobbiamo restituire un AccessResult perchè il risultato deve includere anche i dati sulla cacheability. Un errore nella gestione della cache può essere anche una vulnerabilità di sicurezza, non soltanto un problema di performance.
Validation
Input validation
Un dato ricevuto dall'esterno deve essere sempre validato prima di essere utilizzato:
$allowed = [
'summary',
'extended',
];
if (
!in_array(
$display,
$allowed,
TRUE,
)
) {
// Invalid.
}
Se il dato non è valido non bisogna nascondere l'errore:
// esempio sbagliato da evitare
if (
!in_array(
$display,
['summary', 'extended'],
TRUE,
)
) {
$display = 'summary'; // non bisogna correggere il dato sbagliato ma bisogna far gestire la situazione alla configurazione della route
}
Validazione nei form
I controlli client-side non sono sufficienti, bisogna sempre fare anche la validazione lato server:
if (
$latitude === FALSE ||
$latitude < -90 ||
$latitude > 90
) {
$form_state->setErrorByName(
'latitude',
$this->t(
'The latitude must be a number between -90 and 90.',
),
);
}
XSS Cross-Site Scripting
I dati ricevuti dall'esterno devono essere sempre sanitizzati, nei template Twig viene eseguito automaticamente l'escaping delle variabili. Non utilizzare Markup::create($value) perchè in questo caso si assume che $value sia già sicuro.
Anche gli attributi e le classi nell'HTML potrebbero essere pericolosi:
{# esempio corretto #}
data-display="{{ display }}"
{# esempio sbagliato #}
data-display={{ display }}
{# esempio corretto #}
nome-modulo-forecast-block--{{ display|clean_class }}
{# esempio sbagliato #}
class="{{ external_value }}"
Lato JavaScript preferire textContent a innerHTML perchè textContent tratta il valore come testo mentre innerHTML interpreta il contenuto come markup.
Database / HTTP / File / Entity API
SQL Injection
Esempio sbagliato:
$sql =
"SELECT *
FROM {table}
WHERE location = '$location'";
Esempio corretto:
$sql =
'SELECT *
FROM {table}
WHERE location = :location'; // i placeholder si usano solo per i valori, non per cose come i nomi di tabelle e colonne
$result = $this->database->query(
$sql,
[
':location' => $location,
],
);
//con le query dinamiche basta fare
$query->condition(
'location',
$location,
);
Inoltre è sempre meglio usare Entity API invece di SQL diretto:
// esempio corretto:
$node->setTitle(
'New title',
);
$node->save();
// esempio sbagliato
$database
->update(
'node_field_data',
)
->fields(...)
->execute();
CSRF Cross-Site Request Forgery
Questa vulnerabilità si verifica quando l'utente autenticato sul sito Drupal visita un sito malevolo che preleva il cookie di sessione e lo usa per inviare una richiesta al sito Drupal a nome dell'utente loggato.
Per i form compilati dagli utenti autenticati questo è già gestito dalla Form API, per i form anonimi devono invece essere implementati dei controlli custom.
Per i form che servono e eliminare un dato è meglio usare ConfirmFormBase anzichè eseguire semplicemente l'operazione.
SSRF Server-Side Request Forgery
Questa vulnerabilità si verifica quando un attaccante riesce a far effettuare al server richieste verso destinazioni che non dovrebbe poter raggiungere. Non permettere all'utente di scegliere arbitrariamente il server verso il quale Drupal effettua la richiesta.
HTTP
Il fatto che una richiesta HTTP abbia restituito 200 non garantisce che il payload abbia la struttura prevista, bisogna sempre verificare che il datoci sia, sia del tipo corretto e il valore sia nel range consentito.
Impostare un timeout per evitare che un servizio esterno non disponibile consumi indefinitamente le risorse del server.
$response =
$this->httpClient->request(
'GET',
$url,
[
'timeout' => 10,
],
);
Segreti e credenziali
API key, password e token non devono essere trattati come normale configurazione esportabile, usare environment variable, file esterni richiamati da settings.php o altri sistemi dedicati alla gestione delle chiavi:
$config['nome_modulo.settings']['api_key'] =
getenv('NOME_MODULO_API_KEY');
Evitare anche di mostrare segreti nei log:
// esempio sbagliato
$this->logger->error(
'Request failed: @url',
[
'@url' =>
'https://api.example.com'
. '?api_key='
. $apiKey,
],
);
Anche nei messaggi mostrati all'utente evitare di mostrare stack trace, percorsi filesystem, credenziali, dettagli delle query, configurazioni interne, response integrali contenenti dati sesnsibili:
// esempio sbagliato
catch (Throwable $exception) {
return [
'#markup' =>
$exception->getMessage(),
];
}
File upload
Quando l'utente carica un file bisogna verificare almeno:
- estensioni ammesse (non fidarsi neanche del nome del file)
- dimensione del file
- tipo di contenuto
- dove verrà salvato il file (public:// oppure private:// oppure temporary://)
- accesso al file
- impossibilità di eseguire il file come codice
Il web server dovrebbe poter scrivere soltanto dove è necessario, tipicamente:
- core/
- modules/
- themes/
- vendor/
- index.php
Redirect
Esempio sbagliato da evitare:
$url =
$request->query->get(
'destination',
);
return new RedirectResponse(
$url,
);
Esempio corretto:
$form_state->setRedirect(
'nome_modulo.forecast',
);
//oppure
Url::fromRoute(
'nome_modulo.forecast',
);
Se dobbiamo costruire un URL non concateniamo manualmente ma usiamo le funzionalità predefinite:
// esempio sbagliato da evitare
$markup =
'<a href="' . $url . '">'
. $label
. '</a>';
//esempio corretto
[ '#type' => 'link',
'#title' => $label,
'#url' => $urlObject,
]
Security updates
Possono sorgere delle vulnerabilità a causa di Drupal core obsoleto, modulo contrib o libreria Composer vulnerabile, è consigliato eseguire composer audit per verificare security advisory nelle dipendenze PHP. Eseguire composer install in produzione e composer update in sviluppo.