What is a curl | sh installer?

Many software projects provide a shell script to automate installation. In order to make it look simple, they’ll suggest downloading that installation script using curl or wget and immediately pipe it to a shell. For example, to install Rust they suggest you run the following in your favorite terminal:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

I call these “curl pipe to shell” (curl | sh) installers. Lots of projects use this pattern. Here’s a list of software that I personally use that include curl | sh as an installation option:

What exactly are these scripts doing? Running curl | $EDITOR - can get you started answering that question. Sometimes the scripts can be complex with many different code paths. I’ve seen installers which download additional scripts which are executed in turn. Most alarming of all is if you’re asked to provide your password for root access through sudo (I’m looking at you IBM Cloud CLI 🤨) !

In addition to inspecting the script itself, I like to inspect the results of actually running the script. Rootless containers are perfect for this! Podman is my preferred tool, but Docker is also able to run rootless these days.

Choose a base, install then diff

Let’s use Starship as an example and go through the steps I’d use to inspect the results of its curl | sh installer.

  1. Choose a base image

    I’m running Debian 12 (bookworm) right now, so we’ll start with the docker.io/debian:bookworm image. This image does not include curl, though, so we’ll need to build on top of it. Here’s a simple Containerfile:

    FROM docker.io/debian:bookworm
    RUN apt-get update
    RUN apt-get install -y curl
    

    Building this will create our start image.

    podman build -t debian-curl:bookwork .
    
  2. Run the base image and open an interactive TTY

    Giving it a name is always helpful but not required.

    podman run -it --name starship-install debian-curl:bookworm
    
  3. From inside the container, run the curl | sh installer

    According to their documentation, the following curl | sh is provided to install the latest version of Starship in Linux:

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    

    Follow the interactive prompts, remembering the selections you make. It’s important to remember this procedure only tests a single code path!

    When the installer finishes, exit out of the container.

  4. Diff the before and after images

    podman diff starship-installer
    

    The output for me is

    C /etc
    C /tmp
    A /tmp/tmp.40X4dFTsTy
    A /tmp/tmp.40X4dFTsTy.tar.gz
    C /usr
    C /usr/local
    C /usr/local/bin
    A /usr/local/bin/starship
    C /root
    A /root/.bash_history
    

    The podman-diff documentation provides the following table:

    SymbolDescription
    AA file or directory was added.
    DA file or directory was deleted.
    CA file or directory was changed.

    So the podman diff shows a compressed archive in /tmp, the starship binary in /usr/local/bin/ and a .bash_history file.

    From here, you can go back into the container and inspect further if you wish.

    podman start -a starship-installer
    
  5. Cleanup

    When you’re finished, don’t forget to remove the container.

    podman rm starship-installer
    

Caveats

  • This is meant to compliment inspecting the script itself, not replace it!

  • This tests only a single code path! Differences between your environment and the container you can result in different outcomes.

  • Intermediate states are overlooked. It’s possible that the installer could do unwanted things 😧, then clean up after itself and end in a state that looks OK in or final diff.

  • Other things I haven’t thought of 🙃.

Post history

DateDescription
2024-02-29Initial publish