Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
.. SPDX-License-Identifier: MIT OR Apache-2.0
SPDX-FileCopyrightText: The Coding Guidelines Subcommittee Contributors

.. guideline:: Prevent OS Command Injection
:id: gui_a3PpM90Fppwh
:category: mandatory
:status: draft
:release: 1.0.0-latest
:fls: fls_hdwwrsyunir
:decidability: undecidable
:scope: module
:tags: injection,sanitization

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my view, Command is actually performing this, the problem is when it is used with sh -c, meaning the Command runs the interpreter. I think one should never actually run the interpreter, rather than taking care of the parameters.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, @alexandruradovici
I would agree that running the "sh" interpreter enables the command injection here. There are two general approaches: first, you would sanitize the arguments to prevent anything malicious from happening. Second, you use a less powerful command, such as invoking 'ls' directly without 'sh', or using fs::read_dir()...both compliant solutions use this second technique.

Are you suggesting a change in the wording, or in the code examples?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion is to use a more powerful command and advise agains running the interpreter (sh or equivalent) with any command. I would not relate it to the arguments, but to the what kind of commands should or should not be ran.

Copy link
Collaborator

@felix91gr felix91gr Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder how we should do this.

Something that seems simple enough to do is a deny-list:

  • In the worst case, we would be denying non-injection-allowing programs. This is fine, as a failure on our labeling would not lead to a safety hazard.
  • An allow list would have the opposite problem: if we fail to account for injectability in a program we mark as "okay", we would be giving the 👍🏻 to a safety hazard. That's why I think an allow list would probably be bad to have.
  • For all other command-line programs, we give the user some criteria by which they might be able to know if running it is okay or not (in a non-ambiguous way)

That criteria could be something along the lines of...

  • "The program must not be turing-complete given its input": this would deny all possible injection mechanisms, but some valid programs could be denied as well. For example, a program that does arbitrary computation inside its own "isolated world" would be fine to run, but would not be allowed by this rule.
  • "The program must not be able to transitively spawn an arbitrary number of other processes": this would be a lot harder to assess, but feels a lot closer to what we want to avoid.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not relate it to the arguments, but to the what kind of commands should or should not be ran.

Ultimately, This is a Rust rule. I don't want the rule to blacklist some commands, because we can't make that blacklist complete. Too many shell commands + too many OS platforms. Creating a whitelist of 'benign' commands is safer, but no less Herculean a problem.

You also have the problem of when arguments are tokenized on Windows (see the BatBadBut vul).

We could try to limit commands in terms of Turing-completeness, but that also sounds like a huge rabbit hole. Which of these commands would we be forbidden from running: bash, python, sqlite3, jq, gcc ?

The only other option I see would be to forbid Commands entirely (at least for security-critical code). Allow exceptions only on an individual basis. Or allow Commands only in unsafe code.


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

.. rationale::
:id: rat_IaAZISFOmAt0
:status: draft

This rule was inspired by :cite:`gui_a3PpM90Fppwh: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 :cite:`gui_a3PpM90Fppwh:CVE-2024-24576`. See :cite:`gui_a3PpM90Fppwh:RUST-WIN-ARG-SPLIT` and :cite:`gui_a3PpM90Fppwh:SEI-BATBADBUT` for more information.

.. non_compliant_example::
:id: non_compl_ex_Owe2nVInv90z
:status: draft

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”.

.. rust-example::

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"
}
}


.. compliant_example::
:id: compl_ex_rJeLKhdopITN
:status: draft

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.

.. rust-example::

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::
:id: compl_ex_BSjAFOLfL4Rk
:status: draft

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

.. rust-example::

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
}
}


.. bibliography::
:id: bib_CNrst9CcDVQJ
:status: draft

.. list-table::
:header-rows: 0
:widths: auto
:class: bibliography-table

* - :bibentry:`gui_a3PpM90Fppwh:CERT-J-IDS07`
- SEI CERT Java. "IDS07-J. Sanitize untrusted data passed to the Runtime.exec() method." https://wiki.sei.cmu.edu/confluence/x/xTdGBQ
* - :bibentry:`gui_a3PpM90Fppwh:RUST-WIN-ARG-SPLIT`
- Module process. "Windows Argument Splitting." https://doc.rust-lang.org/std/process/index.html#windows-argument-splitting
* - :bibentry:`gui_a3PpM90Fppwh:SEI-BATBADBUT`
- SEI Blog. "What Recent Vulnerabilities Mean to Rust | “BatBadBut” Command Injection with Windows’ cmd.exe (CVE-2024-24576)." https://www.sei.cmu.edu/blog/what-recent-vulnerabilities-mean-to-rust/
* - :bibentry:`gui_a3PpM90Fppwh:CVE-2024-24576`
- MITRE. "CVE-2024-24576." https://nvd.nist.gov/vuln/detail/CVE-2024-24576
3 changes: 3 additions & 0 deletions src/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@
dict(name="defect", description="Guideline associated with the defect-prevention profile"),

dict(name="unsafe", description="Guidelines that interact with or involve the unsafe keyword"),

dict(name="injection", description="Guidelines about various kinds of injections"),
dict(name="sanitization", description="Guidelines about sanitizing untrusted input"),
]

needs_categories = [
Expand Down