# Dagu: Path Traversal via `dagRunId` in Inline DAG Execution

Unauthenticated/operator dagRunId=".." in dagu inline DAG endpoints makes filepath.Join resolve to /tmp, then deferred os.RemoveAll wipes it.

- Payout: Research
- Severity: critical
- Program: Dagu
- Found by: NucleiAv
- Disclosed: 2026-03-13
- CVE: CVE-2026-31886
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-m4q3-457p-hh2x
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-m4q3-457p-hh2x
- Tags: area:web domain:api domain:web impact:dos impact:integrity kind:research platform:ghsa sev:critical tech:go technique:source-review vuln:dos vuln:file-upload vuln:path-traversal

**Target:** POST /api/v1/dag-runs and /api/v1/dag-runs/enqueue inline DAG execution endpoints
**Root cause:** User-supplied dagRunId is passed unvalidated into filepath.Join(os.TempDir(), nameHint, dagRunID); Go's lexical Clean resolves '..' so tmpDir becomes /tmp, and a deferred os.RemoveAll(tmpDir) then deletes it.
**Steps:**
1. Authenticate (or omit auth on <=1.30.3 with auth.mode none) to obtain a token.
2. POST to `/api/v1/dag-runs` with body `{"spec":"steps:\n  - name: s\n    command: id\n","dagRunId":".."}`.
3. Server computes `filepath.Join("/tmp","inline","..")` = `/tmp`, MkdirAll succeeds, cleanup closure captures `/tmp`.
4. Spec written to `/tmp/inline.yaml`, loadInlineDAG returns, `defer cleanup()` fires `os.RemoveAll("/tmp")` before the 200 response is sent.
5. Repeat request to maintain DoS; variant with `dagRunId:"../../home/dagu/dags"` and `name:"payload"` writes payload.yaml then deletes the DAGs directory.
**How found:** Code review of loadInlineDAG: noticed dagRunID flows into filepath.Join without validation, that the OpenAPI pattern `^[a-zA-Z0-9_-]+$` is never enforced because StrictValidation is always false (no mapstructure tag, absent from Definition struct), and that sanitizeFilename is applied to log filenames but not dagRunID; traced the deferred cleanup to os.RemoveAll.
**Exploitation:** Single authenticated (or unauthenticated on old versions) HTTP request to the inline DAG endpoint with dagRunId=".." triggers os.RemoveAll on /tmp during the deferred cleanup, deleting all dagu-owned temp files (or all of /tmp as root/Docker), disrupting concurrent runs and system processes; repeatable with no rate limit.
**Escalation:** Chaining the arbitrary file write (spec written as <nameHint>.yaml into traversed dir) with the RemoveAll cleanup lets an attacker delete the entire DAGs directory, destroying all workflow definitions.
**Insight:** When a schema pattern exists but enforcement is gated behind a config flag, verify the flag is actually reachable from config (mapstructure tags, Definition struct membership) — a dead config field means the pattern is decorative. Also audit deferred cleanup closures that capture user-influenced paths.

## Proof of concept (copied exactly from the original report)

**curl request with dagRunId traversal payload**

```
curl -s -X POST http://localhost:8080/api/v1/dag-runs \
  -H "Content-Type: application/json" \
  -d '{"spec":"steps:\n  - name: s\n    command: id\n","dagRunId":".."}'
```

**Enqueue endpoint variant with traversal payload**

```
curl -s -X POST http://TARGET:8080/api/v1/dag-runs/enqueue \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"spec":"steps:\n  - name: s\n    command: id\n","dagRunId":".."}'
```

