A much needed refresh of the “necessary evil” library. Monkey patching (thanks Patchwork) The term “monkey patching”, when applied to code, refers to: […] changing code sneakily – and possibly incompatibly with other such patches – at runtime. Source: WikiPedia. The adverbs used in the sentence above, “sneakily” and “possibly incompatibly”, can be daunting. But, exactly as mocking engines are…
Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
✓ Live Streaming✓ Interactive Chat✓ Private Shows✓ HD Quality
Anya is LIVE right now
FREE
Free to watch • No registration required • HD streaming
Sécuriser les appels de commandes système depuis Ruby 2.x, découverte des `refinements`
Il y a pire que les injections SQL, c'est les injections système. Si votre application doit interagir avec des outils en commande, vous encourez de graves danger si vous passez à vos commandes des paramètres issus de sources pas sûres, comme des données utilisateurs.
Dans cet article, nous verrons comment sécuriser l'exécution des commandes tout en évitant les effets secondaires grâce aux refinements.
Privilégier les API et les couches d'abstraction
À cause de ces risques, il est préférable de communiquer avec les différents services via des API, même au sein d'un même système. C'est possible avec ElasticSearch ou Redis par exemple, qui font tout passer par des endpoint sous localhost.
Mais les outils qui proposent des API restent rares, préférez alors une couche d'abstraction en Ruby qui vous évite de préparer manuellement des commandes paramétrées. Et assurez-vous que cette couche gère de manière optimale la sanitization des paramètres.
Si vous ne trouvez pas de couche d'abstraction, écrivez en une si possible, mais laissez la solution de préparer des commandes en dernier recours.
Appeler une commande système en Ruby, divers moyens nuancés
Il existe plusieurs moyens d'exécuter des commandes système en Ruby. S'ils semblent identiques ou interchangeables, il y a des nuances dans leur mode d'exécution et leurs valeurs de retour.
Le module Kernel, inclus dans Object et dont les méthodes sont par conséquent accessibles à tous les objets Ruby, inclut trois méthodes qui exécutent des commandes système : Kernel#`, Kernel#exec, Kernel#system.
1 - Kernel#` (remarquez le petit backstick après le dièse) exécute la commande dans un subshell (un processus enfant) et renvoie la sortie standard de la commande. Cette méthode est appelée via la syntaxe spéciale `commande` mais rien ne nous empêche de l'appeler plus conventionnellement via send.
2 - Kernel#system exécute la commande dans un subshell (comme `) mais renvoie le statut d'exécution, résumé en soit true soit false (ou nil pour les commandes inexistantes). L'exit-code de la dernière commande exécutée reste accessible dans la variable globale $? avant qu'il ne soit remplacé par le code de la prochaine commande système exécutée. system renvoie true uniquement si l'exit code de la commande est de 0.
system se distingue également de ` par la possibilité de recevoir des variables d'environnement et des paramètres supplémentaires.
3 - Kernel#exec exécute la commande en remplaçant le processus actuel. L'exécution de votre code est donc interrompue pour de bon et il n'est pas possible de tester la valeur de retour ni d'inspecter les sorties standards. exec prend les mêmes paramètres que system.
Il existe d'autres moyens d'exécuter des commandes comme IO#popen et Open3#popen3. Ces derniers permettent un contrôle avancé des flux d'entrée de sortie et d'erreurs, mais nous nous intéresserons plus spécialement à Kernel#` pour la suite de cet article.
Sécuriser les commandes avec Shellwords
Sécuriser une commande est très simple : il suffit d'échapper les paramètres de sources pas sûres à l'aide de Shellwords, un module de la bibliothèque standard de Ruby, qui respecte la norme POSIX / SUSv3.
Notons que require 'shellwords' ouvre la classe String pour y insérer des raccourcis commodes. Nous aurions pu faire :
> `grep rails #{ params[:file].shellescape )}`
Mais encore faut-il se souvenir d'échapper chaque paramètre ajouté de chaque appel de fonction. Un seul oublie et tout est compromis. C'est pourquoi nous allons élaborer une solution de tolérance zéro.
Suspecter toutes les commandes
Ouvrons d'abord la class String pour ajouter le moyen de reconnaitre si une chaine est sûre ou non. Utilisons ce moyen pour marquer les chaines échappées comme étant sûres.
secure_shell.rb
Maintenant, nous devons intervenir sur le module Kernel pour désigner qu'il n'accepte que les chaines échappées ou désignées explicitement comme sûres. Faisons celà par monkey patching classique. Le code peut sembler bizarre avec toutes les apostrophes inversées, mais il est pleinement fonctionnel.
secure_shell.rb
Nous allons à présent apporter quelques modifications de la classe String afin de permettre la création de chaines sûres par la concaténation d'autres chaines sûres.
Pour cela, nous ferons du Monkey Patching d'une approche différente : au lieu de renommer et remplacer les méthodes voulues, nous prependront un module avec de nouvelles versions de nos méthodes :
Mais notre solution n'est pas utilisable en l'état. La syntaxe du backstick `cmd` ne prend pas de variable et nous oblige de passer par des interpolations. Après des heures de recherches et de lecture de code C, j'ai conclu que l'interpolation de chaines en Ruby est effectuée en bas niveau sans appeler des méthodes surchargeables de la classe String ou autre. On est en effet en droit de croire que "grep #{params[:keyword]} Gemfile" n'est qu'un sucre syntaxique pour "grep " + params[:keyword] + " Gemfile", mais il n'en est malheureusement rien !
Nous allons donc être forcés d'utiliser la syntaxe send('`', "grep #{params[:keyword].shellescape} Gemfile".shell_safe!) ou créer une autre méthode dans kernel. C'est ce que nous allons faire en aliasant backstick à ` :
Si nous oublions d'échapper l'une des chaine ou d'en désigner explicitement l'une comme étant saine, une exception est soulevée.
> backstick("grep ".shell_safe! + params[:file].shellescape + " Gemfile")
ArgumentError: Unsafe command, please use String#shellescape or String#shell_safe!
Refinements
Notre exemple illustre parfaitement les dangers du monkey patching. En modifiant le comportement des méthodes, on s'expose au risque de compromettre d'autres codes qui les utilisent.
Si les autres dépendances de votre application (gems) font des appels de commandes système, et c'est toujours le cas, des erreurs surgiront de partout.
C'est la problématique que les refinements introduits dès Ruby 2 sont venus combler. Découverte.
Les refinements permettent de limiter les modifications des classes à des scopes définis (module, class, bloc begin ; end, etc.).
Pour les utiliser, nous devons englober toutes nos ouvertures de classes dans un module. Nous devons également changer d'approche pour la réouverture de modules. refine ne marche en effet qu'avec des classes :
secure_shell.rb
La modification des méthodes de Kernel se fait maintenant par la simple inclusion d'un module. Nous n'avons pas besoin d'utiliser prepend ici car nous savons que Kernel est inclut dans Object. Kernel est donc l'un des ancêtres de Objet. Le dernier module inclut dans une classe est placée en tête de liste des ancêtres. SecureShell::KernelMethods passe donc devant Kernel et peut par conséquent utiliser ses méthodes via `super`.
L'utilisation des refinements se fait à l'aide du mot clé using, qui n'est, sans surprise, qu'une méthode de la class Module. Essayons :
> irb
>> $: << '.'
>> require 'secure_shell'
>> "aString".shell_safe?
NoMethodError: undefined method `shell_safe?' for "aString":String
>> begin
>> using SecureShell
>> puts "aString".shell_safe?
>> end
=> false
>> "aString".shell_safe?
NoMethodError: undefined method `shell_safe?' for "aString":String
Nous utilisons irb pour tester notre solution, la première ligne ajoute le dossier en cours au load_path de Ruby, ce qui nous permet de require le code du refinement. Notez comment l'utilisation de la méthode shell.safe? en dehors du bloc begin / end échoue.
Faisons maintenant des appels plus avancés :
> irb
>> $:
>> require 'secure_shell'
>> require 'securerandom'
>> begin
>> using SecureShell
>>
>> backstick('git init '.shell_safe! + SecureRandom.hex(20).shellescape)
>> end
NoMethodError: undefined method `shell_safe?' for "git init ":String
from /Users/Ihcene/aritylabs/secure_shell/secure_shell.rb:48:in `+'
Erreur ! Nous constatons que "git init ".shell_safe! fonctionne mais pas l'appel à shell_safe? situé dans notre surcharge de la méthode :+ de String via SecureShell::StringConcat. Il est vrai que ce module est inclus dans String mais il ne bénéficie pas, lui, du refinement dont il fait pourtant partie. Nous venons d'être témoin du caractère très strict par rapport au scope des Refinements, qui fait leur force.
La solution est d'isoler les méthodes #shell_safe!, #shell_safe? et #shellescape dans un refinement imbriqué, qui sera utilisé dans le refinement global lui-même et dans les scopes qui nécessitent de sécuriser les appels de commandes système :
secure_shell.rb
L'ensemble peut être utilisé sur une classe de cette manière :
gist.id
> Gist.new.create
=> "Initialized empty Git repository in /Users/Ihcene/aritylabs/secure_shell/ecc8984c07440cecfc0badbd02bea560e9c58b1b/.git/\n"
Conclusion
Nous venons de faire la découverte en pratique de la plus importante nouvelle feature de Ruby 2.x. Les refinements nous ont permis de sécuriser l'exécution de commandes système sans prendre le risque d'affecter les autres dépendances.
At Fiesta we run our own custom mailserver. Since we handle a lot of incoming mail, we have to be confident our updates don't break the mailserver. As can be expected, we have a large amount of test code and run it very often.
dgottlieb@mango-chutney:~$ wc -l ./fiesta/*.py ... 11850 total dgottlieb@mango-chutney:~$ wc -l ./fiesta/test/*.py ... 13566 total
We want to test end-to-end delivery of email, but we don't want our tests leading to a lot of unintentionally sent messages. This is where monkey patching comes in. Because Python has open classes and modules, we can easily redefine methods on the fly. To test our mailserver, we replace some portions of the Python smtplib module during test runs. Let's take a look at how it's done:
mailbox = {} class SMTP(object): def sendmail(self, from, recipients, data): message = email_parser.from_string(data) for recipient in recipients: if recipient not in mailbox: mailbox[recipient] = [message] else: mailbox[recipient].append(message) smtplib.SMTP = SMTP
Here we are actually replacing the entire SMTP object in smtplib with our own custom implementation. Our class defines a sendmail() function that takes the passed in email and adds it to a mailbox dictionary. The dictionary is just a map from email addresses to lists of messages sent to those addresses. In our tests when we expect an email to be sent to someone we simply peek into the mailbox dictionary:
Another example of monkey patching in the Fiesta test code is when we test links that will automatically log a user in without requiring a password. These links are generated by concatenating the User ID, the destination page and a time when the link should expire (and then signing the URL to prevent forgery). What we want to test is the timeout portion of these links, but it would be ideal if we did not have to have the test program sleep for any amount of time. This is how we monkey patch the time() method to go forward in time:
For those that haven't had a chance to play with monkey patching, I hope this helps you come up with some clever, but clear (!) ways to test your code. It's especially useful for testing methods that make library calls that need to exhibit less common/hard to force behavior.