Com escriure codi segur amb PHP

Ja sigui per escriure un petit bloc de codi PHP o una aplicació sencera és important escriure codi de forma segura. A continuació detallarem algunes nocions bàsiques i elementals per escriure codi de forma minimament segura, el fet d'aplicar aquestes pràctiques no garanteix la seguretat del nostre codi però ajuda, i el fet de no aplicar algunes d'elles, com el correcte escapament de variables, sí que és garantia que estem deixant un forat de seguretat en la nostre aplicació força fàcil d'explotar.

1. Valida el contingut abans de ser publicat

Cap peça de contingut enviada pels usuaris MAI ha de ser publicada directament en el HTML, sempre ha de passar abans per alguna funció de sanejament.

Per exemple, una manera de sanititzar el text pla es passar-lo a través d'una funció del tipus:

/**
* Codificar els caràcters especials d'una cadena de text sense format per a la
* visualització en HTML.
*
* També valida cadenes UTF-8 per evitar els atacs XSS a Internet Explorer 6.
**/
function filter_plain($text) {
  return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}

Per sanititzar HTML en lloc de text pla resulta més complicat, degut a la multitud de vies d'atac, així que utilitzem una llibreria anomenada HTML Purifier. No només sanititza el HTML sinó que et permet fer moltes altres coses com arreglar la sortida de codi mal formada, forçar que el codi de sortida sigui XHTML vàlid, eliminar els tags d'estructura sense contingut, etc.

Podeu trobar més informació sobre aquesta llibreria a: http://htmlpurifier.org/

2. Escapa les variables de qualsevol consulta a la base de dades

Per evitar atacs d'injecció de SQL abans cal escapar totes les variables correctament que s'utilitzaran en qualsevol sentència SQL. Per exemple, mai concatenis les dades directament en les consultes SQL d'aquesta manera:

$nom = $_REQUEST['user'];
$sql = "SELECT * FROM taula t WHERE t.name = ''" . $nom . "'";
$stmt = $dbh->prepare($sql);
$stmt->execute();
$usuari = $stmt->fetch();

Aquest codi està provocant un forat de seguretat enorme, doncs el paràmetre user no està sent tractat ni escapat de cap manera, cosa que permet a un atacant maliciós fer qualsevol cosa que li permetin els permisos de l'usuari de connexió sobre la base de dades (veure totes les taules, esborrar dades, etc...) si sap enviar una variable 'user' de la forma correcta.

Per exemple, si algú ens envia una variable user de la següent forma podrà recuperar tots els usuaris: ' OR 1 = 1 '. Doncs la consulta resultant que executarem serà:

SELECT * FROM taula t WHERE t.name = '' OR 1 = 1

O fins hi tot podria esborrar tots els usuaris de la base de dades, enviant un user d'aquesta forma: '; DELETE FROM taula;

Doncs executaríem a la base de dades el següent:

SELECT * FROM taula t WHERE t.name = ''; DELETE FROM taula;

I això es vàlid tan si enviem la variable per POST, per GET o per COOKIE, però és especialment senzill per GET perquè podríem fer accions sobre la base de dades utilitzant aquesta variable no escapada simplement construint URLs de la forma correcta, tipus: http://la_meva_url.cat?user='%20%3BDELETE%20FROM%20TAULA. Utilitzat els caràcters en codi hexadecimal per passar espais en blanc i el punt i coma.

En el cas d'utilitzar PDO els mètodes prepare() i execute() permeten escapar les variables correctament de dues maneres:

1) Utilitzant marques amb noms que fan el codi de la consulta SQL més llegible:

$nom = isset($_REQUEST['user']) ? $_REQUEST['user'] : FALSE;
if ($nom) {
  $sql  = 'SELECT * FROM taula t WHERE t.name = :nom';
  $stmt = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
  $stmt->execute(array(':nom' => $nom));
  $usuari = $stmt->fetch();
}

2) Passant tots els paràmetres en una matriu amb el mateix ordre en que apareixen a la sentència, fent el codi més compacte:

$nom = isset($_REQUEST['user']) ? $_REQUEST['user'] : FALSE;
if ($nom) {
  $sql  = 'SELECT * FROM taula t WHERE t.name = ?';
  $stmt = $dbh->prepare($sql);
  $stmt->execute(array($nom));
  $usuari = $stmt->fetch();
}

Fixeu-vos que en el codi estem escapant la cadena de text del nom d'usuari ($nom) per prevenir la injecció de codi SQL, i a més, no executem la sentència si no ens arriba aquesta variable. També podem veure que recuperem les dades utilitzant el mètode ->fetch() enlloc de ->fechAll(), el primer ens tornarà sempre una fila, el segon totes les files de la consulta. Com que en aquest cas el nom d'usuari és únic no cal invocar un fechAll().

3. Casting i filtrat discret de valors

Un altre forma adicional de securitzar el codi es forçar un casting cap al tipus de variable que estem utilitzant. Per exemple, si enviem per POST una variable amb el codi d'usuari (uid) i sabem que aquest codi d'usuari és un nombre enter, es molt recomanable forçar que així sigui. Així, enlloc d'escriure:

$uid = $_POST['uid'];

Forçarem que recuperem la variable com a entern, convertint qualsevol altre cosa que ens puguin enviar com a paràmetre al número 0, podent filtrar posteriorment qualsevol tipus d'error:

$uid = (int) $_POST['uid'];

Això ens permet fer una validació molt senzill posterior de la recepció correcta del paràmetre.

if ($uid > 0) {
  // Codi d'usuari correcte, executem el nostre codi.
}
else {
  echo "Error: Codi d'usuari incorrecte.";
}

Una altre cas que es pot donar és la recepció d'un paràmetre que només pot prendre un nombre de valors concret. Per exemple, si tenim un paràmetre d'idioma podem definir la llista d'idiomes permesos, i un idioma per defecte que serà el valor que prengui la variable si no rebem cap valor o rebem alguna cosa que no està a la llista de valors vàlids.

$idiomes_actius = array('ca', 'es', 'en');
$idioma = (isset($_REQUEST['lang']) && in_array($_REQUEST['lang'], $idiomes_actius)) ? $_REQUEST['lang'] : 'ca';

D'aquesta manera ens assegurem que la variable $idioma sempre agafarà un dels tres possibles valors vàlids, i que qualsevol valor incorrecte es transformarà en l'idioma per defecte (en aquest cas 'ca'). Evitant diferents tipus d'atacs a través d'aquesta variable.

4. Encriptació dels passwords

La política general de la casa sempre ha estat utilitzar usuaris personals del Oracle corporatiu per a realitzar la validació, per tant, normalment no ha calgut desar els passwords d'usuaris en cap taula adicional, doncs cada usuari corporatiu tenia el seu propi usuari/password ja creat i correctament encriptat. Tot i així, aquesta visió general no sempre és aplicable, i a vegades cal tenir usuari/password genèric d'aplicació, a una gestió d'usuaris a nivell aplicació molt més dinàmica (usuaris externs sense DNI, softwares de gestió de continguts, aplicacions amb registres online, etc...).

En aquests casos, mai deseu un password a la base de dades sense encriptar ni feu una validació utilitzant el valor no encriptat d'un password. Penseu que segons la configuració del servidor web hi ha tècniques per exposar els passwords no encriptats intentant provocar errors de PHP.

Per tant, qualsevol variable de password ha de ser directament encriptada després de ser recollida, i sempre hem de desar a la base de dades la versió encriptada.

Un bon algoritme d'encriptació asimètric és el sha1, tot i que és molt habtiual utilitzar el md5 molt més ràpid per encriptar però un pel més insegur:

// Encriptació amb sha1
$password = isset($_POST['password']) ? sha1(trim($_POST['password'])) : FALSE;
// Encriptació amb md5
$password = isset($_POST['password']) ? md5(trim($_POST['password'])) : FALSE;

Això seria una primera aproximació, tot i que podem millorar molt la cadena de hashing d'una forma molt senzilla: introduint una llavor variable segons l'usuari (username, data d'alta, etc.) i fent una re-encriptació d'alguna part. Generant un clau molt més segura i que serà diferent fins hi tot per a usuaris que tenen el mateix password. Per exemple:

$password_hash = md5($usuari . sha1($password));

5. Javascript

Algunes recomanacions generals:

  • Desconfia del javascript per a la validació, els usuaris poden desactivar-lo.
  • No donis per fet que les dades enviades a una funció AJAX de devolució de dades són enviades per la teva funció de JavaScript. Qualsevol pot invocar-les d'es d'un servidor extern, hi ha tècniques com el JSONP o similars per fer-ho.
  • No donis per fet que les dades enviades a una funció JavaScript o per una funció de JavaScript no poden ser observades per l'usuari.
  • Tingueu en compte que algunes funcions del DOM descodifiquen les entitats HTML. Per tant, no mostris el contingut HTML passat a través de javascript en una pàgina sense escapar-lo.
1
Categories:
Plataforma PHP
1
Grups de treball:
Plataforma PHP