GitHub Security Advisory: GHSA-m4q3-457p-hh2x
Package: github.com/dagu-org/dagu (GO)
Severity: CRITICAL (CVSS 9.1)
CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Vulnerable versions: <= 2.2.4
Patched in: unpatched

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

Details:
## 1. Vulnerability Summary

The `dagRunId` request field accepted by the inline DAG execution endpoints is passed directly into `filepath.Join` to construct a temporary directory path without any format validation. Go's `filepath.Join` resolves `..` segments lexically, so a caller can supply a value such as `".."` to redirect the computed directory outside the intended `/tmp/<name>/<id>` path. A deferred cleanup function that calls `os.RemoveAll` on that directory then runs unconditionally when the HTTP handler returns, deleting whatever directory the traversal resolved to.

With `dagRunId` set to `".."`, the resolved directory is the system temporary directory (`/tmp` on Linux). On non-root deployments, `os.RemoveAll("/tmp")` removes all files in `/tmp` owned by the dagu process user, disrupting every concurrent dagu run that has live temp files. On root or Docker deployments, the call removes the entire contents of `/tmp`, causing a system-wide denial of service.


## 2. This Is Not a Duplicate of Existing Advisories

Two security advisories are already published for dagu. This vulnerability is distinct from both.

**GHSA-6qr9-g2xw-cw92** fixed the fact that the default authentication mode was `none`, allowing unauthenticated access to the inline execution endpoint. That advisory covers authentication bypass. The `dagRunId` path traversal described here is a separate input-validation flaw in `loadInlineDAG()` that exists regardless of whether authentication is required and was not addressed by that fix.

**CVE-2026-27598** fixed a path traversal in the DAG creation endpoint (`POST /api/v1/dags`) via the `name` field. The fix added `filepath.Base()` and a base-directory prefix check inside `generateFilePath()`. That fix applies only to `generateFilePath()` in `dags.go`. The function `loadInlineDAG()` in `dagruns.go` has no equivalent guard on its `dagRunID` argument and was not part of that patch.


## 3. Vulnerable Code

**File**: `internal/service/frontend/api/v1/dagruns.go`

The `loadInlineDAG` function (lines 202-267) constructs the temp directory at line 234:

```go
tmpDir := filepath.Join(os.TempDir(), nameHint, dagRunID)
```

`dagRunID` is user-supplied. No validation of the value occurs before this line. The cleanup closure is then registered:

```go
cleanup := func() {
    _ = os.RemoveAll(tmpDir)
}
```

In `ExecuteDAGRunFromSpec` (lines 52-119), the cleanup is deferred unconditionally:

```go
dag, cleanup, err := a.loadInlineDAG(ctx, request.Body.Spec, request.Body.Name, dagRunId)
if err != nil {
    return nil, err
}
defer cleanup()  // registered after loadInlineDAG succeeds; fires on all subsequent return paths
```

The same pattern appears in `EnqueueDAGRunFromSpec` (lines 122-200), line 160:

```go
defer cleanup()
```

**Why the OpenAPI schema pattern does not prevent this:**

The `DAGRunId` schema in `api/v1/api.yaml` (line 5738) declares:

```yaml
pattern: "^[a-zA-Z0-9_-]+$"
```

This pattern excludes `.` and `/`, which would b