1- import { execSync } from 'node:child_process' ;
1+ import { execFileSync } from 'node:child_process' ;
22import { existsSync , mkdirSync , writeFileSync , cpSync , rmSync , readFileSync } from 'node:fs' ;
33import { basename , join , resolve } from 'node:path' ;
44import chalk from 'chalk' ;
@@ -16,6 +16,28 @@ function modulesDir(): string {
1616 return PATHS . moduleDir ;
1717}
1818
19+ function safeModuleDirName ( name : string , label = 'module name' ) : string {
20+ if ( ! / ^ [ A - Z a - z 0 - 9 . _ @ - ] + $ / . test ( name ) || name === '.' || name === '..' ) {
21+ throw new Error ( `Unsafe ${ label } : ${ name } ` ) ;
22+ }
23+ return name ;
24+ }
25+
26+ function moduleDestination ( dir : string , name : string , label ?: string ) : string {
27+ return join ( dir , safeModuleDirName ( name , label ) ) ;
28+ }
29+
30+ function assertSafeTarballEntries ( tarPath : string ) : void {
31+ const listing = execFileSync ( 'tar' , [ '-tzf' , tarPath ] , { encoding : 'utf-8' } ) ;
32+ for ( const entry of listing . split ( '\n' ) . map ( ( line ) => line . trim ( ) ) . filter ( Boolean ) ) {
33+ const normalized = entry . replace ( / \\ / g, '/' ) ;
34+ const segments = normalized . split ( '/' ) ;
35+ if ( normalized . startsWith ( '/' ) || segments . includes ( '..' ) ) {
36+ throw new Error ( `Unsafe tarball entry: ${ entry } ` ) ;
37+ }
38+ }
39+ }
40+
1941function validateManifest ( modPath : string ) : { ok : boolean ; error ?: string ; name ?: string ; version ?: string } {
2042 const manifestPath = join ( modPath , 'mod.toml' ) ;
2143 if ( ! existsSync ( manifestPath ) ) {
@@ -105,7 +127,13 @@ export async function modulesInstallCommand(source: string): Promise<void> {
105127 console . log ( chalk . red ( ` ✗ ${ check . error } \n` ) ) ;
106128 return ;
107129 }
108- const dest = join ( dir , check . name ! ) ;
130+ let dest : string ;
131+ try {
132+ dest = moduleDestination ( dir , check . name ! ) ;
133+ } catch ( err ) {
134+ console . log ( chalk . red ( ` x ${ ( err as Error ) . message } \n` ) ) ;
135+ return ;
136+ }
109137 if ( existsSync ( dest ) ) {
110138 console . log ( chalk . yellow ( ` ! ${ check . name } is already installed at ${ dest } ` ) ) ;
111139 console . log ( chalk . dim ( ` Run ${ chalk . white ( `threatcrush modules remove ${ check . name } ` ) } first.\n` ) ) ;
@@ -130,16 +158,23 @@ export async function modulesInstallCommand(source: string): Promise<void> {
130158 ? `https://github.com/${ source . slice ( 'github:' . length ) } .git`
131159 : source ;
132160
133- const name = basename ( gitUrl . replace ( / \. g i t $ / , '' ) ) ;
134- const dest = join ( dir , name ) ;
161+ let name : string ;
162+ let dest : string ;
163+ try {
164+ name = safeModuleDirName ( basename ( gitUrl . replace ( / \. g i t $ / , '' ) ) , 'repository name' ) ;
165+ dest = moduleDestination ( dir , name ) ;
166+ } catch ( err ) {
167+ console . log ( chalk . red ( ` x ${ ( err as Error ) . message } \n` ) ) ;
168+ return ;
169+ }
135170 if ( existsSync ( dest ) ) {
136171 console . log ( chalk . yellow ( ` ! ${ name } is already installed at ${ dest } ` ) ) ;
137172 console . log ( chalk . dim ( ` Run ${ chalk . white ( `threatcrush modules remove ${ name } ` ) } first.\n` ) ) ;
138173 return ;
139174 }
140175 const spinner = ora ( { text : `Cloning ${ gitUrl } ...` , color : 'green' } ) . start ( ) ;
141176 try {
142- execSync ( ` git clone --depth 1 ${ gitUrl } ${ dest } ` , { stdio : 'pipe' } ) ;
177+ execFileSync ( ' git' , [ ' clone' , ' --depth' , '1' , '--' , gitUrl , dest ] , { stdio : 'pipe' } ) ;
143178 } catch ( err ) {
144179 spinner . fail ( `Clone failed: ${ ( err as Error ) . message } ` ) ;
145180 return ;
@@ -201,7 +236,13 @@ export async function modulesInstallCommand(source: string): Promise<void> {
201236 spinner . succeed ( `Found ${ mod . name } v${ mod . version } ` ) ;
202237
203238 const install = mod . install ;
204- const dest = join ( dir , mod . slug ) ;
239+ let dest : string ;
240+ try {
241+ dest = moduleDestination ( dir , mod . slug , 'module slug' ) ;
242+ } catch ( err ) {
243+ console . log ( chalk . red ( ` x ${ ( err as Error ) . message } \n` ) ) ;
244+ return ;
245+ }
205246 if ( existsSync ( dest ) ) {
206247 console . log ( chalk . yellow ( ` ! ${ mod . slug } is already installed at ${ dest } \n` ) ) ;
207248 return ;
@@ -210,7 +251,7 @@ export async function modulesInstallCommand(source: string): Promise<void> {
210251 if ( install . npm_package ) {
211252 const npmSpinner = ora ( { text : `Installing ${ install . npm_package } ...` , color : 'green' } ) . start ( ) ;
212253 try {
213- execSync ( ` npm install -g ${ install . npm_package } ` , { stdio : 'pipe' } ) ;
254+ execFileSync ( ' npm' , [ ' install' , '-g' , '--' , install . npm_package ] , { stdio : 'pipe' } ) ;
214255 npmSpinner . succeed ( `Package ${ install . npm_package } installed globally` ) ;
215256 } catch ( err ) {
216257 npmSpinner . fail ( `npm install failed: ${ ( err as Error ) . message } ` ) ;
@@ -219,7 +260,7 @@ export async function modulesInstallCommand(source: string): Promise<void> {
219260 } else if ( install . git_url ) {
220261 const cloneSpinner = ora ( { text : 'Cloning module repository...' , color : 'green' } ) . start ( ) ;
221262 try {
222- execSync ( ` git clone --depth 1 ${ install . git_url } ${ dest } ` , { stdio : 'pipe' } ) ;
263+ execFileSync ( ' git' , [ ' clone' , ' --depth' , '1' , '--' , install . git_url , dest ] , { stdio : 'pipe' } ) ;
223264 } catch ( err ) {
224265 cloneSpinner . fail ( `Clone failed: ${ ( err as Error ) . message } ` ) ;
225266 return ;
@@ -238,7 +279,8 @@ export async function modulesInstallCommand(source: string): Promise<void> {
238279 if ( ! res . ok ) { dlSpinner . fail ( `HTTP ${ res . status } ` ) ; return ; }
239280 const tar = join ( dir , `${ mod . slug } .tar.gz` ) ;
240281 writeFileSync ( tar , Buffer . from ( await res . arrayBuffer ( ) ) ) ;
241- execSync ( `tar -xzf ${ tar } -C ${ dir } ` , { stdio : 'pipe' } ) ;
282+ assertSafeTarballEntries ( tar ) ;
283+ execFileSync ( 'tar' , [ '-xzf' , tar , '-C' , dir ] , { stdio : 'pipe' } ) ;
242284 rmSync ( tar , { force : true } ) ;
243285 const check = validateManifest ( dest ) ;
244286 if ( ! check . ok ) {
@@ -266,7 +308,13 @@ export async function modulesRemoveCommand(name: string): Promise<void> {
266308 console . log ( chalk . gray ( ' ' + '─' . repeat ( 50 ) ) ) ;
267309
268310 const dir = modulesDir ( ) ;
269- const target = join ( dir , name ) ;
311+ let target : string ;
312+ try {
313+ target = moduleDestination ( dir , name ) ;
314+ } catch ( err ) {
315+ console . log ( chalk . red ( ` x ${ ( err as Error ) . message } \n` ) ) ;
316+ return ;
317+ }
270318 if ( ! existsSync ( target ) ) {
271319 console . log ( chalk . yellow ( ` ! No module named "${ name } " in ${ dir } \n` ) ) ;
272320 return ;
0 commit comments