> As it turns out, bash can speak HTTP by itself.
No, it can not. Bash lets you open TCP sockets.
What you are doing here is trying to speak HTTP yourself, which is fine for testing and debugging, and hella cool for fun to do by hand, but you will shoot yourself in the foot if you try to use this pseudo http client unattended in reality. This toy code does not parse HTTP properly and will break.
You could of course write a full http/1.1 client in bash, you can even do a full http server in pure bash: https://github.com/bahamas10/bash-web-server
For less insane, non-bash shells there is always nc which is usually probably the wiser choice.
I ran into this while checking connectivity between containers on an internal Docker network where the image had neither curl nor wget.
The main surprise was that Bash has /dev/tcp which lets you do the equivalent of an HTTP request with a bit of shell magic, for instance:
exec 3<>/dev/tcp/service/8642
printf 'GET /health HTTP/1.1\r\nHost: service\r\nConnection: close\r\n\r\n' >&3
cat <&3
Where `service` is just the hostname of whatever you’re talking to and 8642 is the port you are trying to talk HTTP to.Pretty cool!
Neat, works against example.com
exec 3<>/dev/tcp/example.com/80
printf 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' >&3
cat <&3
Outputs: HTTP/1.1 200 OK
Date: Tue, 16 Jun 2026 17:37:45 GMT
Content-Type: text/html
...
I always end up on example.com for this kind of thing because there are so few domains these days that don't enforce https!This was something I learned about 10 years ago when earning my OSCP, useful during penetration tests and CTFs when you get a low-priv shell that's running a minimal OS (No curl, nc, python, etc.) but running a web server listening on localhost.
Using /dev/tcp was also handy in getting that initial low-priv shell.
TIL: bash and other shells try to copy Plan 9's /net directory and the kernel ip(3) file server. Too bad it's not a real file system. And a missed opportunity to call the root of the path /net.
At a past job the security team wouldn't let us have netcat or curl on our systems. So I just used /dev/TCP to get around that. The ergonomics were not as nice as using netcat or curl, but it got the job done.
This is the kind of content we all deserved in 2026, and this is still why I ask during interviews to explain how cookies are represented in HTTP protocol.
I actually have a couple of Dockerfiles that are using exactly this in the HEALTHCHECK. Less packages to install.
It is a KornShell feature since ca. 1997
https://github.com/ksh93/ast-open-archive/blame/master/src/c...
I would use HTTP/1.0 without a need for Connection: close. Unless 1.0 is not generally supported anymore, but this is not the case in my experience.
A few years ago I had to do this for a SpringBoot health check from a Docker container:
FROM openjdk:11-jre-slim HEALTHCHECK --start-period=10s --timeout=3s --retries=5 \ CMD perl -e "use IO::Socket; $sock = IO::Socket::INET->new(Proto => 'tcp', PeerAddr => 'localhost', PeerPort => '8888') or die $@; $sock->autoflush(1); print $sock 'GET /actuator/health HTTP/1.1' . chr(0x0a) . chr(0x0d) . 'Host: localhost:8888' . chr(0x0a) . chr(0x0d) . 'Connection: close' . chr(0x0a) . chr(0x0d) . chr(0x0a) . chr(0x0d); while (my $line = $sock->getline ) { if ($line =~ /UP/) {exit;} }; close $sock; exit 1;"
You could also use nsenter if curl is installed on the host, eg
docker inspect -f '{{.State.Pid}}' container-name
# let's imagine that outputs 814538
nsenter -t 814538 -n curl example.com
Once had a coworker tell me to never to use this because "you never know when the customer doesn't have bash installed; use python instead" even though our contract required that the customer had bash. I'm still laughing at that.
Fun story: A few years ago, I worked for a small company that customized off the shelf routers to enable businesses provide Wifi Hotspots.
The routers were very basic model with very limited flash memory (~4MB?). I was brought in to build firmware for those routers. I ended up customising openwrt - removed all kinds of packages to make their packages fit on those routers. At the end, I had less than 4KB space, And I needed to implement a "heart beat" service. A lot of routers were behind firewalls that only allowed http, https and a couple of other protocols. Libcurl was too heavy. So I ended up writing a shell script that used this feature of bash to send out heart beats.
Fun times...
This is an old post-compromise trick used when an attacker needs to download a payload or make a network connection and curl, wget and nc are all not available.
This is pretty neat if all you need is to ping a local server but please use curl (or something equivalent) for contacting remote services. HTTP1.1 seems like such a simple protocol but in the real world you need to deal with proxies, different encodings, and redirects. Curl takes care of that (and a host of other annoying stuff) for you.
It's interesting that most of the comments here are about using this feature to bypass security restrictions (whether valid or not). It says a lot about the attack surface of GNU utilities caused by featuritis.
I find /dev/udp much more useful. I can create aliases for fire and forget commands to my daemons without actually writing *ctl program.
It's a fun trick, but I really don't like that bash does this. It's such an un-clean interface, and I'm not aware of any use cases beyond trying to exfiltrate data from a badly locked-down shell.
It was fun exploring this to make a native-shell-only peer-to-peer file transfer utility at work for some automation scripts. At least, it was until trying to replicate it in Powershell was somehow triggering Crowdstrike and the corporate Cybersecurity team thought I was writing malware.
I discovered this bash trick by chance when I was once trying to healthCheck the Envoy's official OCI image container which didn't include curl or wget while forcing the envoy admin interface to listen on localhost which breaks the traditional k8s httpGet checks.
Yes, it used to be my goto few times when some devices tried to lockdown everything with bare minimum core utils and no network capable tools like curl etc.
That's pretty neat, thanks for sharing
At least on my systems there's also /dev/udp...
Reminds me of telnetting to port 80 to make a get request years and years ago
Wait until they hear about Plan 9!
brb. recompiling bash in all my base images.
Reminds me of using telnet to port 80 to make get requests aeons ago
[dead]
[flagged]
As a kid in the late 90s my mind was blown when I realized I could telnet to port 80, 25, or 110 and interact with the servers manually.
Simple get: GET / HTTP/1.1 Content-Type: text/html User-Agent: l33t hax0rs lol X-Funny-Monkey: farts
For sending a mail message on port 25: HELO mail-from: [email protected] mail-to: [email protected] <other headers> <blank line> Body of the message yay. <two blank lines to end>
POP3 was so long ago I forgot but you could list the mailboxes then get individual messages and so on.
This revelation was the beginning of "there is no magic" for me. The realization that every part of the computer was built by human beings and was at some level understandable if one undertook the effort.
Perhaps most people in the future won't bother. They'll just let agents do it all. I'm sure that will leave some interesting holes in various systems for people willing to actually learn how they work without the filter of a model (or its safety rails).