Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/operator-manual/upgrading/3.0-3.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# v3.0 to 3.1

## Symlink protection in API `--staticassets` directory

The `--staticassets` directory in the API server (`/app/shared` by default) is now protected against out-of-bounds
symlinks. This is to help protect against symlink attacks. If you have any symlinks in your `--staticassets` directory
to a location outside the directory, they will return a 500 error starting with 3.1.
4 changes: 3 additions & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,9 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts, appsetOpts Applicatio

var staticFS fs.FS = io.NewSubDirFS("dist/app", ui.Embedded)
if opts.StaticAssetsDir != "" {
staticFS = io.NewComposableFS(staticFS, os.DirFS(opts.StaticAssetsDir))
root, err := os.OpenRoot(opts.StaticAssetsDir)
errorsutil.CheckError(err)
staticFS = io.NewComposableFS(staticFS, root.FS())
}

argocdService, err := service.NewArgoCDService(opts.KubeClientset, opts.Namespace, opts.RepoClientset)
Expand Down
37 changes: 37 additions & 0 deletions server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1643,3 +1643,40 @@ func Test_enforceContentTypes(t *testing.T) {
assert.Equal(t, http.StatusUnsupportedMediaType, resp.StatusCode, "should not have passed, since a disallowed content type was provided")
})
}

func Test_StaticAssetsDir_no_symlink_traversal(t *testing.T) {
tmpDir := t.TempDir()
assetsDir := filepath.Join(tmpDir, "assets")
err := os.MkdirAll(assetsDir, os.ModePerm)
require.NoError(t, err)

// Create a file in temp dir
filePath := filepath.Join(tmpDir, "test.txt")
err = os.WriteFile(filePath, []byte("test"), 0o644)
require.NoError(t, err)

argocd, closer := fakeServer(t)
defer closer()

// Create a symlink to the file
symlinkPath := filepath.Join(argocd.StaticAssetsDir, "link.txt")
err = os.Symlink(filePath, symlinkPath)
require.NoError(t, err)

// Make a request to get the file from the /assets endpoint
req := httptest.NewRequest(http.MethodGet, "/link.txt", nil)
w := httptest.NewRecorder()
argocd.newStaticAssetsHandler()(w, req)
resp := w.Result()
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode, "should not have been able to access the symlinked file")

// Make sure a normal file works
normalFilePath := filepath.Join(argocd.StaticAssetsDir, "normal.txt")
err = os.WriteFile(normalFilePath, []byte("normal"), 0o644)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodGet, "/normal.txt", nil)
w = httptest.NewRecorder()
argocd.newStaticAssetsHandler()(w, req)
resp = w.Result()
assert.Equal(t, http.StatusOK, resp.StatusCode, "should have been able to access the normal file")
}
Loading