-
Notifications
You must be signed in to change notification settings - Fork 37
[Coding Guideline] Prevent OS Command Injection #370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sei-dsvoboda
wants to merge
6
commits into
main
Choose a base branch
from
guideline/prevent-os-cmd-injection
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6aa11aa
Add guideline: Prevent OS Command Injection
sei-dsvoboda 5b41984
review feedback
sei-dsvoboda b338384
more feedback tweaks
sei-dsvoboda 2c0c53b
New tags
sei-dsvoboda bcac33d
Cite IDS07-J
sei-dsvoboda 8bf8735
Merge branch 'main' into guideline/prevent-os-cmd-injection
felix91gr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
117 changes: 117 additions & 0 deletions
117
src/coding-guidelines/program-structure-and-compilation/gui_J3K3ZqC8qoOn.rst.inc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my view,
Commandis actually performing this, the problem is when it is used withsh-c, meaning theCommandruns the interpreter. I think one should never actually run the interpreter, rather than taking care of the parameters.There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 (
shor 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.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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:
That criteria could be something along the lines of...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.