VYPR
High severityNVD Advisory· Published Jul 26, 2024· Updated Aug 2, 2024

Starship vulnerable to shell injection via undocumented, unpredictable shell expansion in custom commands

CVE-2024-41815

Description

Starship cross-shell prompt versions 1.0.0 to 1.19.9 allow shell injection due to undocumented shell expansion in custom commands, patched in 1.20.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Starship cross-shell prompt versions 1.0.0 to 1.19.9 allow shell injection due to undocumented shell expansion in custom commands, patched in 1.20.0.

Vulnerability

CVE-2024-41815 affects Starship prompt versions 1.0.0 through 1.19.9. The bug stems from undocumented and unpredictable shell expansion and quoting rules when custom commands are used in bash. This can lead to shell injection if an attacker crafts a malicious custom command.

Exploitation

Exploitation requires the victim to have custom commands configured. The attack vector is local, with high attack complexity, but no user interaction or privileges required [1][3]. The limited scope makes targeted attacks difficult without knowledge of the victim's custom commands.

Impact

Successful exploitation allows arbitrary command execution with the privileges of the shell, leading to high confidentiality, integrity, and availability impact (CVSS 7.4) [3].

Mitigation

The vulnerability is fixed in Starship version 1.20.0. Users should upgrade immediately or avoid using custom commands until patched [4].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
starshipcrates.io
>= 1.0.0, < 1.20.01.20.0

Affected products

2

Patches

1
cfc58161e0ec

Merge commit from fork

https://github.com/starship/starshipDavid KnaackJul 26, 2024via ghsa
4 files changed · +85 20
  • docs/config/README.md+5 3 modified
    @@ -4756,11 +4756,12 @@ If you have an interesting example not covered there, feel free to share it ther
     
     :::
     
    -::: warning Command output is printed unescaped to the prompt
    +::: warning If `unsafe_no_escape` is enabled or prior to starship v1.20 command output is printed unescaped to the prompt.
     
     Whatever output the command generates is printed unmodified in the prompt. This means if the output
    -contains special sequences that are interpreted by your shell they will be expanded when displayed.
    -These special sequences are shell specific, e.g. you can write a command module that writes bash sequences,
    +contains shell-specific interpretable sequences, they could be interpreted on display.
    +Depending on the shell, this can mean that e.g. strings enclosed by backticks are executed by the shell.
    +Such sequences are usually shell specific, e.g. you can write a command module that writes bash sequences,
     e.g. `\h`, but this module will not work in a fish or zsh shell.
     
     Format strings can also contain shell specific prompt sequences, e.g.
    @@ -4778,6 +4779,7 @@ Format strings can also contain shell specific prompt sequences, e.g.
     | `require_repo`      | `false`                         | If `true`, the module will only be shown in paths containing a (git) repository. This option alone is not sufficient display condition in absence of other options.                                                                                                                           |
     | `shell`             |                                 | [See below](#custom-command-shell)                                                                                                                                                                                                                                                            |
     | `description`       | `'<custom module>'`             | The description of the module that is shown when running `starship explain`.                                                                                                                                                                                                                  |
    +| `unsafe_no_escape`  | `false`                         | When set, command output is not escaped of characters that could be interpreted by the shell.                                                                                                                                                                                                 |
     | `detect_files`      | `[]`                            | The files that will be searched in the working directory for a match.                                                                                                                                                                                                                         |
     | `detect_folders`    | `[]`                            | The directories that will be searched in the working directory for a match.                                                                                                                                                                                                                   |
     | `detect_extensions` | `[]`                            | The extensions that will be searched in the working directory for a match.                                                                                                                                                                                                                    |
    
  • .github/config-schema.json+4 0 modified
    @@ -6444,6 +6444,10 @@
             "ignore_timeout": {
               "default": false,
               "type": "boolean"
    +        },
    +        "unsafe_no_escape": {
    +          "default": false,
    +          "type": "boolean"
             }
           },
           "additionalProperties": false
    
  • src/configs/custom.rs+2 0 modified
    @@ -30,6 +30,7 @@ pub struct CustomConfig<'a> {
         #[serde(skip_serializing_if = "Option::is_none")]
         pub use_stdin: Option<bool>,
         pub ignore_timeout: bool,
    +    pub unsafe_no_escape: bool,
     }
     
     impl<'a> Default for CustomConfig<'a> {
    @@ -50,6 +51,7 @@ impl<'a> Default for CustomConfig<'a> {
                 os: None,
                 use_stdin: None,
                 ignore_timeout: false,
    +            unsafe_no_escape: false,
             }
         }
     }
    
  • src/modules/custom.rs+74 17 modified
    @@ -59,30 +59,38 @@ pub fn module<'a>(name: &str, context: &'a Context) -> Option<Module<'a>> {
             }
         }
     
    -    let parsed = StringFormatter::new(config.format).and_then(|formatter| {
    -        formatter
    +    let variables_closure = |variable: &str| match variable {
    +        "output" => {
    +            let output = exec_command(config.command, context, &config)?;
    +            let trimmed = output.trim();
    +
    +            if trimmed.is_empty() {
    +                None
    +            } else {
    +                Some(Ok(trimmed.to_string()))
    +            }
    +        }
    +        _ => None,
    +    };
    +
    +    let parsed = StringFormatter::new(config.format).and_then(|mut formatter| {
    +        formatter = formatter
                 .map_meta(|var, _| match var {
                     "symbol" => Some(config.symbol),
                     _ => None,
                 })
                 .map_style(|variable| match variable {
                     "style" => Some(Ok(config.style)),
                     _ => None,
    -            })
    -            .map_no_escaping(|variable| match variable {
    -                "output" => {
    -                    let output = exec_command(config.command, context, &config)?;
    -                    let trimmed = output.trim();
    -
    -                    if trimmed.is_empty() {
    -                        None
    -                    } else {
    -                        Some(Ok(trimmed.to_string()))
    -                    }
    -                }
    -                _ => None,
    -            })
    -            .parse(None, Some(context))
    +            });
    +
    +        if config.unsafe_no_escape {
    +            formatter = formatter.map_no_escaping(variables_closure)
    +        } else {
    +            formatter = formatter.map(variables_closure)
    +        }
    +
    +        formatter.parse(None, Some(context))
         });
     
         match parsed {
    @@ -244,6 +252,11 @@ fn exec_when(cmd: &str, config: &CustomConfig, context: &Context) -> bool {
     fn exec_command(cmd: &str, context: &Context, config: &CustomConfig) -> Option<String> {
         log::trace!("Running '{cmd}'");
     
    +    #[cfg(test)]
    +    if cmd == "__starship_to_be_escaped" {
    +        return Some("`to_be_escaped`".to_string());
    +    }
    +
         if let Some(output) = shell_command(cmd, config, context) {
             if !output.status.success() {
                 log::trace!("Non-zero exit code '{:?}'", output.status.code());
    @@ -298,6 +311,7 @@ fn handle_shell(command: &mut Command, shell: &str, shell_args: &[&str]) -> bool
     mod tests {
         use super::*;
     
    +    use crate::context::Shell;
         use crate::test::{fixture_repo, FixtureProvider, ModuleRenderer};
         use nu_ansi_term::Color;
         use std::fs::File;
    @@ -761,4 +775,47 @@ mod tests {
             assert_eq!(expected, actual);
             repo_dir.close()
         }
    +
    +    #[test]
    +    fn output_is_escaped() -> io::Result<()> {
    +        let dir = tempfile::tempdir()?;
    +
    +        let actual = ModuleRenderer::new("custom.test")
    +            .path(dir.path())
    +            .config(toml::toml! {
    +                [custom.test]
    +                format = "$output"
    +                command = "__starship_to_be_escaped"
    +                when = true
    +                ignore_timeout = true
    +            })
    +            .shell(Shell::Bash)
    +            .collect();
    +        let expected = Some("\\`to_be_escaped\\`".to_string());
    +        assert_eq!(expected, actual);
    +
    +        dir.close()
    +    }
    +
    +    #[test]
    +    fn unsafe_no_escape() -> io::Result<()> {
    +        let dir = tempfile::tempdir()?;
    +
    +        let actual = ModuleRenderer::new("custom.test")
    +            .path(dir.path())
    +            .config(toml::toml! {
    +                [custom.test]
    +                format = "$output"
    +                command = "__starship_to_be_escaped"
    +                when = true
    +                ignore_timeout = true
    +                unsafe_no_escape = true
    +            })
    +            .shell(Shell::Bash)
    +            .collect();
    +        let expected = Some("`to_be_escaped`".to_string());
    +        assert_eq!(expected, actual);
    +
    +        dir.close()
    +    }
     }
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.