VYPR
High severityNVD Advisory· Published Feb 25, 2026· Updated Feb 26, 2026

Dagu: Path traversal in DAG creation allows arbitrary YAML file write outside DAGs directory

CVE-2026-27598

Description

Dagu is a workflow engine with a built-in Web user interface. In versions up to and including 1.16.7, the CreateNewDAG API endpoint (POST /api/v1/dags) does not validate the DAG name before passing it to the file store. An authenticated user with DAG write permissions can write arbitrary YAML files anywhere on the filesystem (limited by the process permissions). Since dagu executes DAG files as shell commands, writing a malicious DAG to the DAGs directory of another instance or overwriting config files can lead to remote code execution. Commit e2ed589105d79273e4e6ac8eb31525f765bb3ce4 fixes the issue.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Dagu workflow engine <=1.16.7 has a path traversal in the CreateNewDAG API allowing authenticated users to write arbitrary YAML files, leading to remote code execution.

Vulnerability

Details

The CreateNewDAG API endpoint (POST /api/v1/dags) in Dagu versions up to and including 1.16.7 fails to validate the DAG name before passing it to the file store. Unlike the RenameDAG endpoint, which calls core.ValidateDAGName() to reject names containing path separators, CreateNewDAG skips this validation entirely [3]. The underlying generateFilePath function in internal/persis/filedag/store.go checks if the name contains a path separator and, if so, resolves it via filepath.Abs(name), completely ignoring the configured base directory [3]. This allows an attacker to supply a name like ../../tmp/pwned to write a file to an arbitrary location on the filesystem.

Exploitation

An authenticated user with DAG write permissions can exploit this by sending a crafted POST request to the vulnerable endpoint. The attacker controls both the file name (via path traversal) and the YAML content (the DAG specification) [2][3]. The file is written with the permissions of the dagu process. Since Dagu executes DAG files as shell commands, writing a malicious DAG to the DAGs directory of another instance or overwriting configuration files can lead to remote code execution [2].

Impact

Successful exploitation allows an attacker to achieve remote code execution as the dagu process user. By creating a DAG that contains arbitrary shell commands, the attacker can execute those commands when the DAG is run. Additionally, overwriting configuration files could alter the behavior of the workflow engine or other components [2][3].

Mitigation

The vulnerability is fixed in commit e2ed589105d79273e4e6ac8eb31525f765bb3ce4 [2]. Users should update to a version containing this fix. No workaround is documented. The issue is tracked as GHSA-6v48-fcq6-ff23 [3].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/dagu-org/daguGo
<= 1.16.7

Affected products

2
  • Dagu Org/Dagullm-fuzzy2 versions
    <=1.16.7+ 1 more
    • (no CPE)range: <=1.16.7
    • (no CPE)range: <= 1.16.7

Patches

1
e2ed589105d7

fix: create API: path traversal bug (#1691)

https://github.com/dagu-org/daguYota HamadaFeb 21, 2026via ghsa
5 files changed · +95 8
  • internal/core/validator.go+3 0 modified
    @@ -36,6 +36,9 @@ func ValidateDAGName(name string) error {
     	if name == "" {
     		return nil
     	}
    +	if name == "." || name == ".." {
    +		return ErrNameInvalidChars
    +	}
     	if len(name) > DAGNameMaxLen {
     		return ErrNameTooLong
     	}
    
  • internal/persis/filedag/store.go+11 8 modified
    @@ -490,15 +490,18 @@ func (store *Storage) Rename(_ context.Context, oldID, newID string) error {
     }
     
     // generateFilePath generates the file path for a DAG by its name.
    +// It uses filepath.Base to strip directory components and verifies
    +// the result stays inside baseDir to prevent path traversal.
     func (store *Storage) generateFilePath(name string) string {
    -	if strings.Contains(name, string(filepath.Separator)) {
    -		filePath, err := filepath.Abs(name)
    -		if err == nil {
    -			return filePath
    -		}
    -	}
    -	filePath := fileutil.EnsureYAMLExtension(path.Join(store.baseDir, name))
    -	return filepath.Clean(filePath)
    +	safeName := filepath.Base(name)
    +	filePath := fileutil.EnsureYAMLExtension(path.Join(store.baseDir, safeName))
    +	filePath = filepath.Clean(filePath)
    +	// Verify the resolved path is inside baseDir.
    +	basePrefix := filepath.Clean(store.baseDir) + string(filepath.Separator)
    +	if !strings.HasPrefix(filePath, basePrefix) {
    +		return filepath.Join(store.baseDir, "_invalid.yaml")
    +	}
    +	return filePath
     }
     
     // locateDAG locates the DAG file by its name or path.
    
  • internal/persis/filedag/store_test.go+26 0 modified
    @@ -219,6 +219,32 @@ steps:
     	assert.Equal(t, exec.ErrDAGAlreadyExists, err)
     }
     
    +func TestGenerateFilePathPreventsTraversal(t *testing.T) {
    +	baseDir := "/base/dir"
    +	store := New(baseDir, WithSkipExamples(true)).(*Storage)
    +
    +	tests := []struct {
    +		name  string
    +		input string
    +	}{
    +		{"parent traversal", "../../tmp/pwned"},
    +		{"single parent", "../escape"},
    +		{"subdirectory", "foo/bar"},
    +		{"deep traversal", "../../../etc/malicious"},
    +		{"empty string", ""},
    +		{"dot dot", ".."},
    +		{"normal name", "my-dag"},
    +	}
    +
    +	for _, tc := range tests {
    +		t.Run(tc.name, func(t *testing.T) {
    +			result := store.generateFilePath(tc.input)
    +			assert.Equal(t, baseDir, filepath.Dir(result),
    +				"generated path must be directly inside baseDir")
    +		})
    +	}
    +}
    +
     func TestUpdateSpec(t *testing.T) {
     	tmpDir := fileutil.MustTempDir("test-update-spec")
     	defer func() {
    
  • internal/service/frontend/api/v1/dags.go+16 0 modified
    @@ -122,6 +122,22 @@ func (a *API) CreateNewDAG(ctx context.Context, request api.CreateNewDAGRequestO
     		return nil, err
     	}
     
    +	if request.Body.Name == "" {
    +		return nil, &Error{
    +			HTTPStatus: http.StatusBadRequest,
    +			Code:       api.ErrorCodeBadRequest,
    +			Message:    "DAG name must not be empty",
    +		}
    +	}
    +
    +	if err := core.ValidateDAGName(request.Body.Name); err != nil {
    +		return nil, &Error{
    +			HTTPStatus: http.StatusBadRequest,
    +			Code:       api.ErrorCodeBadRequest,
    +			Message:    err.Error(),
    +		}
    +	}
    +
     	var yamlSpec []byte
     	if request.Body.Spec != nil && strings.TrimSpace(*request.Body.Spec) != "" {
     		_, err := spec.LoadYAML(ctx,
    
  • internal/service/frontend/api/v1/dags_test.go+39 0 modified
    @@ -81,6 +81,45 @@ func TestDAGWritesAllowedWhenGitSyncDisabled(t *testing.T) {
     	server.Client().Delete("/api/v1/dags/test_dag_gitsync_disabled").ExpectStatus(http.StatusNoContent).Send(t)
     }
     
    +func TestCreateNewDAGPathTraversal(t *testing.T) {
    +	server := test.SetupServer(t)
    +
    +	traversalNames := []string{
    +		"../../tmp/traversal",
    +		"../escape",
    +		"foo/bar",
    +		"../../../etc/malicious",
    +	}
    +
    +	for _, name := range traversalNames {
    +		t.Run("with_spec/"+name, func(t *testing.T) {
    +			spec := "steps:\n  - command: echo test"
    +			server.Client().Post("/api/v1/dags", api.CreateNewDAGJSONRequestBody{
    +				Name: name,
    +				Spec: &spec,
    +			}).ExpectStatus(http.StatusBadRequest).Send(t)
    +		})
    +
    +		t.Run("without_spec/"+name, func(t *testing.T) {
    +			server.Client().Post("/api/v1/dags", api.CreateNewDAGJSONRequestBody{
    +				Name: name,
    +			}).ExpectStatus(http.StatusBadRequest).Send(t)
    +		})
    +	}
    +
    +	t.Run("empty_name", func(t *testing.T) {
    +		server.Client().Post("/api/v1/dags", api.CreateNewDAGJSONRequestBody{
    +			Name: "",
    +		}).ExpectStatus(http.StatusBadRequest).Send(t)
    +	})
    +
    +	t.Run("dot_dot_name", func(t *testing.T) {
    +		server.Client().Post("/api/v1/dags", api.CreateNewDAGJSONRequestBody{
    +			Name: "..",
    +		}).ExpectStatus(http.StatusBadRequest).Send(t)
    +	})
    +}
    +
     func TestDAG(t *testing.T) {
     	server := test.SetupServer(t)
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.