27
Jul/09
0

Sécurisation des accès aux URL et REST

Le premier développement Erlang sera de pouvoir sécuriser un petit peu l’accès à l’application, et préparer le futur système de routage.

La sécurisation d’une application passe par la possibilité de restreindre les vecteurs d’attaque potentiels et de ne permettre l’utilisation de l’API que de la façon dont on l’a imaginée.

Ainsi, notre requête /test ne devrait être accessible que par méthode GET, et uniquement dans le cadre d’un appel Ajax.

De plus, on essaiera d’avoir un fonctionnement proche d’une architecture RESTful.

Objectifs

Actuellement, la requête est d’abord orientée par rapport à sa méthode d’accès, puis on teste le chemin utilisé. Dans la boucle loop/2 on observe ce schéma :

si req[method] == get et si req[path] == ‘test’ alors action

Le but est de proposer une interface type REST. Il s’agit d’un objectif, mais nous devons rester pragmatique et des écarts pourront être observés avec la théorie : il est un peu improductif de se poser les questions de savoir si telle ou telle mise en œuvre est 100% REST ou non, il faut surtout qu’elle fonctionne.

Le plus important dans ce type d’approche est de réfléchir principalement en termes de ressources, ce qui détermine fortement les URL produites… et donc l’API de l’application.

Dans le cas d’une application RESTful, les mêmes ressources (mêmes uri) peuvent avoir des conséquences différentes. Par exemple :

  • /article/id (méthode GET)
  • /article/id (méthode POST)

sont deux actions différentes avec la même URL. La différence se fait ici par rapport à la méthode d’accès. La première action pourra afficher un article alors que la seconde sera une opération de mise à jour.

Autre exemple :

  • /article/id (méthode GET, Accept text/html)
  • /article/id (méthode GET, Accept application/json)

Ces URL fourniront deux réponses différentes. La première affichera une page complète de l’article (avec en-tête, menus, etc) alors que la seconde ne renverra que la partie donnée de l’article sous format JSON.

Le type du format de retour sera déterminé, comme ce qui est fait en Ruby On Rails, par le format attendu par le navigateur. Ce format est stocké dans le header dans la variable Accept. Mochiweb permet de récupérer n’importe quelle valeur d’en-tête via la méthode req:get_header/1.

Donc, le fonctionnement actuel n’est pas adapté à notre objectif.

On préférera faire le switch principal par rapport a l’URL puis prendre une action en fonction des autres paramètres.

Mise en oeuvre

Vient ensuite le problème de la façon dont on va traiter la mise en œuvre de ces aspects dans le programme. On pourrait imaginer que le meilleur moyen est de re-définir un mini-framework (rails routing ? hehe) permettant de définir simplement et de façon uniquement déclarative ces routages.

Ceci est quasi-obligatoire dans un langage compilé (hum, java ?), ou bien même dans un langage interpreté car alors la mise a jour des serveurs est un vrai problème d’exploitation.

Dans notre cas, la mise à jour de modules à chaud est une des raisons d’être d’Erlang… On peut donc raisonnablement penser que dans un premier temps, le fait d’incorporer cette configuration dans le code est une alternative tout à fait réaliste.

Versionnement + remplacement à chaud + intégration continue = coktail explosif !

Donc, on ira au plus simple, et le plus simple en l’occurrence est d’utiliser les techniques de pattern matching propres a erlang.

Boucle

On réécrira d’abord la boucle afin de déterminer ce que l’on veut :

loop(Req, DocRoot) ->
    "/" ++ Path = Req:get(path),
    {ok, Params, _} = decode(Req),
    case Path of
      "test" ->
        case Params of
          {'GET', json} ->
            Req:ok({"application/json", [], ["{title:'Test',content:'Data loaded from server'}"]})
        end;
      _ ->
        case Params of
          {'GET', _} ->
            Req:serve_file(Path, DocRoot);
          _ ->
            Req:respond({501, [], []})
        end
    end.

La ligne la plus importante est la deuxième ligne de la fonction, où on décode les paramètres. Ici, on renvoie la méthode + le type de données acceptées par le navigateur.

Pour toutes les URL non gérées, on essaie de renvoyer le fichier (ça par contre, ce n’est pas très sécurisé !).

Décodage

On devra donc construire la méthode qui permet de décoder la requête pour renvoyer de façon plus accessible et plus rapidement exploitable les codes qui nous intéressent.

On écrit donc ce code dans le même module :

decode(Req) ->
  Method = Req:get(method),
  [Accept|_] = string:tokens(Req:get_header_value('Accept'), ","),
  [_,Short] = string:tokens(Accept, "/"),
  Data = "",
  {ok, {Method, list_to_existing_atom(Short)}, Data}.

Plusieurs points sont intéressants.

  • _ représente un élément obligatoire d’un point de vue syntaxique mais dont on se fiche éperdument pour la suite.
  • On utilise le pattern matching pour récupérer facilement le découpage des string
  • [A|B] et [A,B] sont deux éléments de sélection complètement différent. Pour le premier, on désire récupérer uniquement le premier élément (et B contiendra l’ensemble des autres éléments). Pour la seconde construction, il s’agit d’une liste à deux éléments, et chaque variable représente un des éléments.
  • Il n’existe pas de type String en Erlang ! Une string n’est qu’une liste de caractères (d’où quelques problèmes pour gérer de l’utf-8, mais passons).

Lors d’un appel Ajax, on aura la valeur suivante :

Accept: application/json, ...

Donc ici, on transforme "application/json" en une liste ["application", "json"] dont chaque entrée est elle même une liste (de caractères). C’est pourquoi la chaine "json" est transformée en final via la méthode list_to_atom/1. Après quoi, au lieu d’utiliser "json", on peut utiliser directement json.

Cependant, il faut faire attention lors de l’utilisation de cette fonction list_to_atom/1. Notamment, il y a un nombre limité d’atomes disponibles dans la vm : les atomes ne sont pas gérés par le garbage collector !! Donc ils ne sont jamais supprimés de la mémoire.

Il est absolument interdit d’utiliser cette fonction sur des données utilisateur saisies. Il ne faut l’utiliser que lorsqu’on est sûr de posseder un ensemble fini d’éléments.

Ici, on peut donc utiliser list_to_existing_atom/1 qui générera une erreur si jamais une valeur de l’en-tête Accept n’existe pas déjà dans le système, ce qui est beaucoup plus sûr (permet de lutter contre des attaques DOS).

Par exemple, vous pouvez saisir ceci dans un shell :

19> list_to_existing_atom("pipo").
** exception error: bad argument
     in function  list_to_existing_atom/1
        called as list_to_existing_atom("pipo")
20> pipo.  
pipo
21> list_to_existing_atom("pipo").
pipo

Le simple fait d’utiliser / déclarer un atome permet de le réutiliser. C’est pourquoi on crée une fonction qui référence les atomes a utiliser known_accept/0 et qu’on appelle cette méthode dans le start/1 :

start(Options) ->
  init_accept(),
  ...
 
init_accept() ->
  {html, css, json, javascript}.

Test

Maintenant, lorsque l’on tape directement l’URL http://localhost:8000/test, on obtient une erreur à la fois dans le navigateur (réponse vide) et dans la machine virtuelle :

=CRASH REPORT==== 27-Jul-2009::13:32:07 ===
  crasher:
    initial call: mochiweb_socket_server:acceptor_loop/1
    pid: <0.212.0>
    registered_name: []
    exception error: no case clause matching {'GET',html}
      in function  ercm_web:loop/2
      in call from mochiweb_http:headers/5
    ancestors: [ercm_web,ercm_sup,<0.53.0>]
    messages: []
    links: [<0.211.0>,#Port<0.1479>]
    dictionary: [{mochiweb_request_path,"/test"}]
    trap_exit: false
    status: running
    heap_size: 1597
    stack_size: 24
    reductions: 1212
  neighbours:
 
=ERROR REPORT==== 27-Jul-2009::13:32:07 ===
{mochiweb_socket_server,235,{child_error,{case_clause,{'GET',html}}}}
Filed under: Erlang
Comments (0) Trackbacks (0)

No comments yet.

Leave a comment

No trackbacks yet.