So many developer and AI tools today ship the same first step: Copy this line to clipboard, paste it into a terminal, hit enter:

curl -fsSL https://example.com/install.sh | sh

The argument against it is a saturated genre. Nearly every write-up addresses the same three points:

I’d like to cover an additional angle - the risk that lives in the page rendering the command rather than in the script itself. What a publisher can do to make tampering detectable rather than silent. And the part that matters most once the pattern is already running in your environment: what a blue team can actually see in their own logs.

The part nobody threat-models: the page hosting the text for users to copy & run

The other blogs on this topic tend to focus on the bytes piped into the shell. They almost never analyse the web page that renders the command for you to copy. That page is an asset, and it has its own attack surface. This is something that I observed today as I looked at yet another new AI tool announcement on LinkedIn. In that case, the product vendor had done little to nothing to secure their website from which the install string is advertised.

Two things make it interesting. First, the install and landing pages tend to carry a marketing tag stack. Google Tag Manager, an analytics SDK with session replay, a CRM script, an ad pixel or two.

A tag manager exists specifically to load and run further code defined in a remotely managed container, which is an unreviewed code-push channel to every visitor. Any one of those third-party vendors, or anyone who compromises one of those accounts, can rewrite the install command in the DOM.

With no Content Security Policy (CSP) on the page, nothing constrains what an injected script does, including swapping the URL that users are about to paste into their shell.

Second, the command on the page is inert DOM text with no integrity binding. Any script that executes in the page can find it and rewrite it.

An attacker can leave the visible command untouched and still get you. They could inject a copy event listener that intercepts the clipboard write and substitutes a different string, so the terminal-ready text on screen stays correct while whatever lands in your buffer points somewhere else. This same trick has been swapping crypto wallet addresses on copy for years, pointed at an install command headed for a root shell. The training that most people have received, reading the line before they run it, assumes the rendered text and the clipboard contents are the same thing.

There’s a further wrinkle. Savvy developers will probably eyeball the domain in a curl | sh line. If the published command uses -L to follow redirects, an open redirect anywhere on the publisher’s own domain lets an attacker show a command whose visible URL still reads as the legitimate site while the fetch lands somewhere else entirely.

We have recent examples…

Codecov’s Bash Uploader compromise in 2021 is the canonical case: an attacker got a GCS credential, added one line to the uploader script served to every customer, and exfiltrated environment variables out of CI pipelines. No integrity check on the served bytes meant it stayed silent. The attacker even reverted the change during audits, so only a customer comparing the served hash against the shipped one eventually caught it. Months of dwell time.

In 2025, VulnCheck documented the Linuxsys cryptominer staging its install scripts on compromised WordPress sites, which is the “distribution surface is softer than the source repo” problem in one sentence.

Microsoft’s write-up on Shai-Hulud 2.0, the npm and PyPI worm that resurfaced in 2026, shows a chain that spawns curl -fsSL https://bun.sh/install | bash as part of its propagation. The one-liner is an active link in live supply-chain attacks, not a theoretical weak point on a slide.

What the publisher can do

If you ship an installer this way, the highest-value change is to bind integrity to the artefact and let people verify it out of band. Publish a SHA-256 and a detached signature (minisign, GPG, or cosign) for install.sh on a different origin, and document a verify-then-run path:

# Fetch, don't execute
curl -fsSLo install.sh https://example.com/install.sh
# Verify against the published signature before running a single line
minisign -Vm install.sh -P RWS...publickey...
sh install.sh

This makes tampering detectable across both the injection routes and the channel-compromise routes, with one caveat that decides whether it works at all: the verification material has to travel a different trust path than the script, and the signing key has to stay out of the attacker’s hands. A SHA-256 published next to install.sh on the same origin is theatre, because whoever rewrote the script can easily rewrite the hash sitting beside it. The detached signature is the part that holds, because its public key reached the user through a different channel and an attacker who owns the origin can’t forge against it.

Make the script truncation-safe while you’re at it. Wrap every bit of logic in functions and call main "$@" on the very last line, so a download cut off partway through executes nothing:

#!/bin/sh
main() {
    detect_platform
    download_binary
    verify_checksum
    install_binary
}

# A truncated download never reaches this line, so nothing runs
main "$@"

For the page itself, render the command from a dedicated minimal page or subdomain (install.example.com) that loads zero third-party JavaScript, under a strict CSP (script-src 'self' plus a nonce, no 'unsafe-inline'). GTM, analytics and CRM tags make a strict CSP fragile to keep intact, which is the argument for keeping them off the one page that renders a command headed for a shell, not off the rest of the marketing site.

Audit the domain for open redirects and treat any finding as high severity given the -L amplifier. On the pipeline side, short-lived deploy credentials via OIDC instead of a long-lived bucket write key, branch protection with code-owner review on the path that produces install.sh, and CI Actions pinned to a commit SHA.

Alternatively, shipping your tool through a package manager or signed repository (Homebrew, winget, apt with signed metadata) moves verification into tooling the user already trusts, with signatures and revocation handled for you. That’s still no guarantee, as the never-ending supply chain attacks illustrate on a weekly basis.

The channel hardening that’s a config change away

Most of the page and channel attacks above are mitigated by controls that cost nothing but a few configuration tweaks, and plenty of install pages I’ve seen ship without them.

These are the table stakes for any site that serves a command someone is meant to paste into a shell:

  • Start with the name and the transport. DNSSEC signs your DNS responses so a resolver can tell a forged answer from a real one. Without it, the name that points at your install host is unauthenticated, and a poisoned response can send curl to an attacker’s address before TLS gets a say.
  • CAA records restrict which certificate authorities may issue for your domain, which narrows the mis-issued-certificate path that TLS interception depends on.
  • HSTS with preload forecloses the downgrade-to-HTTP trick, so a network attacker can’t strip the connection back to plaintext and rewrite the script in flight.
  • The strict CSP from earlier is the real cross-site-scripting control now that browsers have retired the old X-XSS-Protection header, which did little and is best left unset.
  • X-Content-Type-Options: nosniff stops the browser second-guessing your declared content types.
  • X-Frame-Options, or a frame-ancestors directive in the CSP, keeps the page out of an attacker’s iframe and closes the clickjacking route to a swapped command.

What the blue team can do

Depending on which endpoint platform you’re running, application control (whitelisting) can shut down the execution of arbitrary scripts downloaded from the Internet.

Windows App Control for Business (formerly WDAC, thanks to the Microsoft product renaming department!) plus Constrained Language Mode and AMSI actually constrain the PowerShell equivalents such as iwr ... | iex.

For detection, Splunk ships a maintained, first-party rule: File Download or Read to Pipe Execution (26f86252-1549-45e1-a212-eb26840e86bc), updated May 2026, mapped to T1105 (Ingress Tool Transfer) with the execution landing under T1059 (Command and Scripting Interpreter).

There’s no first-party Sentinel equivalent, so here’s some example KQL that keeps the same three-part logic, checks both the child and the initiating process command lines, and extends the sink to catch the no-pipe PowerShell form (IEX (New-Object Net.WebClient).DownloadString('...')) that has no pipe and falls outside the Splunk rule:

let Pre = dynamic(["curl","wget","certutil","bitsadmin","mshta","iwr","irm","iex","downloadstring","downloadfile","getstring","invoke"]);
let Downloaders = @"(?i)(\.downloadfile\(|\.downloadstring\(|ascii\.getstring|bitsadmin|certutil|\bcurl\b|\bwget\b|invoke-restmethod|invoke-webrequest|\birm\b|\biwr\b|mshta)";
let PipeToUnixShell = @"(?i)\|\s*(sudo\s+)?(rbash|bash|dash|tcsh|zsh|ksh|csh|fish|sh)\b";
let PsExec = @"(?i)\b(iex|invoke-expression)\b";
DeviceProcessEvents
| where TimeGenerated > ago(7d)
| where ProcessCommandLine has_any (Pre) or InitiatingProcessCommandLine has_any (Pre)
| extend HitChild = ProcessCommandLine matches regex Downloaders and (ProcessCommandLine matches regex PipeToUnixShell or ProcessCommandLine matches regex PsExec)
| extend HitParent = InitiatingProcessCommandLine matches regex Downloaders and (InitiatingProcessCommandLine matches regex PipeToUnixShell or InitiatingProcessCommandLine matches regex PsExec)
| where HitChild or HitParent
| extend MatchedCommandLine = iff(HitChild, ProcessCommandLine, InitiatingProcessCommandLine)
| extend ElevatedPipe = MatchedCommandLine matches regex @"(?i)\|\s*sudo\s"
| project TimeGenerated, DeviceName, AccountName, FileName, ElevatedPipe, MatchedCommandLine, ProcessCommandLine, InitiatingProcessCommandLine, FolderPath, ReportId
| sort by TimeGenerated desc

The above query is written for Sentinel in Log Analytics. YMMV in the Defender XDR portal.

Any unfamiliar hostname piped into a shell on a corporate endpoint warrants further inspection. I spotted a malicious ClickFix example in a customer environment while testing the above KQL query.

To tune the query, you can extract the host from the matched command line and whitelist the installer domains you trust. Add the let beside the others and the two pipeline lines before the final project:

let TrustedInstallHosts = dynamic(["chocolatey.org","astral.sh","get.docker.com","sh.rustup.rs"]);
// ... existing query body ...
| extend FetchedHost = tolower(extract(@"https?://([^/\s""']+)", 1, MatchedCommandLine))
| where isempty(FetchedHost) or FetchedHost !in (TrustedInstallHosts)

Keep that whitelist to domains a single vendor controls. Make sure you don’t add things like shared-hosting hosts such as raw.githubusercontent.com, a GitHub gist, an S3 bucket, etc as they will re-open the exact hole that the rule exists to catch - because anyone can serve a script from those.

The principle underneath all of it

  • Assume that the channel will be compromised at some point, then make tampering detectable on the publisher side and the outcome survivable on the consumer side.
  • Bind integrity to the artefact and verify it out of band. Don’t pipe to a shell when you can fetch, check a signature, and run.
  • Keep the marketing tags off the page that renders the command.

Further reading

  • Codecov, Security Update - the canonical curl-pipe distribution-channel compromise.
  • Microsoft Security, Shai-Hulud 2.0 guidance - current supply-chain worm that spawns a pipe-to-shell install.
  • Splunk, File Download or Read to Pipe Execution - the maintained detection this post extends to Sentinel.
  • idontplaydarts, [Detecting the use of curl bash server side](https://www.idontplaydarts.com/2016/04/detecting-curl-pipe-bash-server-side/) - the originating server-side detection write-up, for the canon this post deliberately steps around.