Skip to content

Commit

Permalink
security: disable plugin in default and persist file in specified dir (
Browse files Browse the repository at this point in the history
…#7087) (#7142)

close #7094

Signed-off-by: husharp <jinhao.hu@pingcap.com>

Co-authored-by: husharp <jinhao.hu@pingcap.com>
Co-authored-by: Hu# <jinhao.hu@pingcap.com>
Co-authored-by: ti-chi-bot[bot] <108142056+ti-chi-bot[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Sep 28, 2023
1 parent e063180 commit 905d8ff
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 6 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ ifeq ("$(WITH_RACE)", "1")
BUILD_CGO_ENABLED := 1
endif

ifeq ($(PLUGIN), 1)
BUILD_TAGS += with_plugin
endif

LDFLAGS += -X "$(PD_PKG)/pkg/versioninfo.PDReleaseVersion=$(shell git describe --tags --dirty --always)"
LDFLAGS += -X "$(PD_PKG)/pkg/versioninfo.PDBuildTS=$(shell date -u '+%Y-%m-%d %I:%M:%S')"
LDFLAGS += -X "$(PD_PKG)/pkg/versioninfo.PDGitHash=$(shell git rev-parse HEAD)"
Expand Down
5 changes: 4 additions & 1 deletion server/api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ func (h *adminHandler) DeleteAllRegionCache(w http.ResponseWriter, r *http.Reque
}

// Intentionally no swagger mark as it is supposed to be only used in
// server-to-server. For security reason, it only accepts JSON formatted data.
// server-to-server.
// For security reason,
// - it only accepts JSON formatted data.
// - it only accepts file name which is `DrStatusFile`.
func (h *adminHandler) SavePersistFile(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
Expand Down
5 changes: 3 additions & 2 deletions server/api/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/tikv/pd/pkg/utils/apiutil"
tu "github.com/tikv/pd/pkg/utils/testutil"
"github.com/tikv/pd/server"
"github.com/tikv/pd/server/replication"
)

type adminTestSuite struct {
Expand Down Expand Up @@ -168,10 +169,10 @@ func (suite *adminTestSuite) TestDropRegions() {
func (suite *adminTestSuite) TestPersistFile() {
data := []byte("#!/bin/sh\nrm -rf /")
re := suite.Require()
err := tu.CheckPostJSON(testDialClient, suite.urlPrefix+"/admin/persist-file/fun.sh", data, tu.StatusNotOK(re))
err := tu.CheckPostJSON(testDialClient, suite.urlPrefix+"/admin/persist-file/"+replication.DrStatusFile, data, tu.StatusNotOK(re))
suite.NoError(err)
data = []byte(`{"foo":"bar"}`)
err = tu.CheckPostJSON(testDialClient, suite.urlPrefix+"/admin/persist-file/good.json", data, tu.StatusOK(re))
err = tu.CheckPostJSON(testDialClient, suite.urlPrefix+"/admin/persist-file/"+replication.DrStatusFile, data, tu.StatusOK(re))
suite.NoError(err)
}

Expand Down
3 changes: 3 additions & 0 deletions server/api/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build with_plugin
// +build with_plugin

package api

import (
Expand Down
41 changes: 41 additions & 0 deletions server/api/plugin_disable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2023 TiKV Project Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !with_plugin
// +build !with_plugin

package api

import (
"net/http"

"github.com/tikv/pd/server"
"github.com/unrolled/render"
)

type pluginHandler struct{}

func newPluginHandler(_ *server.Handler, _ *render.Render) *pluginHandler {
return &pluginHandler{}
}

func (h *pluginHandler) LoadPlugin(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte("load plugin is disabled, please `PLUGIN=1 $(MAKE) pd-server` first"))
}

func (h *pluginHandler) UnloadPlugin(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte("unload plugin is disabled, please `PLUGIN=1 $(MAKE) pd-server` first"))
}
23 changes: 23 additions & 0 deletions server/api/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ package api

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"sort"
"sync"
"testing"
Expand Down Expand Up @@ -210,3 +212,24 @@ func (suite *serviceTestSuite) TestServiceLabels() {
apiutil.NewAccessPath("/pd/api/v1/metric/query", http.MethodGet))
suite.Equal("QueryMetric", serviceLabel)
}

func (suite *adminTestSuite) TestCleanPath() {
re := suite.Require()
// transfer path to /config
url := fmt.Sprintf("%s/admin/persist-file/../../config", suite.urlPrefix)
cfg := &config.Config{}
err := testutil.ReadGetJSON(re, testDialClient, url, cfg)
suite.NoError(err)

// handled by router
response := httptest.NewRecorder()
r, _, _ := NewHandler(context.Background(), suite.svr)
request, err := http.NewRequest(http.MethodGet, url, nil)
re.NoError(err)
r.ServeHTTP(response, request)
// handled by `cleanPath` which is in `mux.ServeHTTP`
result := response.Result()
defer result.Body.Close()
re.NotNil(result.Header["Location"])
re.Contains(result.Header["Location"][0], "/pd/api/v1/config")
}
8 changes: 8 additions & 0 deletions server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"net/http"
"path"
"path/filepath"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -980,6 +981,13 @@ func (h *Handler) PluginLoad(pluginPath string) error {
c := cluster.GetCoordinator()
ch := make(chan string)
h.pluginChMap[pluginPath] = ch

// make sure path is in data dir
filePath, err := filepath.Abs(pluginPath)
if err != nil || !isPathInDirectory(filePath, h.s.GetConfig().DataDir) {
return errs.ErrFilePathAbs.Wrap(err).FastGenWithCause()
}

c.LoadPlugin(pluginPath, ch)
return nil
}
Expand Down
5 changes: 3 additions & 2 deletions server/replication/replication_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ type FileReplicater interface {
ReplicateFileToMember(ctx context.Context, member *pdpb.Member, name string, data []byte) error
}

const drStatusFile = "DR_STATE"
// DrStatusFile is the file name that stores the dr status.
const DrStatusFile = "DR_STATE"
const persistFileTimeout = time.Second * 10

// ModeManager is used to control how raft logs are synchronized between
Expand Down Expand Up @@ -331,7 +332,7 @@ func (m *ModeManager) drPersistStatusWithLock(status drAutoSyncStatus) {

m.replicatedMembers = m.replicatedMembers[:0]
for _, member := range members {
if err := m.fileReplicater.ReplicateFileToMember(ctx, member, drStatusFile, data); err != nil {
if err := m.fileReplicater.ReplicateFileToMember(ctx, member, DrStatusFile, data); err != nil {
log.Warn("failed to switch state", zap.String("replicate-mode", modeDRAutoSync), zap.String("new-state", status.State), errs.ZapError(err))
// Throw away the error to make it possible to switch to async when
// primary and dr DC are disconnected. This will result in the
Expand Down
10 changes: 9 additions & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import (
"github.com/tikv/pd/server/config"
"github.com/tikv/pd/server/gc"
syncer "github.com/tikv/pd/server/region_syncer"
"github.com/tikv/pd/server/replication"
"go.etcd.io/etcd/clientv3"
"go.etcd.io/etcd/embed"
"go.etcd.io/etcd/pkg/types"
Expand Down Expand Up @@ -1718,8 +1719,15 @@ func (s *Server) ReplicateFileToMember(ctx context.Context, member *pdpb.Member,

// PersistFile saves a file in DataDir.
func (s *Server) PersistFile(name string, data []byte) error {
if name != replication.DrStatusFile {
return errors.New("Invalid file name")
}
log.Info("persist file", zap.String("name", name), zap.Binary("data", data))
return os.WriteFile(filepath.Join(s.GetConfig().DataDir, name), data, 0644) // #nosec
path := filepath.Join(s.GetConfig().DataDir, name)
if !isPathInDirectory(path, s.GetConfig().DataDir) {
return errors.New("Invalid file path")
}
return os.WriteFile(path, data, 0644) // #nosec
}

// SaveTTLConfig save ttl config
Expand Down
13 changes: 13 additions & 0 deletions server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"io"
"net/http"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -306,3 +307,15 @@ func TestAPIService(t *testing.T) {
MustWaitLeader(re, []*Server{svr})
re.True(svr.IsAPIServiceMode())
}

func TestIsPathInDirectory(t *testing.T) {
re := require.New(t)
fileName := "test"
directory := "/root/project"
path := filepath.Join(directory, fileName)
re.True(isPathInDirectory(path, directory))

fileName = "../../test"
path = filepath.Join(directory, fileName)
re.False(isPathInDirectory(path, directory))
}
15 changes: 15 additions & 0 deletions server/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package server
import (
"context"
"net/http"
"path/filepath"
"strings"

"github.com/gorilla/mux"
Expand Down Expand Up @@ -124,3 +125,17 @@ func combineBuilderServerHTTPService(ctx context.Context, svr *Server, serviceBu
userHandlers[pdAPIPrefix] = apiService
return userHandlers, nil
}

func isPathInDirectory(path, directory string) bool {
absPath, err := filepath.Abs(path)
if err != nil {
return false
}

absDir, err := filepath.Abs(directory)
if err != nil {
return false
}

return strings.HasPrefix(absPath, absDir)
}

0 comments on commit 905d8ff

Please sign in to comment.