1+ "use strict" ;
12/**
23 * ESLint rule to detect unsafe child_process spawn/spawnSync calls
34 * that use a single string argument instead of separate command and args array.
45 *
56 * This prevents shell injection vulnerabilities and ensures proper argument handling.
67 */
78
8- import type { Rule } from "eslint" ;
9-
10- // Extend RuleContext to include report method which exists at runtime
11- interface ExtendedRuleContext extends Rule . RuleContext {
12- report ( options : { node : Rule . Node ; messageId : string } ) : void ;
13- }
14-
15- // Extend RuleModule to require meta (it's optional in the base type)
16- interface ExtendedRuleModule extends Rule . RuleModule {
17- meta : {
18- type : "problem" ;
19- docs : {
20- description : string ;
21- category : string ;
22- recommended : boolean ;
23- } ;
24- fixable : undefined ;
25- schema : [ ] ;
26- messages : {
27- unsafeSpawn : string ;
28- unsafeSpawnSync : string ;
29- } ;
30- } ;
31- create ( context : ExtendedRuleContext ) : Rule . RuleListener ;
32- }
33-
34- const rule : ExtendedRuleModule = {
35- meta : {
36- type : "problem" ,
37- docs : {
38- description :
39- "disallow child_process.spawn/spawnSync with single string argument containing spaces" ,
40- category : "Security" ,
41- recommended : true ,
42- } ,
43- fixable : undefined ,
44- schema : [ ] ,
45- messages : {
46- unsafeSpawn :
47- "Use spawn(command, args[]) instead of spawn(commandString). Split the command string into command and args array to prevent shell injection." ,
48- unsafeSpawnSync :
49- "Use spawnSync(command, args[]) instead of spawnSync(commandString). Split the command string into command and args array to prevent shell injection." ,
50- } ,
51- } ,
52- create ( context : ExtendedRuleContext ) : Rule . RuleListener {
9+ const rule = {
10+ create ( context ) {
5311 /**
5412 * Check if a call expression is spawn or spawnSync
5513 */
56- function isSpawnCall (
57- node : Rule . Node ,
58- ) : node is Rule . Node & { type : "CallExpression" } {
14+ function isSpawnCall ( node ) {
5915 if ( node . type !== "CallExpression" || ! ( "callee" in node ) ) {
6016 return false ;
6117 }
62-
6318 const callee = node . callee ;
64-
6519 // Check for: spawnSync(...) or spawn(...) as identifier
6620 if ( callee . type === "Identifier" ) {
6721 return callee . name === "spawnSync" || callee . name === "spawn" ;
6822 }
69-
7023 // Check for: child_process.spawnSync(...) or cp.spawnSync(...)
7124 if (
7225 callee . type === "MemberExpression" &&
@@ -77,31 +30,24 @@ const rule: ExtendedRuleModule = {
7730 const methodName = callee . property . name ;
7831 return methodName === "spawnSync" || methodName === "spawn" ;
7932 }
80-
8133 return false ;
8234 }
83-
8435 /**
8536 * Check if the call is unsafe (command string instead of command + args array)
8637 */
87- function isUnsafeCall (
88- node : Rule . Node & { type : "CallExpression" } ,
89- ) : boolean {
38+ function isUnsafeCall ( node ) {
9039 const args = node . arguments ;
9140 if ( ! args || args . length === 0 ) {
9241 return false ;
9342 }
94-
9543 const firstArg = args [ 0 ] ;
9644 if ( ! firstArg ) {
9745 return false ;
9846 }
99-
10047 // If there's a second argument that's an array, it's safe (proper usage)
10148 if ( args . length > 1 && args [ 1 ] . type === "ArrayExpression" ) {
10249 return false ;
10350 }
104-
10551 // Check if second argument is an options object with shell: true
10652 // This is unsafe because it goes through shell interpretation
10753 if (
@@ -125,14 +71,12 @@ const rule: ExtendedRuleModule = {
12571 }
12672 }
12773 }
128-
12974 // Check if first argument is a string literal with spaces
13075 if ( firstArg . type === "Literal" && typeof firstArg . value === "string" ) {
13176 const value = firstArg . value . trim ( ) ;
13277 // Flag if it contains spaces (command + args) but allow single commands
13378 return value . includes ( " " ) && value . length > 0 ;
13479 }
135-
13680 // Check if it's a template literal
13781 if ( firstArg . type === "TemplateLiteral" ) {
13882 const quasis = firstArg . quasis || [ ] ;
@@ -144,7 +88,6 @@ const rule: ExtendedRuleModule = {
14488 }
14589 }
14690 }
147-
14891 // Check if first argument is a variable and second is options (not array)
14992 // This is potentially unsafe - we can't know the variable's value statically,
15093 // but if it's not followed by an array, it's likely a command string
@@ -156,16 +99,13 @@ const rule: ExtendedRuleModule = {
15699 // Variable with options object (not array) - likely unsafe
157100 return true ;
158101 }
159-
160102 return false ;
161103 }
162-
163104 return {
164- CallExpression ( node : Rule . Node ) {
105+ CallExpression ( node ) {
165106 if ( ! isSpawnCall ( node ) ) {
166107 return ;
167108 }
168-
169109 // Check if it's an unsafe call
170110 if ( isUnsafeCall ( node ) ) {
171111 const methodName =
@@ -177,19 +117,34 @@ const rule: ExtendedRuleModule = {
177117 node . callee . property . type === "Identifier"
178118 ? node . callee . property . name
179119 : "spawn" ;
180-
181120 context . report ( {
182- node,
183121 messageId :
184122 methodName === "spawnSync" ? "unsafeSpawnSync" : "unsafeSpawn" ,
123+ node,
185124 } ) ;
186125 }
187126 } ,
188127 } ;
189128 } ,
129+ meta : {
130+ docs : {
131+ category : "Security" ,
132+ description :
133+ "disallow child_process.spawn/spawnSync with single string argument containing spaces" ,
134+ recommended : true ,
135+ } ,
136+ fixable : undefined ,
137+ messages : {
138+ unsafeSpawn :
139+ "Use spawn(command, args[]) instead of spawn(commandString). Split the command string into command and args array to prevent shell injection." ,
140+ unsafeSpawnSync :
141+ "Use spawnSync(command, args[]) instead of spawnSync(commandString). Split the command string into command and args array to prevent shell injection." ,
142+ } ,
143+ schema : [ ] ,
144+ type : "problem" ,
145+ } ,
190146} ;
191-
192- export default {
147+ module . exports = {
193148 rules : {
194149 "no-unsafe-spawn" : rule ,
195150 } ,
0 commit comments