Skip to content

[Coding Guideline]: Prevent OS Command Injection #360

@sei-dsvoboda

Description

@sei-dsvoboda

Chapter

Program Structure and Compilation

Guideline Title

Prevent OS Command Injection

Category

Mandatory

Status

Draft

Release Begin

1.0.0

Release End

latest

FLS Paragraph ID

fls_hdwwrsyunir

Decidability

Undecidable

Scope

Module

Tags

injection,sanitization

Amplification

Commands that are passed to an external OS command interpreter, like std::process::Command, should not allow untrusted input to be parsed as part of the command syntax.

Instead, an untrusted input should be passed as a single argument.

Exception(s)

No response

Rationale

This rule was inspired by CERT-J-IDS07.

When preparing a command to be executed by the operating system, untrusted input should be sanitized to make sure it does not alter the syntax of the command to be executed. For commands that do not tokenize their arguments, such as sh, the easiest way to do this is to avoid mixing untrusted data with trusted data via concatenation or formatting (a la format!()). Instead provide the untrusted data as a lone argument. The Command::new() constructor makes this easy by accepting the pre-tokenized arguments as a list of strings.

Traditionally untrusted data should be one argument (aka command-line token). OS command injection occurs when a malicious data fools the command tokenizer into interpreting it as multiple arguments, or even multiple commands. Complexity in the command tokenizer can exacerbate this problem, leading to vulnerabilities such as CVE-2024-24576. See RUST-WIN-ARG-SPLIT and SEI-BATBADBUT for more information.

Non-Compliant Example 1 - Prose

The following code lists the contents the directory provided in the dir variable. However, since this variable is untrusted, a dir such as dummy | echo BOO will cause the command to be executed. Thus, the program prints "BOO".

Non-Compliant Example 1 - Code

use std::process::{Command, Output};
use std::io;

fn files(dir: &str) -> io::Result<Output> {
    return Command::new("sh")
        .arg("-c")
        .arg(format!("ls {dir}"))
        .output();
}

fn main() {
    if cfg!(unix) {
        let _ = files("dummy | echo BOO");  // Program prints "BOO"
    }
}

Non-Compliant Example 2 - Prose (Optional)

No response

Non-Compliant Example 2 - Code (Optional)

No response

Non-Compliant Example 3 - Prose (Optional)

No response

Non-Compliant Example 3 - Code (Optional)

No response

Non-Compliant Example 4 - Prose (Optional)

No response

Non-Compliant Example 4 - Code (Optional)

No response

Compliant Example 1 - Prose

An untrusted input should be passed as a single argument. This prevents any spaces or other shell punctuation in the input from being misinterpreted by the OS command interpreter.

Compliant Example 1 - Code

use std::process::{Command, Output};
use std::io;

fn files(dir: &str) -> io::Result<Output> {
    return Command::new("ls")
        .arg(dir)
        .output();
}

fn main() {
    if cfg!(unix) {
        let _ = files("dummy | echo BOO");  // Command is invalid, but does not print BOO
    }
}

Compliant Example 2 - Prose (Optional)

A better approach is to avoid OS commands and use a specific API (in this case fs::read_dir()) to achieve the desired result.

Compliant Example 2 - Code (Optional)

use std::fs;
use std::io;

fn files(dir: &str) -> io::Result<Vec<std::ffi::OsString>> {
    return fs::read_dir(dir)?
        .map(|res| res.map(|e| e.file_name()))
        .collect();
}

fn main() {
    if cfg!(unix) {
        let _ = files("dummy | echo BOO");  // Command is invalid, but does not print BOO
    }
}

Compliant Example 3 - Prose (Optional)

No response

Compliant Example 3 - Code (Optional)

No response

Compliant Example 4 - Prose (Optional)

No response

Compliant Example 4 - Code (Optional)

No response

Bibliography

Metadata

Metadata

Assignees

Labels

category: mandatoryA coding guideline with category mandatorychapter: program-structure-and-compilationcoding guidelineAn issue related to a suggestion for a coding guidelinedecidability: undecidableA coding guideline which cannot be checked automaticallyscope: moduleA coding guideline that can be determined applied at the module levelsign-off: create prA coding guideline issue that's been reviewed and now requesting to create a Pull Request for itstatus: draft

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions