Historique des révisions (première version, publiée le 2 Sep 2026)
- Première publication
« Ça passait en test, mais l’outil externe ne démarre pas sur un PC dont le chemin contient un espace. » « J’ai passé C:\data\ et ça a été fusionné avec l’argument suivant en un seul. » « J’ai passé du JSON en argument, les guillemets ont disparu, et l’autre côté n’a pas pu l’analyser. » Ce sont des échecs qui reviennent sans cesse dans le code qui lance des processus enfants. La plupart ne viennent pas de la logique, mais d’un code écrit sans la prémisse que Windows n’a pas de mécanisme pour passer un « tableau d’arguments ».
Ce que reçoit CreateProcess, la fonction qui crée un processus sous Windows, est une seule chaîne appelée lpCommandLine. Aussi soigneusement que l’appelant prépare un tableau, il est toujours concaténé en une chaîne lorsqu’il franchit la frontière du système d’exploitation, et le destinataire le découpe à nouveau. Les règles de découpage sont décidées par le runtime du destinataire, et le runtime C, CommandLineToArgvW, le runtime .NET et cmd.exe sont chacun un code distinct. Passer des arguments, c’est assembler une chaîne que l’analyseur de l’autre côté recouppe en morceaux d’origine.
Cet article se place du point de vue du lancement de processus enfants depuis du code Win32 et .NET, et non depuis des scripts PowerShell, et expose où la chaîne est concaténée, où elle est découpée, et quelles règles s’appliquent. Le côté PowerShell (le changement du passage d’arguments en 7.3, --%, $PSNativeCommandArgumentPassing) est traité dans « Bien appeler un exe externe depuis PowerShell », aussi cet article creuse la couche en dessous.
flowchart TB
accTitle: La couche que cet article couvre
accDescr: Le passage d'arguments PowerShell est traité dans un article distinct ; cet article couvre la couche en dessous, de Win32 CreateProcess et .NET ProcessStartInfo jusqu'à l'analyseur de l'exe cible
ps["Passage d'arguments PowerShell (article distinct)"] --> net[".NET ProcessStartInfo"]
net --> win["Win32 CreateProcessW"]
win --> str["Une seule chaîne de ligne de commande"]
str --> parser["L'analyseur de l'exe cible"]
net -.->|"Périmètre de cet article"| parser
Figure 1 : Sous PowerShell se trouvent les couches .NET et Win32, et d’où que l’on lance, le résultat est une seule chaîne. Cet article traite les règles de cette couche.
1. D’abord la conclusion
- Un processus Windows ne reçoit jamais un tableau d’arguments. La chaîne unique passée à
CreateProcessparvient au nouveau processus (le système d’exploitation peut seulement compléter le nom de l’exécutable en tête par le chemin complet), etGetCommandLineWla renvoie.argvest créé par le destinataire lui-même.1 2 - Le cœur des règles de découpage tient en trois points : découper sur les espaces et les tabulations, ne pas découper à l’intérieur d’une zone entre guillemets doubles, et une barre oblique inverse n’est particulière que lorsqu’un guillemet double la suit immédiatement (2n barres obliques inverses deviennent n plus le guillemet ouvre ou ferme le quotage ; 2n+1 deviennent n plus un guillemet littéral).3 4
- Seul le jeton de tête (
argv[0], le nom de l’exécutable) suit une autre règle : on peut l’entourer de guillemets, mais l’échappement par barre oblique inverse ne s’applique pas. SilpApplicationNameestNULL, l’interprétation d’un chemin contenant des espaces devient ambiguë etC:\Program.exeest essayé en premier.1 4 - Côté assemblage, une règle suffit : « si l’argument contient un espace ou un guillemet, ou s’il est vide, l’entourer de guillemets, doubler les barres obliques inverses qui précèdent un guillemet et les barres obliques inverses finales, et écrire les guillemets en
\". »ProcessStartInfo.ArgumentListdans .NET Core 2.1 et versions ultérieures le fait pour vous.5 6 - Ne générez pas la forme qui place deux guillemets adjacents à l’intérieur d’un argument non vide (quelque chose comme
"ab""c"), parce que les destinataires l’interprètent différemment. Le""qui représente un argument vide est autre chose et est correct. cmd.exe et les fichiers batch se situent hors de ces règles, aussi ne faites pas transiter par eux des valeurs non fiables.6 7 - Les limites sont 32 767 unités de code UTF-16 pour
lpCommandLine(y compris le caractère nul terminal ; les caractères en paires de substitution, comme les emoji, comptent pour deux) et 8 191 caractères pour cmd.exe. Si vous risquez de les dépasser, passez à un fichier de réponse, mais seulement lorsque la cible peut en lire un (ou peut être corrigée pour en lire un) avec une syntaxe telle que@file.1 8
Dans le diagramme, un trait continu marque une relation qui vaut toujours et un trait pointillé une relation conditionnelle (les conditions figurent dans l’explication de chaque relation sur la page de détail). La liste complète des relations (28 au total, avec preuve et niveau de certitude) et les définitions des concepts principaux sont rassemblées sur la page de détail de la carte des connaissances (en japonais). Données : JSON-LD / Turtle
2. Il n’y a pas de tableau d’arguments — CreateProcess et la chaîne unique
Le second paramètre de CreateProcessW, lpCommandLine, est une seule chaîne terminée par un nul dans laquelle le nom de l’exécutable et les arguments sont disposés, séparés par des espaces. La limite de longueur est de 32 767 unités de code UTF-16 y compris le caractère nul terminal (le nombre d’éléments wchar_t ; un caractère en paire de substitution tel qu’un emoji en consomme deux par caractère, ne vérifiez donc jamais à l’avance d’après le nombre apparent de caractères), et parce que la version Unicode peut modifier cette chaîne, passer un littéral de chaîne ou un tampon const peut provoquer une violation d’accès.1
Cette chaîne est remise telle quelle au nouveau processus comme partie de ses paramètres de processus, et le processus enfant la récupère avec GetCommandLineW. Parce que le système d’exploitation peut compléter le nom de l’exécutable en tête par le chemin complet, la chaîne que voit l’enfant ne correspond pas nécessairement exactement à celle que le parent a passée.2 Le lpCmdLine passé au WinMain d’une application graphique est cette chaîne privée du nom du programme.9
flowchart TB
accTitle: Le chemin que prennent les arguments pour parvenir au processus enfant
accDescr: Le tableau d'arguments de l'appelant est concaténé en une seule chaîne dans lpCommandLine de CreateProcess et passé au nouveau processus, et le processus enfant découpe avec son propre analyseur la chaîne récupérée par GetCommandLineW pour créer argv
arr["Le tableau d'arguments de l'appelant"] --> join["Concaténé en une chaîne (responsabilité de l'appelant)"]
join --> cp["lpCommandLine de CreateProcessW"]
cp --> peb["Les paramètres de processus du nouveau processus"]
peb --> gcl["La chaîne que GetCommandLineW renvoie"]
gcl --> parse["L'analyseur du destinataire la découpe"]
parse --> argv["Le tableau argv / args"]
Figure 2 : Le tableau ne franchit pas la frontière. La concaténation est la responsabilité de l’appelant, le découpage celle du destinataire, et le tableau d’origine n’est restauré que lorsque les règles des deux côtés s’accordent.
Le point à retenir ici est que la concaténation et le découpage se produisent dans des processus différents, dans un code différent. L’appelant ne peut pas concaténer correctement sans savoir avec quoi l’autre côté va découper, et le destinataire n’a aucun moyen de savoir comment la chaîne a été concaténée. Sur les systèmes de type Unix, execve accepte le tableau tel quel, aussi ce problème n’existe pas. C’est une prémisse propre à Windows, mais qui accompagne chaque lancement de processus.
3. Qui découpe — trois analyseurs
Côté destinataire, le code qui découpe la chaîne en argv se présente surtout sous trois formes.
| Destinataire | Code qui découpe | Quand il est invoqué |
|---|---|---|
main / wmain en C/C++ |
Le code de démarrage du runtime C de MSVC | Crée argc / argv automatiquement au démarrage du programme4 |
| Code qui utilise l’API Win32 directement | CommandLineToArgvW |
On lui passe la valeur de retour de GetCommandLineW pour la convertir en forme argv3 |
Main(string[] args) / Environment.GetCommandLineArgs() de .NET (la configuration habituelle lancée via l’apphost ou dotnet.exe) |
Le code de démarrage du runtime C de l’hôte (apphost / dotnet.exe) |
Sous Windows l’hôte est un programme wmain ; il prend l’argv que le runtime C a construit, retire ses propres options et le chemin de l’application, et passe le reste au runtime avec le chemin de l’application. Au démarrage le runtime construit un tableau dont le premier élément est le nom du programme (le nom de lancement passé par l’hôte, ou le chemin de l’assembly s’il n’y en a pas) et le conserve pour GetCommandLineArgs(), tandis que Main reçoit dans args seulement les arguments privés du nom du programme10 11 12 |
| Une configuration qui charge le runtime .NET comme bibliothèque hébergée et ne reçoit pas d’arguments de démarrage | Le code de découpage propre au runtime .NET (SegmentCommandLine) |
En repli, GetCommandLineArgs() découpe lui-même la valeur de retour de GetCommandLineW. Il est implémenté pour correspondre aux règles du runtime C et n’utilise pas CommandLineToArgvW, parce que celui-ci « se comporte légèrement autrement »12 |
Le code de découpage vient en trois lignées, le code de démarrage du runtime C, CommandLineToArgvW, et le code de découpage propre au runtime .NET, et ils implémentent des règles au même squelette, mais ce n’est pas le même code. Une application .NET lancée via l’apphost ou dotnet.exe est en pratique découpée selon les règles de la première lignée (code de démarrage du runtime C), parce que l’hôte lui-même est un programme wmain construit avec le runtime C de MSVC. Le code source du runtime .NET porte encore un commentaire indiquant que CommandLineToArgvW n’est pas utilisé parce qu’il se comporte légèrement autrement.12 Les différences apparaissent aux bords, comme le traitement de "" décrit plus loin, et les arguments du quotidien les rencontrent rarement, mais supposer que « les règles sont les mêmes, donc tout passe » est ce qui casse aux bords.
flowchart TB
accTitle: Les trois analyseurs côté destinataire
accDescr: La chaîne unique que GetCommandLineW renvoie est découpée par le code de démarrage du runtime C pour C/C++, par CommandLineToArgvW pour un usage Win32 direct, et par le code de découpage propre au runtime pour .NET chargé comme bibliothèque hébergée ; chacun suit des règles au même squelette mais est une implémentation distincte. Une application .NET normale lancée via l'apphost ou dotnet.exe reçoit le tableau découpé par le code de démarrage du runtime C de l'hôte
s["La chaîne de GetCommandLineW"] --> crt["Code de démarrage du runtime C"]
s --> api["CommandLineToArgvW"]
s --> net["Code de découpage propre à .NET (chargement par un hôte)"]
crt --> app[".NET via apphost / dotnet.exe est le même"]
crt --> same["Même squelette de règles, implémentations distinctes"]
api --> same
net --> same
Figure 3 : Il y a trois lignées de code de découpage. Une application .NET lancée via l’apphost ou dotnet.exe reçoit le tableau découpé par le code de démarrage du runtime C de l’hôte, et le code de découpage propre au runtime est le repli de la configuration en bibliothèque hébergée. Parce que l’on ne peut pas voir de l’extérieur lequel l’exe cible exécute, la réponse pratique est d’assembler une chaîne qui donne le même résultat sur tous.
Notez que args dans Main(string[] args) de .NET n’inclut pas le nom du programme, alors que le premier élément de Environment.GetCommandLineArgs() l’inclut. Ce dernier occupe la même place que argv[0] en C/C++.13 Dans un lancement normal tel que dotnet app.dll x, l’hôte retire les options de l’hôte et le chemin de l’application (dotnet.exe et app.dll), et seul x parvient à args dans Main.14 GetCommandLineArgs(), de son côté, renvoie le tableau auquel le runtime a ajouté en tête le nom du programme au démarrage (le chemin de app.dll suivi de x).11 Le code de découpage propre au runtime ne découpe GetCommandLineW que dans la configuration en bibliothèque hébergée qui ne reçoit pas d’arguments de démarrage ; dans une configuration où un hôte natif passe son propre argc/argv et appelle Main, args dans Main est ce que l’hôte a passé.
4. Les règles de découpage — espaces, guillemets et barres obliques inverses
Voici les règles que partagent les trois analyseurs, pour argv[1] et la suite.3 4
- Les arguments sont séparés par des espaces ou des tabulations.
- Une zone entourée de guillemets doubles devient un argument même si elle contient des espaces. Les guillemets eux-mêmes ne font pas partie de l’argument. Un guillemet peut commencer au milieu d’un argument, et si la chaîne se termine sans guillemet fermant, tout jusqu’à la fin devient le dernier argument.
- Une barre oblique inverse est traitée comme un caractère ordinaire. Seulement lorsqu’un guillemet double la suit immédiatement, les règles suivantes s’appliquent.
- Si 2n barres obliques inverses précèdent un guillemet double, n barres obliques inverses sont émises, et le guillemet agit comme « début ou fin du quotage ».
- Si 2n+1 barres obliques inverses précèdent un guillemet double, n barres obliques inverses et un guillemet littéral sont émis, et l’état de quotage ne change pas.
- L’accent circonflexe (
^) n’est pas un caractère d’échappement (c’est une règle de cmd.exe, pas une règle d’analyseur).
L’analyseur conserve un bit d’état, « suis-je entre guillemets », le bascule à chaque guillemet, et lit la chaîne de gauche à droite. Le fait qu’un espace sépare les arguments est décidé par cet état.
flowchart TB
accTitle: Le flux de découpage qui bascule entre l'intérieur et l'extérieur des guillemets
accDescr: Hors guillemets l'analyseur sépare les arguments sur les espaces ; lorsqu'il rencontre un guillemet il entre et traite les espaces comme partie de l'argument ; lorsqu'il rencontre un autre guillemet il revient à l'extérieur. Une barre oblique inverse n'est traitée de façon particulière que lorsqu'un guillemet la suit immédiatement
out["Hors guillemets : découper sur les espaces"] -->|"Un guillemet est rencontré"| inq["Entre guillemets : les espaces font partie de l'argument"]
inq -->|"Un guillemet est rencontré"| out
out -->|"Une barre oblique inverse est immédiatement suivie d'un guillemet"| bs["Appliquer la règle de la barre oblique inverse"]
inq -->|"Une barre oblique inverse est immédiatement suivie d'un guillemet"| bs
bs -->|"2n : émettre n et ouvrir/fermer"| toggle["Basculer l'état de quotage"]
bs -->|"2n+1 : émettre n et un guillemet littéral"| lit["Conserver l'état de quotage"]
Figure 4 : Le cœur du découpage se décide par un seul bit, « à l’intérieur ou à l’extérieur des guillemets », et le nombre de barres obliques inverses immédiatement avant un guillemet.
Plutôt que de mémoriser les règles en prose, il est plus fiable de regarder la correspondance entre entrée et sortie.
| Partie de la ligne de commande (entrée) | Arguments résultants | Règle en jeu |
|---|---|---|
a b c |
a, b, c |
Découper sur les espaces |
"a b" c |
a b, c |
Une zone quotée n’est pas découpée |
C:\data\ next |
C:\data\, next |
La barre oblique inverse n’est pas suivie d’un guillemet, c’est donc un caractère ordinaire |
"C:\data\\" next |
C:\data\, next |
Les deux avant le guillemet deviennent un, et le guillemet ferme |
"C:\data\" next |
C:\data" next |
Une barre oblique inverse, donc le guillemet devient un guillemet littéral et le quotage ne se ferme jamais, l’argument suivant est englouti |
"say \"hi\"" |
say "hi" |
Un nombre impair, donc des guillemets littéraux |
"" |
Chaîne vide | La seule façon de passer un argument vide |
'a b' |
'a, b' |
Les guillemets simples n’ont pas de signification particulière15 |
La ligne 5 est la véritable identité de « j’ai passé C:\data\ et ça a été fusionné avec l’argument suivant en un seul » de l’ouverture. Dès que vous entourez de guillemets un chemin qui se termine par une barre oblique inverse, le guillemet fermant se transforme en caractère et le quotage ne se ferme jamais.
flowchart TB
accTitle: Comment une barre oblique inverse finale engloutit l'argument suivant
accDescr: Lorsqu'un chemin qui se termine par une barre oblique inverse est entouré de guillemets, le guillemet qui devrait fermer se trouve immédiatement après une seule barre oblique inverse et est interprété comme guillemet littéral, si bien que le quotage ne se ferme jamais et que tout jusqu'à l'argument suivant est lu comme un seul argument
a["Un chemin quoté se terminant par une barre oblique inverse"] --> b["Un nombre impair de barres obliques inverses précède le guillemet fermant"]
b --> c["Le guillemet est émis comme caractère et le quotage ne se ferme pas"]
c --> d["Les espaces suivants ne séparent plus"]
d --> e["Tout jusqu'à l'argument suivant arrive comme un seul argument"]
a -.->|"Doubler la barre oblique inverse"| ok["Le quotage se ferme et les arguments se séparent"]
Figure 5 : Pourquoi « doubler la barre oblique inverse finale » est nécessaire. Un quotage écrit sans connaître les règles casse à la fin d’un chemin.
« Deux guillemets consécutifs à l’intérieur du quotage », là où les implémentations divergent
Les règles du runtime C de MSVC ont encore un point : « deux guillemets consécutifs à l’intérieur d’une chaîne quotée sont traités comme un guillemet » (une forme comme "ab""c", qui est une autre affaire que le "" qui représente un argument vide).4 Les règles officielles de CommandLineToArgvW, toutefois, n’ont pas ce point, et le code d’assemblage du runtime .NET évite explicitement de générer cette forme parce que « un guillemet suivant un guillemet fermant est interprété différemment par VC avant et après 2008 ».6
En tant que destinataire, il suffit de savoir qu’une telle entrée peut arriver. En tant qu’assembleur, lorsque vous voulez passer un guillemet comme caractère, n’utilisez que la forme \". Elle donne le même résultat sur chaque analyseur.
5. argv[0] suit une autre règle — lpApplicationName et le problème Program.exe
Le jeton de tête, c’est-à-dire le nom de l’exécutable, est hors des règles précédentes. On suppose que c’est une chaîne valide comme chemin de système de fichiers, aussi on peut l’entourer de guillemets pour y inclure des espaces, mais les règles d’échappement des barres obliques inverses ne s’appliquent pas. Il n’y a pas non plus de moyen d’inclure un guillemet lui-même dans argv[0].4 3 Le code d’assemblage .NET traite aussi le premier élément à part : « l’entourer de guillemets s’il a des espaces, et lever une exception s’il contient un guillemet ».6
Ce qui devient un problème côté appelant, c’est le comportement lorsque lpApplicationName de CreateProcess est NULL. Dans ce cas, le module à exécuter est déduit du jeton de tête délimité par des espaces de lpCommandLine. Lorsque le chemin contient des espaces, plusieurs candidats apparaissent, et le système d’exploitation les essaie en commençant par le plus court.1
flowchart TB
accTitle: L'ordre dans lequel l'exécutable est deviné lorsque lpApplicationName est NULL
accDescr: Si C:\Program Files\MyApp -L -S est passé sans guillemets, CreateProcess teste C:\Program.exe puis C:\Program Files\MyApp.exe dans cet ordre, si bien que C:\Program.exe s'exécute s'il a été placé là
in["Passer un chemin non quoté (contenant des espaces) dans lpCommandLine"] --> t1["Candidat 1 : essayer C:\Program.exe"]
t1 -->|"Existe"| bad["Un exécutable non voulu démarre"]
t1 -->|"N'existe pas"| t2["Candidat 2 : essayer C:\Program Files\MyApp.exe"]
t2 --> ok["L'exécutable voulu démarre"]
in -.->|"Passer lpApplicationName, ou quoter le jeton de tête"| ok
Figure 6 : Placer un chemin avec des espaces au début sans guillemets fait essayer au système d’exploitation les candidats du plus court. La documentation officielle appelle cela « dangereux » en termes clairs.
La documentation officielle indique que si C:\Program.exe est placé là, il s’exécute à la place de l’application voulue, et demande de ne pas passer NULL pour lpApplicationName et, si on le fait, d’entourer de guillemets le chemin de tête.1 En pratique, faites les deux. Passez le chemin complet de l’exécutable dans lpApplicationName, et placez aussi le même chemin, entouré de guillemets, au début de lpCommandLine. Lorsque les deux sont passés, le module qui s’exécute est décidé par lpApplicationName, et l’argv[0] du processus enfant devient le jeton de tête de lpCommandLine. À moins de maintenir les deux cohérents par convention, le code qui dérive son propre chemin de argv[0] casse. La façon fiable d’obtenir son propre chemin est GetModuleFileNameW.4
flowchart TB
accTitle: Comment le module exécuté et argv[0] sont décidés
accDescr: Lorsque lpApplicationName et lpCommandLine sont tous deux passés, le module qui s'exécute est décidé par lpApplicationName et l'argv[0] de l'enfant est le jeton de tête de lpCommandLine. Le code qui dérive son propre chemin de argv[0] casse lorsque les deux divergent, aussi obtenez votre propre chemin avec GetModuleFileNameW
app["lpApplicationName"] --> run["Le module qui s'exécute"]
cl["Le jeton de tête de lpCommandLine"] --> a0["L'argv[0] de l'enfant"]
a0 -.->|"Casse lorsqu'ils divergent"| self["Code qui dérive son propre chemin de argv[0]"]
self -.->|"Utiliser à la place"| gmf["GetModuleFileNameW"]
Figure 7 : « Ce qui s’exécute » et « ce qui va dans argv[0] » sont décidés séparément. Une conception qui dérive son propre chemin de argv[0] ne peut pas tenir sur cette séparation.
Un point de plus : lorsque lpApplicationName est NULL, la partie nom d’exécutable de lpCommandLine est limitée à MAX_PATH.1 Pour le traitement des chemins longs, voir « MAX_PATH et les pièges des chemins et noms de fichiers Windows ».
6. Les règles côté assemblage — une fonction suffit
Une fois les règles de découpage connues, on peut assembler « une chaîne que l’autre côté recouppe en l’original » simplement en les faisant tourner à l’envers. Pour chaque argument à partir de argv[1], faites ce qui suit.6
- S’il n’est pas vide et ne contient ni espaces ni guillemets, placez-le tel quel.
- Sinon, entourez le tout de guillemets. À l’intérieur du quotage,
- transformez une suite de k barres obliques inverses immédiatement avant un guillemet en 2k+1 puis placez le guillemet (rendre le nombre impair en fait un « guillemet littéral ») ;
- transformez une suite de k barres obliques inverses finales en 2k (elles précèdent le guillemet fermant, aussi un nombre pair en fait la « fin du quotage ») ;
- laissez toute autre barre oblique inverse telle quelle.
- Placez une chaîne vide comme
"".
flowchart TB
accTitle: Le flux de décision pour assembler un argument
accDescr: Si l'argument n'est pas vide et ne contient ni espaces ni guillemets, le placer tel quel ; sinon l'entourer de guillemets, rendre les barres obliques inverses avant un guillemet 2k+1 et les barres obliques inverses finales 2k, préfixer les guillemets d'une barre oblique inverse, et fermer
s["Recevoir un argument"] --> q{"Vide, ou contient un espace ou un guillemet ?"}
q -->|"Non"| raw["Le placer tel quel"]
q -->|"Oui"| open["Guillemet ouvrant"]
open --> scan["Parcourir de gauche à droite"]
scan --> bq["k barres obliques inverses avant un guillemet → 2k+1"]
scan --> be["k barres obliques inverses finales → 2k"]
scan --> other["Tout le reste tel quel"]
bq --> close["Guillemet fermant"]
be --> close
other --> close
Figure 8 : L’assemblage est l’inverse des règles de découpage. Il n’y a que trois branches, et vous n’ajustez le nombre de barres obliques inverses qu’à la fin et immédiatement avant un guillemet ; avec cela, toute chaîne fait l’aller-retour, pourvu que le destinataire découpe les caractères larges avec les mêmes règles de découpage que CommandLineToArgvW, le runtime C et .NET (chapitre 4) (une cible qui interprète la ligne de commande brute avec sa propre grammaire, ou un analyseur de shell intercalé, sort du périmètre), n’a pas activé l’expansion des jokers telle que wsetargv.obj, la chaîne ne contient pas de caractères NUL, et la chaîne assemblée tient dans la limite de lpCommandLine (32 767 unités de code UTF-16 y compris le caractère nul terminal). (La ligne de commande est une chaîne terminée par un nul, aussi un caractère NUL est la seule chose qui ne peut pas être passée par principe. Sur une cible avec l’expansion des jokers activée, un argument contenant * ou ? est remplacé par des noms de fichiers ; voir le chapitre 8. Une chaîne au-delà de la limite est rejetée par CreateProcessW ; voir le chapitre 10.)
Cette règle reflète telle quelle l’asymétrie « une barre oblique inverse n’est particulière que immédiatement avant un guillemet ». Il n’est pas nécessaire de doubler mécaniquement les barres obliques inverses qui séparent les composants de chemin ; le point est que vous ne touchez que celles immédiatement avant un guillemet et à la fin.
7. Implémentation en .NET — ArgumentList et Arguments
ProcessStartInfo dans .NET Core 2.1 et versions ultérieures a ArgumentList, qui prend en charge cet assemblage. Un élément est un argument, les chaînes que vous ajoutez n’ont pas besoin d’échappement à l’avance, et à Process.Start .NET les assemble en interne en une chaîne et la remet au système d’exploitation.5
var psi = new ProcessStartInfo
{
FileName = @"C:\Program Files\MyTool\convert.exe",
UseShellExecute = false,
};
psi.ArgumentList.Add("--input");
psi.ArgumentList.Add(inputPath); // peut contenir des espaces, des barres obliques inverses finales et des guillemets
psi.ArgumentList.Add("--output");
psi.ArgumentList.Add(outputPath);
psi.ArgumentList.Add("--label");
psi.ArgumentList.Add(""); // un argument vide est passé correctement comme ""
using var proc = Process.Start(psi)
?? throw new InvalidOperationException("Process.Start a renvoyé null");
proc.WaitForExit();
if (proc.ExitCode != 0)
throw new InvalidOperationException($"convert.exe a échoué (ExitCode={proc.ExitCode})");
Arguments est une propriété qui passe telle quelle une seule chaîne que vous avez assemblée vous-même. Les deux sont indépendantes, et lorsque vous utilisez l’une, l’autre doit être vide.16 La documentation officielle conseille aussi de choisir ArgumentList si vous n’êtes pas sûr du quotage.5
flowchart TB
accTitle: Où ArgumentList et Arguments deviennent une chaîne
accDescr: Avec ArgumentList, .NET échappe chaque élément et assemble une seule chaîne avant de la passer à CreateProcess ; avec Arguments, la chaîne assemblée par l'appelant est passée telle quelle. Dans les deux cas, ce qui parvient au système d'exploitation est une seule chaîne
al["ArgumentList (1 élément = 1 argument)"] --> esc[".NET échappe chaque élément et concatène"]
ar["Arguments (une seule chaîne assemblée par vous)"] --> pass["Telle quelle"]
esc --> cmd["Une seule chaîne de ligne de commande"]
pass --> cmd
cmd --> cp["CreateProcess"]
Figure 9 : Quelle que soit celle que vous utilisez, ce qui parvient au système d’exploitation est une seule chaîne. La seule différence est qui l’assemble, et ArgumentList laisse cela au côté qui connaît les règles.
Le code d’assemblage derrière ArgumentList est exactement les règles du chapitre 6. Si l’argument n’est pas vide et ne contient ni espaces ni guillemets, il est placé tel quel ; sinon il est entouré de guillemets, les barres obliques inverses immédiatement avant un guillemet deviennent 2k+1, les barres obliques inverses finales deviennent 2k, et chaque guillemet est toujours préfixé d’une barre oblique inverse. Il ne génère jamais la forme avec des guillemets adjacents à l’intérieur d’un argument non vide. Seul un argument vide est placé comme "", et c’est la forme correcte.6
Sous .NET Framework, assemblez vous-même
ArgumentList est une API introduite dans .NET Core 2.1 et n’existe pas sur ProcessStartInfo sous .NET Framework.5 Dans une application .NET Framework 4.8, ou un outil interne construit dessus, écrivez vous-même les règles du chapitre 6 et passez le résultat à Arguments.
// Pour .NET Framework. Assemble la chaîne unique à passer à ProcessStartInfo.Arguments.
// Les règles sont les mêmes que ProcessStartInfo.ArgumentList utilise en interne.
static string BuildArguments(IEnumerable<string> args)
{
var sb = new StringBuilder();
foreach (var arg in args)
{
if (sb.Length > 0) sb.Append(' ');
AppendArgument(sb, arg);
}
return sb.ToString();
}
static void AppendArgument(StringBuilder sb, string arg)
{
if (arg.IndexOf('\0') >= 0)
throw new ArgumentException("Un argument ne peut pas contenir un caractère NUL (la ligne de commande est une chaîne terminée par un nul et serait coupée là)");
bool needsQuote = arg.Length == 0 || arg.Any(c => char.IsWhiteSpace(c) || c == '"');
if (!needsQuote)
{
sb.Append(arg); // tel quel
return;
}
sb.Append('"');
int i = 0;
while (i < arg.Length)
{
int backslashes = 0;
while (i < arg.Length && arg[i] == '\\') { i++; backslashes++; }
if (i == arg.Length)
{
sb.Append('\\', backslashes * 2); // final : doublé parce que le guillemet fermant suit
}
else if (arg[i] == '"')
{
sb.Append('\\', backslashes * 2 + 1).Append('"'); // avant un guillemet : doublé plus un
i++;
}
else
{
sb.Append('\\', backslashes).Append(arg[i]); // tout le reste : tel quel
i++;
}
}
sb.Append('"');
}
Voici les entrées et les sorties côte à côte.
| Valeur à passer | Chaîne que AppendArgument émet |
|---|---|
strict |
strict |
| Chaîne vide | "" |
C:\Program Files\input |
"C:\Program Files\input" |
C:\Program Files\input\ |
"C:\Program Files\input\\" |
say "hi" |
"say \"hi\"" |
a\"b |
"a\\\"b" |
C:\data\ (sans espaces) |
C:\data\ |
Notez la dernière ligne. Une valeur qui ne contient ni espaces ni guillemets n’est pas entourée, aussi la barre oblique inverse finale sort telle quelle. Sans entourage, les règles 4 et 5 ne se déclenchent jamais, et C:\data\ arrive correctement.
flowchart TB
accTitle: Le choix de la méthode d'assemblage selon la version de .NET
accDescr: Sous .NET Core 2.1 ou ultérieur, laisser faire ProcessStartInfo.ArgumentList ; sous .NET Framework, assembler la chaîne Arguments avec une fonction maison suivant les mêmes règles. Dans aucun des deux cas on n'écrit les guillemets à la main par concaténation de chaînes
v{"Quelle version de .NET ?"}
v -->|"Core 2.1 ou ultérieur"| al["Ajouter à ArgumentList un élément à la fois"]
v -->|"Framework"| own["Assembler Arguments avec une fonction maison"]
al --> no["Ne jamais écrire les guillemets à la main"]
own --> no
Figure 10 : Deux méthodes, un principe. Tenez-vous à « ne jamais écrire les guillemets à la main » et la casse à la fin d’un chemin n’arrive jamais.
Notez qu’avec UseShellExecute = true, le lancement passe par ShellExecuteEx plutôt que par CreateProcess, et le contenu de ArgumentList devient les paramètres passés au shell. Lorsqu’on ouvre un document ou une URL, l’association de fichiers assemble la ligne de commande réelle du gestionnaire, aussi la chaîne assemblée ici n’atteint pas nécessairement la cible telle quelle. Pour les usages où vous redirigez la sortie ou avez besoin du code de sortie de façon fiable, mettez UseShellExecute = false et concevez le code pour lire la sortie standard et l’erreur standard en même temps. Cette partie est traitée dans « Checklist pour gérer les processus enfants en toute sécurité dans une application Windows ».
8. Implémentation en C++ / Win32
En C++ vous écrivez les deux côtés vous-même, assemblage et découpage. Pour l’assemblage, transformez directement les règles du chapitre 6 en une fonction.
#include <windows.h>
#include <string>
#include <stdexcept>
#include <string_view>
#include <vector>
// Ajoute un argument pour argv[1] et la suite. Les règles sont l'inverse des règles de découpage de CommandLineToArgvW / CRT.
void AppendArgument(std::wstring& cmd, std::wstring_view arg)
{
if (!cmd.empty()) cmd += L' ';
if (arg.find(L'\0') != std::wstring_view::npos)
throw std::invalid_argument("Un argument ne peut pas contenir un caractère NUL (la ligne de commande est une chaîne terminée par un nul et serait coupée là)");
const bool needsQuote =
arg.empty() || arg.find_first_of(L" \t\"") != std::wstring_view::npos;
if (!needsQuote) { cmd += arg; return; }
cmd += L'"';
for (size_t i = 0; ; ) {
size_t backslashes = 0;
while (i < arg.size() && arg[i] == L'\\') { ++i; ++backslashes; }
if (i == arg.size()) {
cmd.append(backslashes * 2, L'\\'); // final : doublé
break;
}
if (arg[i] == L'"') {
cmd.append(backslashes * 2 + 1, L'\\'); // avant un guillemet : doublé plus un
cmd += L'"';
} else {
cmd.append(backslashes, L'\\'); // tout le reste : tel quel
cmd += arg[i];
}
++i;
}
cmd += L'"';
}
// argv[0] (l'exécutable) suit une autre règle : seulement l'entourer de guillemets s'il a des espaces. Il ne peut pas contenir un guillemet.
std::wstring QuoteArgv0(std::wstring_view exe)
{
if (exe.find(L'\0') != std::wstring_view::npos)
throw std::invalid_argument("Le chemin de l'exécutable ne peut pas contenir un caractère NUL (lpApplicationName et la ligne de commande seraient coupés là, et le chemin jusqu'à ce point pourrait être lancé)");
if (exe.find(L'"') != std::wstring_view::npos)
throw std::invalid_argument("Le chemin de l'exécutable ne peut pas contenir un guillemet");
if (exe.empty() || exe.find_first_of(L" \t") != std::wstring_view::npos)
return L'"' + std::wstring(exe) + L'"';
return std::wstring(exe);
}
Dans l’appel, passez le chemin complet de l’exécutable dans lpApplicationName et un tampon inscriptible dans lpCommandLine.
const std::wstring exe = LR"(C:\Program Files\MyTool\convert.exe)";
std::wstring cmd = QuoteArgv0(exe); // garder argv[0] cohérent avec l'exécutable
AppendArgument(cmd, L"--input");
AppendArgument(cmd, inputPath);
AppendArgument(cmd, L"--output");
AppendArgument(cmd, outputPath);
std::vector<wchar_t> buffer(cmd.begin(), cmd.end());
buffer.push_back(L'\0'); // CreateProcessW peut modifier la chaîne
STARTUPINFOW si{}; si.cb = sizeof(si);
PROCESS_INFORMATION pi{};
if (!CreateProcessW(exe.c_str(), // lpApplicationName : jamais NULL
buffer.data(), // lpCommandLine : commence par le même chemin, quoté
nullptr, nullptr, FALSE, CREATE_UNICODE_ENVIRONMENT,
nullptr, nullptr, &si, &pi)) {
const DWORD err = GetLastError();
// Journaliser err ici et le renvoyer à l'appelant. Ne pas l'avaler
return;
}
CloseHandle(pi.hThread); // le handle du thread principal n'est pas nécessaire, le fermer d'abord
switch (WaitForSingleObject(pi.hProcess, INFINITE)) { // ajouter un délai d'attente si besoin
case WAIT_OBJECT_0: { // il s'est terminé. Lire le code de sortie seulement dans cette branche
DWORD exitCode = 0;
if (!GetExitCodeProcess(pi.hProcess, &exitCode)) {
const DWORD err = GetLastError();
// Journaliser aussi l'échec de la récupération, et le renvoyer à l'appelant comme un échec
} else if (exitCode != 0) {
// La cible a démarré mais son traitement a échoué. Ne pas le traiter comme 0 ;
// journaliser le code de sortie et le renvoyer à l'appelant (comme le contrôle d'ExitCode dans l'exemple C#)
}
break;
}
case WAIT_TIMEOUT:
// Il tourne encore. Appeler GetExitCodeProcess ici ne renvoie que STILL_ACTIVE (259),
// ce n'est pas un code de sortie. Cet exemple prend la politique « replier un dépassement de délai en échec » :
// seulement lorsque la demande de terminaison passe, on le voit se terminer, puis on passe à CloseHandle ci-dessous.
// Si la politique est de continuer à attendre, ne pas faire break ici et fermer les handles (cela
// lâcherait l'enfant alors qu'il tourne encore). Revenir à l'attente
if (!TerminateProcess(pi.hProcess, 1)) {
const DWORD err = GetLastError();
// Impossible de le terminer (droits insuffisants, etc.). Attendre ici avec INFINITE rendrait
// sans objet le délai ajouté pour empêcher les dépassements. Journaliser err et renvoyer un échec à
// l'appelant sans attendre (l'enfant est lâché alors qu'il tourne encore, journaliser cela aussi)
break;
}
WaitForSingleObject(pi.hProcess, INFINITE); // la demande de terminaison est passée, donc voir la fin avant de fermer
// Renvoyer le dépassement de délai à l'appelant comme un échec
break;
default: { // WAIT_FAILED
const DWORD err = GetLastError();
// Journaliser aussi l'échec de l'attente elle-même
break;
}
}
CloseHandle(pi.hProcess); // l'oublier fait fuir un handle à chaque lancement
flowchart TB
accTitle: La répartition des rôles des deux arguments passés à CreateProcessW
accDescr: lpApplicationName fixe le module à exécuter, et lpCommandLine décide la chaîne que le processus enfant reçoit via GetCommandLineW. Passer lpCommandLine comme tampon inscriptible et garder l'argv[0] de tête cohérent avec lpApplicationName
app["lpApplicationName : le chemin complet de l'exécutable"] --> mod["Le module à exécuter est fixé"]
cl["lpCommandLine : un tampon inscriptible"] --> child["La chaîne que l'enfant reçoit via GetCommandLineW"]
child --> a0["Jeton de tête = argv[0]"]
a0 -.->|"Garder cohérent"| app
child --> rest["Le reste = arguments assemblés selon les règles du chapitre 6"]
Figure 11 : « Quoi exécuter » et « quoi passer » sont décidés par des arguments différents. Rendez les deux explicites et ni le problème Program.exe ni la violation d’accès d’un tampon non inscriptible ne se produisent.
Côté destinataire, passez la valeur de retour de GetCommandLineW à CommandLineToArgvW pour l’obtenir sous forme argv. Libérez la valeur de retour avec un seul LocalFree. Il y a des comportements de bord : si lpCmdLine est une chaîne vide, le chemin de l’exécutable courant est renvoyé, et s’il commence par un espace, le premier argument devient une chaîne vide.3
int argc = 0;
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
if (argv == nullptr) {
const DWORD err = GetLastError();
// Journaliser aussi l'échec d'analyse
return 1;
}
for (int i = 0; i < argc; ++i) {
// argv[0] est le nom de l'exécutable. Le système d'exploitation peut avoir complété le chemin complet
}
LocalFree(argv);
Si vous utilisez main / wmain, le runtime C fait la même chose pour vous au démarrage. Notez toutefois que argv dans main est une chaîne étroite convertie vers la page de codes courante, aussi les caractères que la page de codes ne peut pas représenter (par exemple un chemin japonais sur un PC hors d’un environnement japonais) sont perdus ici. La fonction d’assemblage du chapitre 6 « fait l’aller-retour » vis-à-vis des destinataires qui découpent les caractères larges tels quels, comme wmain, CommandLineToArgvW et .NET. Par défaut les jokers ne sont pas étendus, mais lier setargv.obj (wsetargv.obj pour wmain) fait étendre * et ?.4 Si vous passez un argument contenant * dans un nom de fichier à une cible avec ce réglage, les arguments qui arrivent diffèrent de ce que vous visiez.
9. Lorsque cmd.exe et les fichiers batch s’intercalent
Les règles précédentes s’appliquent lorsque la chaîne va directement de CreateProcess à l’exe cible. Lorsque cmd.exe s’intercale, une étape d’interprétation supplémentaire s’ajoute.
cmd.exe traite &, |, ( et ) comme syntaxe, et pour les passer comme arguments il faut les échapper avec ^ ou les entourer de guillemets. Le traitement des guillemets dans la chaîne qui suit /c ou /k a ses propres règles, et le fait que « les guillemets extérieurs soient retirés » change avec la présence de /s, le nombre de guillemets et la présence de caractères spéciaux.17 De plus, un fichier batch reçoit les arguments non découpés mais comme chaîne brute de ligne de commande. La documentation officielle de PowerShell met clairement en garde contre le passage d’entrées non fiables à des fichiers batch.7 La documentation de CreateProcess dit que pour lancer un fichier batch on spécifie cmd.exe dans lpApplicationName et on passe /c plus le nom du batch, puis note que l’équipe d’ingénierie de MSRC ne recommande pas cela, avec un lien vers l’exposé de MS14-019.1 Ce que MS14-019 a corrigé, c’est le problème que, lorsqu’un fichier batch était passé directement à CreateProcess, cmd.exe était cherché d’abord dans le répertoire courant et pouvait être détourné, et la recommandation de MSRC est de « passer le chemin pleinement qualifié de cmd.exe et de faire du fichier batch son argument ».18 Autrement dit, le problème est de lancer un fichier batch sans nommer cmd.exe par son chemin complet (mettre lpApplicationName à NULL et laisser le nom du batch le démarrer), pas le lancement /c lui-même avec le chemin complet de cmd.exe dans lpApplicationName.
flowchart TB
accTitle: cmd.exe intercalé ajoute des étapes d'interprétation
accDescr: Lancer l'exe cible directement signifie que le découpage se fait une fois, dans l'analyseur de la cible, mais passer par cmd.exe /c ajoute l'interprétation syntaxique de cmd.exe, et un fichier batch par-dessus reçoit la chaîne brute, si bien que les règles de quotage changent à chaque étape
direct["Votre processus → l'exe cible"] --> p1["Découper une fois, seulement par l'analyseur de la cible"]
p1 ~~~ via
via["Votre processus → cmd.exe /c → l'exe cible"] --> p2["L'interprétation syntaxique de cmd.exe s'ajoute (esperluette, tube, parenthèses, circonflexe)"]
p2 --> p3["Découpage par l'analyseur de la cible"]
p3 ~~~ bat
bat["Votre processus → cmd.exe /c → un fichier batch"] --> p4["Le fichier batch reçoit la chaîne brute"]
p4 --> danger["Y faire transiter des valeurs non fiables devient une injection de commande"]
Figure 12 : Plus il y a d’étapes, plus les règles se mélangent. Lancez directement ce qui peut l’être, et ne passez jamais à un fichier batch des valeurs venues de l’extérieur.
La décision pratique est simple. Si la cible est un exe, n’intercalez pas cmd.exe. Si vous n’avez d’autre choix que d’appeler un .bat, le principe est de ne pas laisser le batch interpréter des valeurs venues de l’extérieur. Écrivez les valeurs dans un fichier, faites passer au batch seulement le chemin de ce fichier, comme chaîne fixe, à l’exe en aval, et lisez le contenu du fichier côté exe. Mettre la valeur dans une variable d’environnement n’est pas une frontière, parce que dès que le batch l’étend en %VAR%, & et | sont réinterprétés par cmd.exe. Passer par une variable d’environnement n’est acceptable que lorsque l’exe en aval lit la variable directement sans passer par le batch. Si même cela est difficile, déplacez le contenu du batch vers PowerShell ou un exe à vous (« Ce fichier bat, faut-il le migrer vers PowerShell ? »).
10. Limites de longueur
Les limites diffèrent aussi selon le chemin.
| Chemin | Limite | Source |
|---|---|---|
lpCommandLine de CreateProcess |
32 767 unités de code UTF-16 (y compris le caractère nul terminal ; une paire de substitution compte pour deux) | 1 |
La partie nom d’exécutable lorsque lpApplicationName est NULL |
MAX_PATH |
1 |
| La ligne de commande de cmd.exe (y compris les lignes dans un fichier batch) | 8 191 caractères | 8 |
ProcessStartInfo.Arguments de .NET |
Longueur de chaîne (unités de code UTF-16) inférieure à 32 699 | 16 |
Une conception qui aligne comme arguments des valeurs de longueur variable telles qu’une liste de fichiers atteint la limite le jour où le nombre croît. Pour les usages qui approchent la limite, passez à la méthode « fichier de réponse » : écrire les arguments dans un seul fichier et ne passer que le chemin de ce fichier. La solution officielle pour la limite de cmd.exe est la même méthode.8 Ni CreateProcess ni cmd.exe, toutefois, n’étendent le fichier pour vous. Cette méthode ne fonctionne que si le programme cible peut lire un fichier de réponse avec une syntaxe telle que @file, ou si vous pouvez corriger la cible pour qu’il le puisse. Si la cible est un exe du commerce que vous ne pouvez pas modifier, la seule option est de fractionner les appels pour que chacun tienne dans la limite.
flowchart TB
accTitle: Les limites du passage de valeurs de longueur variable comme arguments, et le contournement
accDescr: Aligner comme arguments des valeurs de longueur variable telles qu'une liste de fichiers atteint, à mesure que le nombre croît, la limite de 8191 caractères de cmd.exe ou la limite de 32767 unités de code UTF-16 de CreateProcess. Si la cible peut lire un fichier de réponse (ou peut être corrigée pour), passer à la méthode fichier de réponse qui écrit les valeurs dans un fichier et ne passe que le chemin ; si la cible est un exe du commerce qui ne le peut pas, fractionner les appels
list["Aligner comme arguments des valeurs de longueur variable (une liste de fichiers, etc.)"] --> grow["La chaîne s'allonge à mesure que le nombre croît"]
grow --> lim["La limite est atteinte (cmd.exe 8 191 / CreateProcess 32 767)"]
lim --> fail["Un jour le lancement échoue soudain"]
fail -.->|"La cible peut lire un fichier de réponse"| resp["Écrire les valeurs dans un fichier et ne passer que le chemin (fichier de réponse)"]
fail -.->|"Un exe du commerce qui ne le peut pas"| split["Fractionner les appels"]
Figure 13 : La limite est le genre de problème qui est « bon aujourd’hui ». Pour les arguments qui croissent proportionnellement au nombre, si la cible peut lire un fichier de réponse (ou peut être corrigée pour), faites-le dès le départ.
11. Vérifier ce qui est réellement arrivé
Avant d’ajouter du quotage par hypothèse, le chemin le plus court est de regarder les arguments qui ont atteint la cible. Il y a trois choses à regarder, « la chaîne assemblée côté appelant », « la chaîne qui a atteint le côté cible », et « le tableau après découpage », et quatre moyens de le faire. Avant cela, une promesse. Quel que soit le moyen, masquez les secrets avant d’enregistrer une ligne de commande dans un journal. Si la conception met des mots de passe, des clés d’API ou des jetons dans les arguments, les écrire tels quels laisse les secrets dans le journal, qu’il s’agisse du journal de l’appelant ou du journal de démarrage de la cible. Les journaux sont conservés plus longtemps que le processus et vus par plus de personnes. D’ailleurs une ligne de commande peut être lue par d’autres processus sur la même machine, comme avec Process Explorer décrit plus loin, aussi la contre-mesure fondamentale est une conception qui passe les mots de passe et les jetons non comme arguments mais par une autre voie telle que l’entrée standard ou un magasin de configuration protégé ; le masquage dans les journaux est une sauvegarde par-dessus. Soit interprétez les arguments découpés (côté appelant, les éléments avant assemblage) et masquez les valeurs d’options qui pourraient être secrètes avant d’enregistrer, soit n’activez l’enregistrement de la chaîne brute que dans un mode de diagnostic restreint.
- Côté appelant, journalisez la chaîne que vous avez assemblée. C’est le
lpCommandLineimmédiatement avant qu’il soit passé àCreateProcess. Cette comparaison suppose un lancement avecUseShellExecute = falseou un appel direct àCreateProcess. Lorsque vous ouvrez un document ou une URL avecUseShellExecute = true, l’association de fichiers assemble la ligne de commande réelle viaShellExecuteEx(chapitre 7), aussi la chaîne de l’appelant et celle de la cible diffèrent même sans cmd.exe ni fichier batch, et ce n’est pas le problème du chapitre 9. Si vous utilisezArgumentListde .NET, enregistrer la liste des éléments telle quelle ne peut pas servir à la comparaison. Les éléments sont les valeurs avant quotage et avant le doublement des barres obliques inverses finales, et ce qui parvient au système d’exploitation est la chaîne que .NET a formatée à partir d’eux. Soit reconstruisez une seule chaîne à partir des éléments avec les mêmes règles queBuildArgumentsau chapitre 7 et enregistrez-la (cela donne le même résultat que le formatage queArgumentListfait en interne), soit comparez la liste des éléments directement au tableau après découpage. C’est le seul moyen de voir « le tampon d’origine de l’appelant » ; Process Explorer et le journal de la cible, décrits plus bas, ne montrent que la chaîne qu’une étape intercalée de cmd.exe ou d’un batch a reconstruite. Lors de l’enregistrement, tenez la promesse de l’ouverture et masquez les valeurs d’éléments qui pourraient être secrètes (un élément masqué ne correspond plus à la chaîne de la cible, aussi excluez cet élément de la comparaison). - Préparez un exe qui n’affiche que ses arguments. Lancez-le à la place de l’exe cible et faites-lui imprimer les
argsreçus, un par ligne. Si vous écrivez les valeurs telles quelles, un argument contenant des sauts de ligne ou des caractères de contrôle peut apparaître comme plusieurs lignes ou écraser les lignes voisines et vous vous trompez de compte, aussi imprimez chaque valeur échappée comme chaîne JSON avec sa longueur (l’échappement est réversible, aussi la valeur d’origine peut être recouvrée). Souvenez-vous toutefois que, comme l’explique le chapitre 3, il y a trois lignées d’analyseurs, et elles interprètent différemment des formes de bord telles que deux guillemets consécutifs à l’intérieur du quotage. Utilisez un exe d’affichage construit avec le même runtime que la cible (C++ avecwmainsi la cible est du C/C++ MSVC, .NET si c’est .NET). Si la cible est votre propre programme, l’approche la plus fiable est de sauter l’exe d’affichage et de journaliserargvau démarrage de la cible elle-même (sous la règle de masquage du point suivant). Pour .NET, les quelques lignes suivantes suffisent.
using System.Text.Encodings.Web;
using System.Text.Json;
// Échapper les sauts de ligne, caractères de contrôle, guillemets et barres obliques inverses ; émettre le texte japonais tel quel
var json = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
Console.WriteLine("CommandLine: " + JsonSerializer.Serialize(Environment.CommandLine, json)); // la chaîne unique
for (int i = 0; i < args.Length; i++)
Console.WriteLine($"[{i}] len={args[i].Length} {JsonSerializer.Serialize(args[i], json)}");
// Après découpage. Chaque entrée tient toujours sur une ligne, et une chaîne vide s'affiche comme len=0 et "". len est en unités de code UTF-16
- Regardez la ligne de commande du processus enfant dans Process Explorer. Les propriétés du processus montrent la chaîne de ligne de commande que le processus enfant détient. C’est un moyen de vérifier « la chaîne qui a atteint le côté cible » ; cela ne vous dit pas « le tableau après découpage ». Ce qui est montré est la chaîne détenue côté processus enfant, aussi, comme abordé au chapitre 2, le système d’exploitation peut avoir complété le nom de l’exécutable en tête par le chemin complet, et si cmd.exe ou un fichier batch est intercalé, ce que vous voyez est la chaîne reconstruite par cmd.exe. Les points clés sont de ne pas s’alarmer d’une différence sur le seul jeton de tête, et que la chaîne d’origine de l’appelant ne peut être connue que d’après le journal du point 1. L’usage est traité dans « Process Explorer / Handle / VMMap en pratique ».
- Au démarrage de votre propre application, journalisez la ligne de commande qu’elle a reçue. Lorsque quelqu’un sur le terrain dit « ça ne démarre pas », avoir un enregistrement de la chaîne avec laquelle elle a été lancée permet d’isoler d’abord s’il s’agit d’un problème d’arguments. Ici aussi, ne sauvegardez pas telle quelle la valeur de retour de
GetCommandLineW. Tenez la promesse de l’ouverture : soit interprétez les arguments découpés et masquez les valeurs qui pourraient être secrètes avant d’enregistrer, soit n’activez l’enregistrement de la chaîne brute que dans un mode de diagnostic restreint.
L’ordre de comparaison est le suivant. Comparez d’abord la chaîne de l’appelant (point 1) avec la chaîne de la cible (point 3 ou 4). Si elles ne correspondent pas hormis le nom de l’exécutable en tête, une étape intercalée l’a transformée : cmd.exe ou un fichier batch pour un lancement direct (chapitre 9), ou l’association de fichiers du shell pour UseShellExecute = true (chapitre 7). Remplacer votre code par la fonction du chapitre 6 ne corrigera pas cela. Si elles correspondent, comparez cette chaîne avec le tableau après découpage (point 2). Si elle est découpée selon les règles mais n’est pas le tableau voulu, le problème est côté assemblage ; si elle n’est pas découpée selon les règles, le problème est l’analyseur du destinataire.
flowchart TB
accTitle: L'ordre pour isoler un problème d'arguments
accDescr: Comparer d'abord le journal de la chaîne assemblée côté appelant avec la chaîne de la cible vue dans Process Explorer ou le journal de démarrage de la cible. Si elles ne correspondent pas hormis le nom de l'exécutable en tête, une étape intercalée (cmd.exe ou un fichier batch pour un lancement direct, l'association de fichiers du shell pour UseShellExecute=true) l'a transformée. Si elles correspondent, comparer avec le tableau après découpage ; s'il est découpé selon les règles mais n'est pas le tableau voulu, le problème est côté assemblage, et s'il n'est pas découpé selon les règles, le problème est l'analyseur du destinataire
s["Les arguments sont incorrects"] --> caller["Regarder la chaîne assemblée côté appelant (journal de l'appelant)"]
caller --> target["Regarder la chaîne de la cible (Process Explorer / journal de démarrage de la cible)"]
target --> same{"Correspondent-elles hormis le nom de l'exécutable en tête ?"}
same -->|"Non"| mid["Une étape intercalée l'a transformée (voir les chapitres 9 et 7)"]
same -->|"Oui"| arr["Regarder le tableau après découpage (un exe d'affichage sur le même runtime que la cible)"]
arr --> cmp{"La chaîne et le tableau correspondent-ils selon les règles ?"}
cmp -->|"Oui : découpé, mais pas le tableau voulu"| build["Un problème côté assemblage : remplacer par la fonction du chapitre 6"]
cmp -->|"Non : pas découpé selon les règles"| recv["Un problème dans l'analyseur du destinataire"]
Figure 14 : Comparez dans l’ordre les trois choses « la chaîne de l’appelant », « la chaîne de la cible » et « le tableau », et il se décide mécaniquement si la responsabilité est à une étape intercalée, côté assemblage ou côté destinataire. Ajouter des échappements par hypothèse peut attendre après cette vérification.
12. Un guide approximatif (table de décision)
| Situation | Que faire |
|---|---|
| Lancer un exe depuis .NET Core 2.1 ou ultérieur / .NET 5 ou ultérieur | Ajouter à ProcessStartInfo.ArgumentList un élément à la fois |
| Lancer un exe depuis .NET Framework | Assembler Arguments avec une fonction suivant les règles du chapitre 6. Ne jamais écrire les guillemets à la main |
| Lancer depuis C++ | Passer lpApplicationName, et assembler lpCommandLine selon les règles dans un tampon inscriptible |
| Vous voulez un guillemet dans une valeur d’argument | N’utiliser que la forme \". Ne jamais placer de guillemets adjacents à l’intérieur d’un argument non vide |
| Le chemin se termine par une barre oblique inverse | Si vous entourez, doublez la barre oblique inverse finale. S’il n’y a pas d’espaces, ne l’entourez pas |
| Vous voulez passer un argument vide | Mettre "". Si vous l’omettez, l’argument entier disparaît |
| Le chemin de l’exécutable contient un espace | Passer lpApplicationName, et entourer aussi de guillemets le jeton de tête |
Vous n’avez d’autre choix que d’appeler un .bat |
Ne pas laisser le batch interpréter des valeurs venues de l’extérieur. Les écrire dans un fichier et les faire lire par l’exe en aval (une variable d’environnement étendue en %VAR% dans le batch n’est pas une frontière) |
| Les arguments s’allongent | Si la cible peut lire un fichier de réponse (ou peut être corrigée pour), passer à un fichier de réponse. Pour un exe du commerce, fractionner les appels |
| Vous ne savez pas ce qui arrive | Comparer les trois dans l’ordre : le journal de l’appelant, la chaîne de la cible (Process Explorer / journal de démarrage), et le tableau après découpage (un exe d’affichage sur le même runtime que la cible) |
13. Résumé
Les arguments de ligne de commande Windows franchissent la frontière non comme un tableau mais comme une seule chaîne. L’appelant concatène, le destinataire découpe, et les règles de découpage se ramènent à trois : « découper sur les espaces », « entourer de guillemets » et « seules les barres obliques inverses immédiatement avant un guillemet sont particulières ». Seul le nom de l’exécutable en tête suit une autre règle, et omettre lpApplicationName rend ambiguë l’interprétation d’un chemin contenant des espaces.
Ce que le côté assemblage a à faire tient dans une fonction, et sous .NET Core 2.1 ou ultérieur ArgumentList s’en charge. Pour l’exécutable, passez le chemin complet dans lpApplicationName et placez aussi le même chemin, entouré de guillemets, au début de lpCommandLine (en .NET, laissez faire FileName). Ne générez jamais la forme avec des guillemets adjacents à l’intérieur d’un argument non vide (le "" qui représente un argument vide est autre chose), ne faites jamais transiter par cmd.exe ou un fichier batch des valeurs venues de l’extérieur, et pour les arguments qui croissent proportionnellement au nombre, n’utilisez un fichier de réponse que lorsque la cible peut en lire un (ou peut être corrigée pour), sinon fractionnez les appels. Tenez ces cinq points et les échecs « ça ne démarre que sur un PC avec un espace dans le chemin » et « l’argument suivant disparaît à cause d’une barre oblique inverse finale » n’arrivent jamais.
flowchart TB
accTitle: Cinq promesses qui empêchent les échecs d'arguments
accDescr: Passer le chemin complet de l'exécutable dans lpApplicationName et entourer aussi de guillemets le jeton de tête, laisser le quotage à une fonction qui suit les règles ou à ArgumentList, ne jamais générer la forme avec des guillemets adjacents à l'intérieur d'un argument non vide, ne jamais faire transiter par cmd.exe ou un fichier batch des valeurs de l'extérieur, et n'utiliser un fichier de réponse pour les arguments qui croissent avec le nombre que lorsque la cible peut en lire un. En supposant que la cible interprète selon les règles de découpage publiées et n'a pas activé l'expansion des jokers, ces cinq points empêchent les échecs causés par les chemins avec espaces et les barres obliques inverses finales
r0["Passer le chemin complet dans lpApplicationName et quoter aussi le jeton de tête"]
r1["Laisser le quotage à une fonction conforme aux règles ou à ArgumentList"]
r2["Ne jamais générer de guillemets adjacents à l'intérieur du quotage"]
r3["Ne jamais faire transiter par cmd.exe ou un fichier batch des valeurs de l'extérieur"]
r4["Utiliser un fichier de réponse pour les arguments qui croissent (lorsque la cible peut en lire un)"]
goal["Pas d'échecs dus aux espaces ou aux barres obliques inverses finales"]
r0 ~~~ r1 ~~~ r2 ~~~ r3 ~~~ r4
r0 --> goal
r1 --> goal
r2 --> goal
r3 --> goal
r4 --> goal
Figure 15 : Chacune des cinq promesses est une reformulation de « fixer le module à exécuter, et ne passer que des chaînes que l’analyseur de la cible peut découper ». La prémisse est que la cible interprète selon les règles de découpage publiées et n’a pas activé l’expansion des jokers (chapitres 6 et 8) ; par-dessus, ces cinq points empêchent les échecs causés par les espaces et les barres obliques inverses finales.
Lorsque ça ne marche pas, avant d’ajouter des échappements par hypothèse, regardez les trois choses : la chaîne assemblée côté appelant, la chaîne qui a atteint le côté cible, et le tableau après découpage. Si les chaînes de l’appelant et de la cible diffèrent, une étape intercalée est responsable (cmd.exe ou un fichier batch, ou l’association de fichiers du shell pour UseShellExecute = true) ; si elles sont les mêmes, la correspondance entre la chaîne et le tableau décide si c’est le côté assemblage ou le côté destinataire.
Articles connexes
- Bien appeler un exe externe depuis PowerShell — les pièges des guillemets d’arguments, du code de sortie et de l’encodage
- Checklist pour gérer les processus enfants en toute sécurité dans une application Windows — bonnes pratiques pour les Job Objects, la propagation de la fin, les E/S standard et les watchdogs
- Ce qui reste après la mort du parent — garder les processus enfants dans un Job Object
- MAX_PATH et les pièges des chemins et noms de fichiers Windows — la limite de 260 caractères, les noms réservés, les points de fin et la casse
- L’intégration au shell Windows aujourd’hui — menus contextuels, associations de fichiers et ce qui a changé sous Windows 11
- Empêcher les lancements multiples d’une application Windows — Mutex nommé et activation de la fenêtre existante lors d’un second lancement
- Appeler les API Win32 en toute sécurité depuis C# — Guide pratique de P/Invoke (DllImport / LibraryImport / CsWin32)
- Ce fichier bat, faut-il le migrer vers PowerShell ? — Inventaire des actifs cmd/bat et critères de décision
Domaines de conseil associés
KomuraSoft LLC prend en charge la conception d’applications Windows qui combinent des outils externes et des EXE internes, l’investigation des causes de lancements de processus enfants qui « démarrent dans certains environnements et pas dans d’autres », et la revue du code de lancement de processus dans le cadre d’une migration de .NET Framework vers .NET. N’hésitez pas à nous contacter même pour un seul cas de « les arguments se déforment ».
- Développement d’applications Windows
- Analyse de dysfonctionnements et des causes racines
- Valorisation et migration d’actifs existants
- Conseil technique et revue de conception
- Contact
Références
-
Microsoft Learn, CreateProcessW function (processthreadsapi.h). Sur le fait que
lpCommandLineest une seule chaîne d’au plus 32 767 caractères (y compris le caractère nul terminal ; unités de code UTF-16, puisqu’il s’agit d’une chaîne large), que la version Unicode peut modifier son contenu de sorte que de la mémoire en lecture seule ne peut pas être passée, que le jeton de tête délimité par des espaces devient le nom du module lorsquelpApplicationNameestNULLavec un chemin contenant des espaces interprété à partir dec:\program.exe, le danger qu’un autre exécutable s’exécute siProgram.exeest placé là et la nécessité d’éviterNULLou d’entourer le chemin de guillemets, le fait queargv[0]peut ne pas correspondre au nom du module lorsque les deux sont spécifiés, que la partie nom de module est limitée àMAX_PATHlorsqueNULL, et que cmd.exe /c est requis pour lancer un fichier batch. Voir aussi la note dans CreateProcessA function indiquant que l’équipe d’ingénierie de MSRC ne recommande pas cette méthode (avec un lien vers l’exposé de MS14-019). ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 -
Microsoft Learn, GetCommandLineW function (processenv.h). Sur le fait qu’elle renvoie la chaîne de ligne de commande du processus courant, que la valeur de retour ne doit pas être libérée ni modifiée, qu’elle est convertible en forme argv via
CommandLineToArgvW, et qu’elle peut ne pas correspondre à la chaîne que le parent a passée àCreateProcessparce que le système d’exploitation complète le chemin complet du nom de l’exécutable. ↩ ↩2 -
Microsoft Learn, CommandLineToArgvW function (shellapi.h). Sur le traitement particulier des barres obliques inverses immédiatement avant un guillemet double (2n donne n plus l’ouverture ou la fermeture du quotage, 2n+1 donne n plus un guillemet littéral, et elles restent telles quelles lorsqu’aucun guillemet ne suit), le fait que les espaces deviennent partie de l’argument en mode « entre guillemets », que le nom de programme de tête est autorisé avec ou sans guillemets, que le premier argument devient une chaîne vide lorsque
lpCmdLinecommence par un espace, que le chemin de l’exécutable courant est renvoyé lorsqu’une chaîne vide est passée, et que la valeur de retour est libérée avec un seulLocalFree. ↩ ↩2 ↩3 ↩4 ↩5 -
Microsoft Learn,
mainfunction and command-line arguments. Sur les règles selon lesquelles le code de démarrage Microsoft C/C++ interprète la ligne de commande (séparation par espaces et tabulations,argv[0]quotable mais non soumis aux règles suivantes, une chaîne quotée étant un argument, l’accent circonflexe n’étant pas un caractère d’échappement, deux guillemets consécutifs à l’intérieur des guillemets étant un guillemet, tout jusqu’à la fin étant le dernier argument lorsqu’il n’y a pas de guillemet fermant, et le traitement des nombres pairs et impairs de barres obliques inverses), le tableau des entrées et deargv, l’expansion des jokers avecsetargv.obj, et le fait queargv[0]peut ne pas être le nom de l’exécutable lorsquelpApplicationNameetlpCommandLinesont tous deux spécifiés, de sorte qu’il convient de l’obtenir avecGetModuleFileName. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, ProcessStartInfo.ArgumentList Property. Sur le fait que les chaînes ajoutées n’ont pas besoin d’échappement à l’avance, que
ArgumentListetArgumentssont indépendantes et inutilisables en même temps, queArgumentListéchappe les arguments et assemble en interne une seule chaîne passée au système d’exploitation àProcess.Start, queArgumentListest le choix si vous n’êtes pas sûr du quotage, le danger de le combiner avec des données non fiables, et le fait qu’il s’applique à .NET Core 2.1 et versions ultérieures. ↩ ↩2 ↩3 ↩4 -
dotnet/runtime (GitHub), PasteArguments.cs et PasteArguments.Windows.cs. Le code d’assemblage utilisé à l’intérieur de
ArgumentList. Sur le fait de placer tel quel un argument non vide sans espaces ni guillemets, sinon de l’entourer de guillemets, de doubler les barres obliques inverses finales, de rendre les barres obliques inverses avant un guillemet doublées plus un, de toujours préfixer les guillemets d’une barre oblique inverse, de ne pas générer la forme d’un guillemet suivant un guillemet fermant parce que VC avant et après 2008 l’interprètent différemment, et pourargv[0]de seulement l’entourer de guillemets s’il a des espaces et de lever une exception s’il contient un guillemet. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Microsoft Learn, about_Parsing. Sur le fait que les arguments d’un fichier batch sont passés à cmd.exe comme chaîne brute de ligne de commande, et l’avertissement contre le passage d’entrées non fiables. ↩ ↩2
-
Microsoft Learn, Command prompt (Cmd.exe) command-line string limitation. Sur le fait que la longueur maximale d’une chaîne utilisable à l’invite de commandes est de 8 191 caractères, que cela s’applique aussi aux lignes de commande à l’intérieur des fichiers batch, et la solution de contournement consistant à écrire les arguments dans un fichier et à passer ce nom de fichier. ↩ ↩2 ↩3
-
Microsoft Learn, WinMain function (winbase.h). Sur le fait que
lpCmdLineest la ligne de commande sans le nom du programme, que la ligne de commande entière s’obtient avecGetCommandLine, et quewWinMainexiste comme point d’entrée Unicode. ↩ -
dotnet/runtime (GitHub), apphost.c et dotnet.cpp. Sur le fait que les points d’entrée de l’apphost et de
dotnet.exesontwmain(int argc, wchar_t* argv[])sous Windows et passent l’argvconstruit par le runtime C directement au traitement de démarrage de l’hôte. ↩ -
dotnet/runtime (GitHub), corhost.cpp. Sur le fait que
ExecuteAssemblyconstruit le tableau deEnvironment.GetCommandLineArgs()avecSetCommandLineArgs(pwzAssemblyPath, argc, argv), que le premier élément est le nom de lancement passé par l’hôte (ou le chemin de l’assembly s’il n’y en a pas) suivi deargv, et que seul cetargvest passé àMain. ↩ ↩2 -
dotnet/runtime (GitHub), Environment.cs et Environment.Windows.cs. Sur le fait que
GetCommandLineArgsrenvoie le tableau initialisé au démarrage (s_commandLineArgs), qu’une bibliothèque hébergée sans lui se rabat sur le découpage de la valeur de retour deGetCommandLineWavecSegmentCommandLinepropre au runtime, que ces règles suivent la documentation de la fonctionmainde MSVC, et queCommandLineToArgvWn’est pas utilisé parce que son comportement diffère légèrement. ↩ ↩2 ↩3 -
Microsoft Learn, Main() and command-line arguments. Sur le fait que
argsdansMainn’est jamais null et que, contrairement au C/C++, le nom du programme n’est pas inclus au début deargsmais est le premier élément deGetCommandLineArgs(). ↩ -
Microsoft Learn, dotnet command. Sur le fait que l’exécution d’une application prend la forme
dotnet [runtime options] <app path> [arguments], où tout ce qui suit le chemin de l’application sont les arguments passés à l’application. ↩ -
Microsoft Learn, Environment.GetCommandLineArgs Method. Sur le fait que le premier élément est le nom de l’exécutable, que les arguments sont séparés par des espaces avec des guillemets doubles permettant des espaces à l’intérieur, que les guillemets simples n’ont pas cette fonction, les règles pour les nombres pairs et impairs de barres obliques inverses et de guillemets, et le tableau des entrées et des résultats. ↩
-
Microsoft Learn, ProcessStartInfo.Arguments Property. Sur le fait que la longueur de chaîne est inférieure à 32 699, que les arguments sont interprétés par l’application cible et doivent donc correspondre à ses attentes, que les guillemets eux-mêmes ne sont pas passés à la cible lorsqu’un argument contenant des espaces est quoté, et son indépendance vis-à-vis de
ArgumentList. ↩ ↩2 -
Microsoft Learn, cmd. Sur le fait que
&,|et( )sont des caractères spéciaux qui exigent^ou des guillemets, la liste des caractères spéciaux qui devraient être entourés de guillemets, les conditions sous lesquelles les guillemets sont conservés avec/cou/k(pas de/s, exactement une paire de guillemets, pas de caractères spéciaux, contenant des espaces, et étant un nom d’exécutable), et comment le guillemet de tête est retiré lorsque les conditions ne sont pas remplies. ↩ -
Microsoft Security Response Center, MS14-019 – Fixing a binary hijacking via .cmd or .bat file et Microsoft Security Bulletin MS14-019. Sur le fait que
CreateProcesscherchait cmd.exe d’abord dans le répertoire courant lorsqu’un .cmd / .bat était passé directement, ce qui permettait un détournement, que la correction utilise toujours le cmd.exe du système, et la recommandation que les applications passent le chemin pleinement qualifié de cmd.exe avec le fichier batch comme argument. ↩
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Ce qui reste après la mort du parent — garder les processus enfants dans un Job Object
Pourquoi les aides du SDK survivent à une IU tuée et gardent la caméra ou le port COM. Concevoir la durée de vie des processus enfants av...
Tubes nommés en pratique — l'IPC standard de Windows, de la conception à la sécurité
Guide pratique des tubes nommés, mécanisme standard de communication inter-processus sous Windows. Cet article organise, à partir des sou...
Checklist pour gérer les processus enfants en toute sécurité dans une application Windows
Pour gérer en toute sécurité les processus enfants dans une application Windows, l'important n'est pas l'API de lancement mais la concept...
Time Travel Debugging — Enregistrer et rembobiner les bogues qui ne se reproduisent pas dans les applications de longue durée
Un bogue qui n'apparaît qu'une fois par mois ne laisse dans un dump de plantage que le résultat. Enregistrez et rembobinez l'exécution av...
Fin de la maintenance des pilotes d'imprimante Windows — Comment les applications métier doivent préparer l'impression des rapports et des étiquettes
Microsoft met progressivement fin aux pilotes d'imprimante v3/v4. Ce que Windows protected print mode retire, et comment inventorier et p...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Applications métier, intégration d'équipements et outils de communication, des besoins au développement.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- N'existe-t-il pas d'API Windows qui passe un tableau d'arguments ?
- Non. Ce que CreateProcess reçoit est une seule chaîne appelée lpCommandLine, et c'est cette chaîne qui parvient au nouveau processus (le système d'exploitation peut seulement compléter le nom de l'exécutable en tête par le chemin complet). Ce qui ressemble à un tableau argv est créé dans le processus destinataire par le code de démarrage du runtime C, CommandLineToArgvW ou le runtime .NET qui découpe la chaîne. Passer des arguments, c'est donc la même chose qu'assembler une chaîne que l'analyseur de l'autre côté recouppe en morceaux d'origine.
- Quand une barre oblique inverse devient-elle un caractère d'échappement ?
- Seulement lorsqu'un guillemet double la suit immédiatement. Une barre oblique inverse qui n'est pas suivie d'un guillemet double reste telle quelle, quel que soit le nombre de répétitions. Si 2n barres obliques inverses précèdent un guillemet double, elles deviennent n barres obliques inverses et le guillemet ouvre ou ferme le quotage ; si 2n+1 le précèdent, elles deviennent n barres obliques inverses et un guillemet littéral. À cause de cette asymétrie, il ne faut doubler une barre oblique inverse finale dans un chemin que lorsque l'on entoure le chemin de guillemets.
- Faut-il utiliser ProcessStartInfo.ArgumentList ou Arguments ?
- Si les valeurs viennent de variables, ArgumentList. Un élément devient un argument, .NET applique les guillemets et l'échappement nécessaires, et il assemble en interne une seule chaîne avant de la remettre au système d'exploitation. Arguments est une propriété qui passe telle quelle une chaîne que vous avez assemblée vous-même ; les deux sont indépendantes et ne peuvent pas être utilisées en même temps. Notez qu'ArgumentList est une API introduite dans .NET Core 2.1 et n'existe pas sous .NET Framework. Sous .NET Framework, assemblez Arguments avec la fonction d'assemblage de cet article.
- Peut-on écrire deux guillemets adjacents à l'intérieur d'un argument quoté ?
- Ne la générez pas côté assemblage, parce que les destinataires l'interprètent différemment. Il s'agit ici d'entourer de guillemets un argument non vide et d'y placer deux guillemets adjacents. Le "" qui représente un argument vide (seulement deux guillemets) est autre chose, et c'est la façon correcte de passer une chaîne vide. Selon les règles du runtime C de MSVC, deux guillemets consécutifs à l'intérieur d'une chaîne quotée sont traités comme un seul guillemet, mais les règles officielles de CommandLineToArgvW ne décrivent pas ce traitement, et le code source du runtime .NET dit explicitement qu'il ne génère pas cette forme parce que VC avant et après 2008 l'interprètent différemment. Lorsque vous voulez passer un guillemet comme caractère, placez une barre oblique inverse devant, et chaque analyseur donne le même résultat.
- Lorsque le chemin de l'exécutable contient un espace, que faut-il passer à CreateProcess pour être en sécurité ?
- La méthode fiable est de passer le chemin complet de l'exécutable dans lpApplicationName et de placer aussi le même chemin, entouré de guillemets, au début de lpCommandLine. Si lpApplicationName est NULL, CreateProcess devine le nom de l'exécutable à partir du début de lpCommandLine, en découpant sur les espaces. Pour la chaîne C:\Program Files\MyApp -L -S, il teste d'abord si C:\Program.exe existe, si bien qu'un fichier malveillant à cet emplacement s'exécute. La documentation officielle énonce explicitement ce danger et demande d'éviter NULL ou d'entourer le chemin de guillemets.
- Les mêmes règles s'appliquent-elles lorsqu'on passe des arguments à un fichier batch ?
- Non. Un fichier batch est interprété par cmd.exe, et cmd.exe traite la ligne de commande comme une chaîne brute sans la découper en arguments. Des symboles tels que &, |, les parenthèses et ^ agissent comme syntaxe de cmd.exe, si bien que le quotage selon les règles de CommandLineToArgvW ne les rend pas sûrs. La documentation officielle met en garde contre le passage d'entrées non fiables à des fichiers batch. Écrivez les valeurs dans un fichier et faites-les lire par l'exe en aval, pas par le batch, ou déplacez le contenu du batch vers PowerShell ou un exe à vous. Mettre la valeur dans une variable d'environnement n'est pas non plus une frontière, parce que dès que le batch l'étend en %VAR%, cmd.exe réinterprète les symboles.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.