Comment échapper aux appels os.system ()?

124

Lorsque vous utilisez os.system (), il est souvent nécessaire d'échapper aux noms de fichiers et autres arguments passés en paramètres aux commandes. Comment puis-je faire ceci? De préférence quelque chose qui fonctionnerait sur plusieurs systèmes d'exploitation / shells mais en particulier pour bash.

Je fais actuellement ce qui suit, mais je suis sûr qu'il doit y avoir une fonction de bibliothèque pour cela, ou au moins une option plus élégante / robuste / efficace:

def sh_escape(s):
   return s.replace("(","\\(").replace(")","\\)").replace(" ","\\ ")

os.system("cat %s | grep something | sort > %s" 
          % (sh_escape(in_filename), 
             sh_escape(out_filename)))

Edit: j'ai accepté la réponse simple d'utiliser des guillemets, je ne sais pas pourquoi je n'y ai pas pensé; Je suppose que parce que je suis venu de Windows où «et» se comportent un peu différemment.

En ce qui concerne la sécurité, je comprends le problème, mais, dans ce cas, je suis intéressé par une solution rapide et facile fournie par os.system (), et la source des chaînes n'est pas générée par l'utilisateur ou du moins saisie par un utilisateur de confiance (moi).

À M
la source
1
Méfiez-vous du problème de sécurité! Par exemple, si out_filename est foo.txt; rm -rf / L'utilisateur malveillant peut ajouter d'autres commandes directement interprétées par le shell.
Steve Gury
6
Ceci est également utile sans os.system, dans les situations où le sous-processus n'est même pas une option; par exemple, générer des scripts shell.
Une sh_escapefonction idéale échapperait aux ;espaces et et éliminerait le problème de sécurité en créant simplement un fichier appelé quelque chose comme foo.txt\;\ rm\ -rf\ /.
Tom
Dans presque tous les cas, vous devez utiliser un sous-processus, pas os.system. Appeler os.system demande simplement une attaque par injection.
allyourcode

Réponses:

85

Voici ce que j'utilise:

def shellquote(s):
    return "'" + s.replace("'", "'\\''") + "'"

Le shell acceptera toujours un nom de fichier entre guillemets et supprimera les guillemets environnants avant de le transmettre au programme en question. Notamment, cela évite les problèmes avec les noms de fichiers contenant des espaces ou tout autre type de métacaractère de coquille désagréable.

Mettre à jour : si vous utilisez Python 3.3 ou version ultérieure, utilisez shlex.quote au lieu de lancer le vôtre.

Greg Hewgill
la source
7
@pixelbeat: c'est exactement pourquoi il ferme ses guillemets simples, ajoute un guillemet simple littéral échappé, puis rouvre ses guillemets simples.
lhunath
4
Bien que ce ne soit guère la responsabilité de la fonction shellquote, il peut être intéressant de noter que cela échouera toujours si une barre oblique inverse sans guillemets apparaît juste avant la valeur de retour de cette fonction. Moral: assurez-vous de l'utiliser dans un code que vous pouvez faire confiance comme sûr - (comme une partie de commandes codées en dur) - ne l'ajoutez pas à une autre entrée utilisateur non citée.
lhunath
10
Notez qu'à moins que vous n'ayez absolument besoin des fonctionnalités du shell, vous devriez probablement utiliser la suggestion de Jamie à la place.
lhunath
6
Quelque chose de similaire est maintenant officiellement disponible sous shlex.quote .
Janus Troelsen
3
La fonction fournie dans cette réponse fait un meilleur travail de citation shell que shlexou pipes. Ces modules python supposent à tort que les caractères spéciaux sont la seule chose qui doit être entre guillemets, ce qui signifie que les mots-clés du shell (comme time, caseou while) seront analysés lorsque ce comportement n'est pas attendu. Pour cette raison, je recommanderais d'utiliser la routine de guillemets simples dans cette réponse, car elle n'essaie pas d'être "intelligente" et n'a donc pas ces cas extrêmes.
user3035772
157

shlex.quote() fait ce que vous voulez depuis python 3.

(Utilisez pipes.quotepour prendre en charge à la fois python 2 et python 3)

pixelbeat
la source
Il y a aussi commands.mkarg. Il ajoute également un espace de début (en dehors des guillemets) qui peut être souhaitable ou non.Il est intéressant de voir comment leurs implémentations sont assez différentes les unes des autres, et aussi beaucoup plus compliquées que la réponse de Greg Hewgill.
Laurence Gonsalves
3
Pour une raison quelconque, pipes.quoten'est pas mentionné par la documentation standard de la bibliothèque pour le module pipes
Jour
1
Les deux sont sans papiers; command.mkargest obsolète et supprimée dans 3.x, tandis que pipes.quote est resté.
Beni Cherniavsky-Paskin
9
Correction: officiellement documenté comme shlex.quote()en 3.3, pipes.quote()conservé pour compatibilité. [ bugs.python.org/issue9723]
Beni Cherniavsky-Paskin
7
pipes ne fonctionne PAS sous Windows - ajoute des guillemets simples au lieu de guillemets doubles.
Nux
58

Peut-être avez-vous une raison spécifique d'utiliser os.system(). Mais sinon, vous devriez probablement utiliser le subprocessmodule . Vous pouvez spécifier les tuyaux directement et éviter d'utiliser la coque.

Ce qui suit est de PEP324 :

Replacing shell pipe line
-------------------------

output=`dmesg | grep hda`
==>
p1 = Popen(["dmesg"], stdout=PIPE)
p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
output = p2.communicate()[0]
Jamie
la source
6
subprocess(en particulier avec check_calletc.) est souvent considérablement supérieur, mais il y a quelques cas où l'échappement du shell est toujours utile. Le principal que je rencontre est lorsque je dois appeler des commandes à distance ssh.
Craig Ringer
@CraigRinger, ouais, la communication à distance ssh est ce qui m'a amené ici. : PI souhaite que ssh ait quelque chose pour aider ici.
Jürgen A. Erhard
@ JürgenA.Erhard Cela semble étrange qu'il n'ait pas d'option --execvp-remote (ou fonctionne de cette façon par défaut). Tout faire à travers la coquille semble maladroit et risqué. OTOH, ssh est plein de bizarreries étranges, souvent des choses faites dans une vision étroite de la «sécurité» qui pousse les gens à trouver des solutions de contournement beaucoup plus peu sûres.
Craig Ringer
10

Peut subprocess.list2cmdline- être est-ce un meilleur coup?

Gary Shi
la source
Cela semble plutôt bien. Intéressant, il n'est pas documenté ... ( au moins dans docs.python.org/library/subprocess.html )
Tom
4
Il ne s'échappe pas correctement \: subprocess.list2cmdline(["'",'',"\\",'"'])donne' "" \ \"
Tino
Il n'échappe pas aux symboles d'expansion de shell
grep
Subprocess.list2cmdline () est-il destiné uniquement à Windows?
JS.
@JS Oui, list2cmdlineconforme à la syntaxe Windows cmd.exe ( voir la fonction docstring dans le code source Python ). shlex.quoteest conforme à la syntaxe du shell bourne d'Unix, mais ce n'est généralement pas nécessaire car Unix a un bon support pour passer directement des arguments. Windows nécessite à peu près que vous passiez une seule chaîne avec tous vos arguments (donc la nécessité d'un échappement approprié).
eestrada
7

Notez que pipes.quote est en fait cassé dans Python 2.5 et Python 3.1 et n'est pas sûr à utiliser - Il ne gère pas les arguments de longueur nulle.

>>> from pipes import quote
>>> args = ['arg1', '', 'arg3']
>>> print 'mycommand %s' % (' '.join(quote(arg) for arg in args))
mycommand arg1  arg3

Voir le numéro 7476 de Python ; il a été corrigé dans Python 2.6 et 3.2 et plus récents.

John Wiseman
la source
4
Quelle version de Python utilisez-vous? La version 2.6 semble produire la sortie correcte: ma commande arg1 `` arg3 (Ce sont deux guillemets simples ensemble, bien que la police sur Stack Overflow rend cela difficile à dire!)
Brandon Rhodes
4

Remarque : Ceci est une réponse pour Python 2.7.x.

Selon la source , pipes.quote()est un moyen de " citer de manière fiable une chaîne comme argument unique pour / bin / sh ". (Bien qu'il soit obsolète depuis la version 2.7 et finalement exposé publiquement dans Python 3.3 en tant queshlex.quote() fonction.)

D' autre part , subprocess.list2cmdline()est un moyen de " Traduire une séquence d'arguments en une chaîne de ligne de commande, en utilisant les mêmes règles que le runtime MS C ".

Nous voici, la manière indépendante de la plate-forme de citer des chaînes pour les lignes de commande.

import sys
mswindows = (sys.platform == "win32")

if mswindows:
    from subprocess import list2cmdline
    quote_args = list2cmdline
else:
    # POSIX
    from pipes import quote

    def quote_args(seq):
        return ' '.join(quote(arg) for arg in seq)

Usage:

# Quote a single argument
print quote_args(['my argument'])

# Quote multiple arguments
my_args = ['This', 'is', 'my arguments']
print quote_args(my_args)
Rockallite
la source
3

Je crois que os.system appelle simplement le shell de commande configuré pour l'utilisateur, donc je ne pense pas que vous puissiez le faire d'une manière indépendante de la plate-forme. Mon shell de commande peut être n'importe quoi de bash, emacs, ruby ​​ou même quake3. Certains de ces programmes n'attendent pas le type d'arguments que vous leur transmettez et même s'ils le faisaient, il n'y a aucune garantie qu'ils échappent de la même manière.

Pauldoo
la source
2
Il n'est pas déraisonnable de s'attendre à un shell en grande partie ou entièrement compatible POSIX (du moins partout mais avec Windows, et vous savez de toute façon quel "shell" vous avez alors). os.system n'utilise pas $ SHELL, du moins pas ici.
2

La fonction que j'utilise est:

def quote_argument(argument):
    return '"%s"' % (
        argument
        .replace('\\', '\\\\')
        .replace('"', '\\"')
        .replace('$', '\\$')
        .replace('`', '\\`')
    )

c'est-à-dire: je place toujours l'argument entre des guillemets doubles, puis je cite les seuls caractères spéciaux entre guillemets doubles.

tzot
la source
Notez que vous devez utiliser '\\ "', '\\ $' et '\`', sinon l'échappement ne se produit pas.
JanKanis
1
De plus, il y a des problèmes avec l'utilisation de guillemets doubles dans certains paramètres régionaux (étranges) ; les utilisations de correctif suggérées pipes.quoteque @JohnWiseman a souligné est également cassée. La réponse de Greg Hewgill est donc celle à utiliser. (C'est aussi celui que les coquilles utilisent en interne pour les cas normaux.)
mirabilos
-3

Si vous utilisez la commande système, j'essaierais de mettre en liste blanche ce qui entre dans l'appel os.system () .. Par exemple ..

clean_user_input re.sub("[^a-zA-Z]", "", user_input)
os.system("ls %s" % (clean_user_input))

Le module de sous-processus est une meilleure option, et je recommanderais d'essayer d'éviter d'utiliser quelque chose comme os.system / subprocess dans la mesure du possible.

dbr
la source
-3

La vraie réponse est: ne pas utiliser os.system()en premier lieu. Utilisez à la subprocess.callplace et fournissez les arguments sans échappement.

Scarabée
la source
6
La question contient un exemple où le sous-processus échoue tout simplement. Si vous pouvez utiliser un sous-processus, vous devriez, bien sûr. Mais si vous ne pouvez pas ... le sous-processus n'est pas une solution pour tout . Oh, et votre réponse ne répond pas du tout à la question.
Jürgen A. Erhard
@ JürgenA.Erhard L'exemple de l'OP n'échoue-t-il pas parce qu'il veut utiliser des tubes shell? Vous devez toujours utiliser un sous - processus car il n'utilise pas de shell. C'est un exemple un peu maladroit , mais vous pouvez faire des tubes dans des sous-processus natifs, il y a quelques paquets pypi qui essaient de rendre cela plus facile. J'ai tendance à faire le plus possible le post-traitement dont j'ai besoin en python.Vous pouvez toujours créer vos propres tampons StringIO et contrôler les choses assez complètement avec des sous-processus.
ThorSummoner