Affichage des articles dont le libellé est php. Afficher tous les articles
Affichage des articles dont le libellé est php. Afficher tous les articles

dimanche, 25 novembre 2007

Fonction url_get_contents en PHP

Plutôt que d'utiliser un bien moche file_get_contents($url)$url est une url, je vous propose une petite fonction url_get_contents($url) utilisant l'extension curl de PHP qui permet aussi bien de télécharger le contenu d'une url via GET que POST :

function url_get_contents($url, $post = null) {
$curl = curl_init();
curl_setopt ($curl, CURLOPT_URL, $url);
curl_setopt ($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
curl_setopt ($curl, CURLOPT_HEADER, 0);
curl_setopt ($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt ($curl, CURLOPT_SSL_VERIFYPEER, 0);
//curl_setopt ($curl, CURLOPT_FOLLOWLOCATION, 1); // if no safe_mode neither open_basedir

if (is_array($post)) {
curl_setopt ($curl, CURLOPT_POST, 1);
curl_setopt ($curl, CURLOPT_POSTFIELDS, $post);
}

$html = curl_exec ($curl);
curl_close ($curl);
return $html;
}

L'utilisation est réellement simple :

$content = url_get_contents('http://www.noisette.ch');

jeudi, 16 août 2007

Uniqid ou la contre-intuitivité des arguments

D'après le manuel PHP, le prototype de la fonction uniqid est le suivant :

string uniqid ( [string prefix [, bool more_entropy]] )
Le deuxième paramètre à l'air réellement intéressant, dans la mesure où il assure une meilleure unicité de la chaine produite. Une propriété qui de toute évidence nous intéresse si on utilise cette fonction uniqid.

Sans s'étendre sur la probabilité de collision entre les 2 appels (ce que j'étendrai plus longuement dans un autre article), je voulais juste montrer la différence de vitesse d'exécution qu'induisait ce paramètre more_entropy. Un petit script faisant 100 appels à uniqid avec une valeur possible du paramètre, nous donne le résultat suivant (!) :

100 * uniquid(mt_rand()) en 0.799283981323 seconde

100 * uniquid(mt_rand(), true) en 0.00346183776855 seconde

Chose extrêmement intéressante, le temps d'exécution de la fonction est 3 ordres de grandeurs en dessous si on demande une meilleures unicité du résultat.

La question obligée est donc : pourquoi est-ce que cette fonctionne ne prend pas par défaut cette option, puisqu'elle a tous les avantages (meilleures résultats, plus rapide) ?

jeudi, 1 février 2007

PEAR::Mail_Queue2

C'est annoncé officiellement par le mainteneur actuel du projet, je prête mes 10 doigts pour le développement du paquet PEAR::Mail_Queue2, qui est une réécriture de PEAR::Mail_Queue. En effet ce dernier souffre, comme on peut le lire sur l'interface reportant les bugs, de problèmes internes assez ennuyeux et qui ne peuvent pas être corrigés sans casser la compatibilité arrière (backward compatibility, ou BC pour les intimes).

Comme le paquet PEAR::Mail_Queue est noté comme stable, la BC doit être gardée, d'où le paquet Mail_Queue2.

Au menu :

  • Destinataires multiples,
  • Processing concurrent,
  • Management des erreurs,
  • Meilleure gestion du buffer de queue,
  • Multithreading (!, mais uniquement pour Unix)
  • SMTP persistante,
  • ... (et tout ce qui me passera par la tête et sur la mailinglist) ...

vendredi, 19 janvier 2007

De la sécurité des sessions PHP

Dans la majorité des espaces requierant une authentification sur un site web, le soin du suivi de l'utilisateur est laissé aux sessions, ces petits cookies qui viennent se placer chez le client afin de permettre à l'application web de le reconnaitre lors du passage à la page suivante.

Ce modèle de sécurité a du être imaginé à cause de la nature "connexionless" du protocole HTTP, c'est-à-dire la fermeture de la connexion TCP au serveur entre 2 chargements de page consécutifs (contrairement aux modèles de connexions "continues").

Malheureusement différentes techniques pour voler ces informations de sessions existent, elles se nomment credential token stealing, et sont souvent réalisables grâce à des failles de type XSS.

Cet article va expliquer quel est le point faible des sessions, ainsi que présenter une solution pour en améliorer la sécurité. Un exemple d'implémentation sera une fois de plus donné en PHP.

Problèmes des sessions

Le problème lié aux sessions est que l'identité de l'utilisateur, une fois identifiée, repose entièrement sur ce cookie. Si une personne malveillante parvient à obtenir la valeur du cookie, elle pourra alors se faire passer pour la personne légitime aux yeux de l'application.

Meilleure emprunte (fingerprint)

Une première amélioration est d'associer la valeur du cookie à d'autres éléments qu'une personne malveillante ne peut pas modifier : l'ip de connexion, la signature du navigateur, etc...
Tous ces éléments combinés ensemble donnent ce qu'on appelle l'emprunte de la session, et tous ces éléments sont nécessaires pour pouvoir usurper une identité. La principale difficulté est l'adresse ip de connexion, mais si la personne malveillante est sur le même sous-réseau que la personne légitime, l'adresse ip n'est plus un problème. Autre désavantage d'un fingerprinting étendu, si la personne légitime est sur une connexion à adresse ip dynamique, elle devra se réauthentifier à chaque changement d'adresse ip.

ID de session temporaire

Une autre amélioration est la regénération dynamique de la valeur du cookie. A chaque nouvelle connexion, on test si l'utilisateur est légitime, et on génère une nouvelle valeur qu'on lui envoie.
Si une personne malveillante arrive à voler un cookie, sa valeur ne sera que temporaire et dès le prochain chargement de page, la valeur volée devient obsolète.
Mais cela est aussi vrai à l'inverse, si la personne malveillante arrive à usurper l'identité avant que la personne légitime ne redemande une page, c'est la personne légitime qui sera déconnectée du site.

Implémentation en PHP

Une combinaison des 2 méthodes améliorera la sécurité des sessions, sans pour autant la rendre infaillible.

Voici une petite implémentation en PHP :

<?php
/*
* Inspirated from SecureSession class
* initially written by Vagharshak Tozalakyan <vagh@armdex.com>
*/
class SecureSession {

private $_check_browser;
private $_check_ip_blocks = 0;
private $_padding = '*ftt56+g zwc%&gh7/3-lf%254*6c_qm';
private $_regenerate_id = true;
private $_session_var_name = __CLASS__;

public function _construct($check_browser = true,
$check_ip_block = 0, $regenerate_id = true)
{
$this->_check_browser = $check_browser;
$this->_check_ip_block = $check_ip_block;
$this->_regenerate_id = $regenerate_id;

$_SESSION[$this->_session_var_name] = $this->_fingerprint();
$this->_regenerateId();
}

public function isValid()
{
$this->_regenerateId();
return (isset($_SESSION[$this->_session_var_name])
&& $_SESSION[$this->_session_var_name] == $this->_fingerprint());
}

private function _fingerprint()
{
$fingerprint = "";
if ($this->_check_browser) {
$fingerprint .= $_SERVER['HTTP_USER_AGENT'];
}
if ($this->_check_ip_blocks) {
$num_blocks = min(abs(intval($this->check_ip_blocks)), 4);
$blocks = explode('.', $_SERVER['REMOTE_ADDR']);
for ($i=0; $i<$num_blocks; $i++) {
$fingerprint .= $blocks[$i] . '.';
}
}
return sha1($fingerprint . $this->_padding);
}

private function _regenerateId()
{
if ($this->_regenerate_id && function_exists('session_regenerate_id')) {
session_regenerate_id(true);
}
}
}

?>

dimanche, 14 janvier 2007

Form flooding

Les formulaires sont un des points vulnérables d'une application web, car c'est notamment à travers eux que les "clients" peuvent injecter des données dans l'application.

De plus, même si le formulaire est suffisamment protégé contre l'injection de données, qu'elle soit XSS, SQL, string format ou autre, le formulaire reste vulnérable à un flood :

Comment réagit votre formulaire si un client bien authentifié décide de poster 1'000'000 de fois le formulaire ?


Ce genre d'attaque peut avoir des effets très néfastes...

Il y a plusieurs méthodes pour s'en prémunir, et je vais en présenter une qui, contrairement au captcha, ne requiert pas d'intervention de la part de l'utilisateur (on ne peut pas faire de captcha dans un webservice...), mais protège tout de même notre formulaire.

On va donc attribuer à chaque formulaire un identifiant unique, qui est entré dans une table (ou en var de session) conjointement avec un timestamp. L'identifiant nous permettra d'éviter (au pire de remarquer) la soumission multiple du même formulaire, et grâce au timestamp, nous pourrons mesurer le temps entre le chargement de la page contenant le formulaire et son renvoi au serveur, temps qui ne devrait pas être inférieur à une voir plusieurs secondes pour un utilisateur humain, selon la taille du formulaire.

En recevant le formulaire, le serveur peut donc contrôler si :

1. Le formulaire est valide, i.e. l'identifiant du formulaire est valide
2. Le formulaire n'a pas été envoyé plusieurs fois.
3. Le délai de soumission du formulaire n'est pas trop élevé pour une session donnée.

L'implémentation de cette solution est elle aussi multiple, mais j'en donne un exemple ci-dessous :


__toString(); ?> ...
*
* if (isset($_POST)) {
* $canary = Form_Protector::factory($_POST);
* if (!$canary->is_valid()) {
* if ($canary->exists()) {
* // le canary n'existait pas dans la db, afficher un message d'erreur et recharger la page
* } else {
* // le formulaire a été posté trop rapidement, on peut donc compter le nombre de soumissions durant les 5 dernières minutes, et prendre une des actions suivante
* // --> soit on blacklist l'ip un moment,
* // --> soit on sleep(30) pour temporiser (tarpitting)
* }
* } else {
* // le canary est valide, tout est bien dans le meilleur des mondes.
* }
* }
*/

define("_TIME_TO_SUBMIT_FORM", 2); // temps que l'utilisateur fait pour submiter un formulaire
class Form_Protector {

protected $_canary;
protected $_ip;
protected $_date_request;
protected $_date_request;
protected $_exists = false;

public static $input_name = 'form_protector_canary';

protected function __construct($canary = 0, $ip = "") {
$this->_ip = $_SERVER['REMOTE_ADDR'];
if ($canary === 0) {
$this->_canary = rand();
$this->_insert();
} else {
$this->_canary = $canary;
if ($ip !== "") $this->_ip = $ip;
$this->_load();
}
}

protected function _load() {
$query = sprintf("SELECT * FROM form_protector
WHERE canary = %d AND ip = '%s'" . int_val($this->_canary),
mysql_real_secape($this->_ip));
// query,
// load $this->_date_request, $this->_date_request = time();
// si pas un champ est retourné, on passe $this->_exists à true; et on UPDATE ... SET date_response = $this->_date_request
}

protected function _insert() {
$this->_date_request = time();
$query = sprintf("INSERT INTO form_protector (canary, ip, date_request) VALUES (%d, %s, %d)", int_val($this->_canary), mysql_real_secape($this->_ip), $this->_date_request);
// query
// si pas d'erreur : $this->_exists = true;
}

public function is_valid() {
if ($this->_date_request + _TIME_TO_SUBMIT_FORM <>_exists;
}
public function __toString() {
return '<input value="' . $this->_canary . '" name="form_protector_canary" type="hidden">';
}

public static factory($params = NULL) {
if (is_array($params) && isset($params[self::$name])) {
$canary = $params[self::$name];
} else {
if ($params === NULL) {
$canary = 0;
} else {
$canary = $params;
}
}
return new Form_protector($canary);
}
}

?>

mardi, 9 janvier 2007

Optimisation du nombre de requêtes SQL dans les collections d'objets et relation n-m

L'orientation objet de PHP n'est plus contestable, mais les problèmes d'optimisation persistent.

Par exemple le malheureusement célèbre n+1 pattern, qui fait que pour afficher une liste de n objets, le + 1 étant la requête qui sélectionne tous les ids des objets à instancier, n + 1 requêtes SQL seront effectuées.

De même dans des relations n-m, la majorité des implémentations sélectionnent les n objets (en n + 1 requêtes donc), puis pour chacun on sélectionne les m objets de la relation. On obtient donc n * m + 1 requêtes.

Le but de cet article est de présenter deux techniques qui, combinées, réduisent les n * m + 1 requêtes en n + m + 1.

Les deux techniques que je vais illustrer ici se nomment object caching et grouped fetching. Elles se combinent très bien, ce qui permet d'optimiser drastiquement les performances d'un script PHP, du points de vue I/O (moins de requêtes), vitesse d'exécution et même mémoire utilisée (les objets ne sont pas dupliqués).

Object caching :

Le principe de l'object caching est de rendre le constructeur de l'objet privé (au pire protégé) et de l'instancier au moyen d'une factory. Puis on ajoute à la classe un tableau statique dans lequel les références des objets instanciés seront placés. La factory va donc regarder dans le tableau de références si l'objet existe, et si ça n'est pas le cas elle va le créer, l'ajouter au tableau et le retourner.


class A {
protected static $objects_cache = array();

public static function factory($id, $class = __CLASS__)
{
if (array_key_exists($id, $class::$objects_cache)) {
return $class::$objects_cache[$id];
} else {
$o = new $class($id);
$class::$objects_cache[$id] = $o;
return $o;
}
}
}


Cette solution pourrait encore être améliorée si les objets pouvaient être partagés entre toutes les instances de PHP. Ce n'est pas le cas à cause de l'architecture share nothing de PHP, et c'est un des points forts des serveurs d'applications.
Un autre désavantage de ce concept, si on a un script qui tourne suffisamment longtemps pour que le grabage collector se lance, est que tous les objets créés sont toujours référencés, même ceux qui pourraient être des candidats potentiels à la finalisation. Ce problème est résolu dans d'autres langages, en Java par exemple grâce aux références faibles (WeakReference).

Grouped fetching

Le principe du grouped fetching, dérivé d'une solution proposée par notre ami Colder, est de charger les données des objets de manière asynchrone et en bloque. Quand un objet est instancié, il est marqué comme non chargé, et sa référence est placée dans un tableau global de la classe. Puis lors d'un accès à un champ non chargé, la classe va sélectionner dans la base de données tous les objets instanciés mais pas encore chargés. Plus on retarde les accès aux attributs d'un objet, plus on va paralléliser les requêtes SQL.


class A {
private static $_to_load = array();
private $_is_loaded = false;
public function __construct($id)
{
$this->id = $id;
self::$_to_load[$id] = $this;
}

public function __get($attribut)
{
if (!$this->_is_loaded) {
self::_groupedLoad();
}
return $this[$attribut];
}

private function _setLoaded()
{
$this->is_loaded = true;
}

private static function _groupedLoad()
{
$ids = implode(', ', self::$_to_load);
$query = 'SELECT * FROM ' . self::$_table . ' WHERE id IN ( ' . $ids . ' ) ';
$res = db_query($query);
while ($row = $res->getNext()) {
self::_to_load[$row['id']]->_initByArray($row);
self::_to_load[$row['id']]->_setLoaded;
}
}
}


L'overhead de ce concept est très faible (un tableau de références supplémentaire), et le nombre d'accès à la base de données sont grandement réduit. Mais le problème de la finalisation des objets se repose aussi.

samedi, 30 décembre 2006

Surchages de méthodes statiques en PHP

Un tout petit exemple pour exposer le problème :

class A {
  public static function f() {
    echo get_class();
  }
}

class B extends A {}

A::f();
B::f();


A::f() va afficher A, par contre B::f() va aussi afficher A, alors que la réponse attendue serait B.

Ce comportement est connu des développeurs OO, peut s'avérer ennuyeux dès qu'on utilise des fonctions statiques surchargées, car les appels à self::une_fonction() (une_fonction est donc une méthode statique) dans le parent résulteront toujours à un appel de la méthode du parent, et non pas celle surchargée dans le fils. La raison est que self dans la classe parente représente le parent, comme c'est aussi le cas dans d'autres langages OO.
Illustrons les limites de ce comportement avec la mise en place d'une factory toute simple :

class A {
  ...
  public static function create($params) {
    $class = get_class();
    return new $class($params);
  }
}
class B extends A {}
$b = B::create($myparams);


$b contiendra toujours un objet de classe A, car get_class retournera toujours A.

L'idée pour palier à cette limitation est donc simplement d'ajouter un argument à la fonction create de A, qui sera la classe à instancer :

class A {
  ...
  public static function create($params, $class = __CLASS__) {
    return new $class();
  }
}
class B extends A {
  public static function create($params, $class = __CLASS__) {
  return parent::create($params, $class);
  }
}
$b = B::create($myparams);


Avec cette petite adaptation, $b sera bien un objet B.

De manière plus général, si on envisage d'utiliser des fonctions statiques surchargées, PHP nous permet de gérer l'héritage en passant la classe en argument et en appelant la fonction statique au moyen de la macro call_user_func :

class A {
  protected static une_fonction($arg) { return $arg + 1; }
  ...
  public static function create($params, $class = __CLASS__) {
    $res = call_user_func(array($class, 'une_fonction'), $args);
  ...
  }
}
class B extends A {
  protected static une_fonction($arg) { return $arg + 2; }
  public static function create($params, $class = __CLASS__) {
  return parent::create($params, $class);
  }
}
$b = B::create($myparams);


Grâce à cette macro call_user_func, B::create appellera donc bien B::une_fonction.

lundi, 18 décembre 2006

Reverse MD5

La fonction MD5 est une fonction dit à sens unique, c'est-à-dire que ne connaissant que la sortie, il est difficile de trouver l'entrée qui a produit cette sortie.

C'est pourquoi des reverse md5 databases sont apparues sur Internet. Leur principe est tout simple : c'est une grande base de données contenant la pair (texte, hash). Ainsi on peut la questionner du hash recherché et elle nous retourne le texte correspondant si il est connu.

La base de données doit donc être remplie avant de pouvoir produire des réponses, mais une fois qu'elle contient suffisamment de données, les recherches peuvent commencer à être intéressantes. Les techniques des remplissages sont simples, elles vont du parcours de pages web ou de dictionnaires pour rechercher des mots quelconques à l'ajout manuel par des utilisateurs.

J'ai rapidement fait une petite application qui questionne plusieurs des ces reverse md5 databases, qui elle est disponible à l'addresse http://md5.noisette.ch/form.php

Elle se base sur la toute petite API suivante : http://md5.noisette.ch/?hash=<le_hash_en_hex> qui retourne du XML

<md5lookup>
  <hash><!--[CDATA[2a0231531bc1a7fc29e2fa8d64352ae9]]--></hash>
  <string><!--[CDATA[noisette]]--></string>
</md5lookup>


L'approche est très alléchante car une fois le hash dans la base de données, les réponses sont fournies en O(1). Les mots de passe stockés en md5 peuvent donc être retrouvés extrèmenent rapidement.
La contre-partie de cette approche est qu'elle nécessite beaucoup de hash précalculé avant d'être utilisable, et donc produira une énorme base de données :
Le md5 étant sur 128 bits, on pourrait avoir une table de 2^^128 entrées, pour peu qu'on ne prenne pas en compte les doublons. Donc rien que pour les hash, il nous faudrait ~5*10^^39 bytes de stockage = ~5000 téra de téra de téra bytes. Enfin juste pas possible quoi. Si on fait le raisonnement inversion on se dit que notre serveur a 200Gb d'espace disque, on peut donc stocker 12.5 milliards de hash, donc 12.5 milliards de possibilités de mots de passe = un peu plus de 2^^33 possibilités. On est loin des 2^^128...

En résumé, c'est une approchoe time / memory tradoff 100% mémory :)

mercredi, 8 novembre 2006

PHP Multithread

Non, faire tourner PHP en multithread n'est pas un mythe !
C'est possible de lancer des processus fils grâce au module pcntl, et à les synchroniser avec le module sémaphore, de préférence en ligne de commande (la doc précise que leur utilisation en module apache peut amener à des résultats erronés).

Voici donc un petit article qui démontre par un exemple comment implémenter un démon multithread en PHP.

mardi, 26 septembre 2006

Gentoo avec Apache2 + PHP5 + Suexec + FastCGI (dynamic)

FastCGI est un concept tout à fait intéressant qui permet, dans le cadre de PHP, d'allier rapidité et sécurité. Il permet conjointement d'avoir la rapidité de mod_php avec la sécurité de PHP/CGI, notemment en terme de droits d'utilisateurs.
Je ne vais pas vous refaire une série de benchmark comme c'est le cas sur beaucoup d'autre site, mais je viens de réaliser un article explicant en détail l'installation, la configuration et surtout les problèmes qu'on peut rencontrer avec la mise en place PHP, FastCGI dynamique et Suexec :

mercredi, 20 septembre 2006

Système de réécriture d'URL (URL Rewriting)

Les systèmes de réécriture d'URL posent rapidement des problèmes de scalabilité pour un site générant un trafic respectable (genre dès 1000 visiteurs uniques par jour). Je l'ai déjà exprimé dans un précédent article : Mambo 404 SEF, et souhaite maintenant apporter un design de solution.

Une technique relativement simple pour palier à ce problème de VARCHAR non- ou mal -indexable est d'introduire une variable aléatoire dans l'URL, variable à partir de laquelle la vrai URL sera chargée : www.domaine.com/14523623423/news/ma_premiere_news.html
L'important dans cet URL est donc la variable 14523623423, alors que ce qui se situe après n'a aucune importance et peut être modifié à souhait (on remarque aussi ici que cette technique permet rapidement de mettre un place de l'URL flooding, utilisants des URLs de toute sorte pour pointer sur la même page. Attention à ne pas en abuser, car on risque de voir son site se faire bannir des moteurs de recherche).

La requête SQL devient donc

SELECT realurl FROM redirections WHERE urlid = 14523623423

Mieux ça non ? Le problème évoqué ici est en fait global à tout design de base de données : les champs utilisés sans intervention d'un utilisateur dans une condition devraient toujours être de type entier ou un dérivé. Car même si les chaines de caractères sont relativement rapide lors des tests, la réaction à haute charge est très souvent mauvaise...

mardi, 25 avril 2006

404 SEF, un mode Mambo génial ... en apparence

Initialement posté sur Noisette.ch par KillerWhile le 29.04.2006

404 SEF est un composant de Mambo/Joomla qui a pour but de permettre de disposer d'adresses amicales (userfriendly) pour afficher les pages. Sur le papier, c'est vraiment super utile car ça permet une bonne indexation auprès des moteurs de recherche. En pratique, c'est un calvaire pour les administrateurs des serveurs sur lesquelles tourne ce composant.

Explication : un page www.domaine.com/news/ma_premiere_news.html va devoir faire une requête dans la base de données pour savoir vers quelle url réelle news/ma_premiere_news.html pointe. Ca signifie une requête du style

SELECT realurl from jos_redirection WHERE fakeurl = 'news/ma_premiere_news.html'

Cette requête, qui recherche donc une valeur dans un champs VARCHAR et donc ne peut pas utiliser d'index, est vraiment sous-optimale et devient un syphon en terme de ressources sql.

Une chose à faire donc : changer de composant de réécriture d'url (je n'en ai malheureusement pas encore de nouveau à proposer) ou s'en passer.